// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; import 'package:test/test.dart'; import 'package:test_api/src/backend/group.dart'; import 'package:test_api/src/backend/invoker.dart'; import 'package:test_api/src/backend/live_test.dart'; import 'package:test_api/src/backend/message.dart'; import 'package:test_api/src/backend/metadata.dart'; import 'package:test_api/src/backend/state.dart'; import 'package:test_api/src/backend/suite.dart'; import '../utils.dart'; void main() { late Suite suite; setUp(() { lastState = null; suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false); }); group('Invoker.current', () { var invoker = Invoker.current; test('returns null outside of a test body', () { expect(invoker, isNull); }); test('returns the current invoker in a test body', () async { late Invoker invoker; var liveTest = _localTest(() { invoker = Invoker.current!; }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); await liveTest.run(); expect(invoker.liveTest, equals(liveTest)); }); test( 'returns the current invoker in a test body after the test completes', () async { Status? status; var completer = Completer(); var liveTest = _localTest(() { // Use the event loop to wait longer than a microtask for the test to // complete. Future(() { status = Invoker.current!.liveTest.state.status; completer.complete(Invoker.current); }); }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); expect(liveTest.run(), completes); var invoker = await completer.future; expect(invoker.liveTest, equals(liveTest)); expect(status, equals(Status.complete)); }, ); }); group('in a successful test,', () { test('the state changes from pending to running to complete', () async { late State stateInTest; late LiveTest liveTest; liveTest = _localTest(() { stateInTest = liveTest.state; }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); expect(liveTest.state.status, equals(Status.pending)); expect(liveTest.state.result, equals(Result.success)); var future = liveTest.run(); expect(liveTest.state.status, equals(Status.running)); expect(liveTest.state.result, equals(Result.success)); await future; expect(stateInTest.status, equals(Status.running)); expect(stateInTest.result, equals(Result.success)); expect(liveTest.state.status, equals(Status.complete)); expect(liveTest.state.result, equals(Result.success)); }); test('onStateChange fires for each state change', () { var liveTest = _localTest(() {}).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); var first = true; liveTest.onStateChange.listen( expectAsync1( (state) { if (first) { expect(state.status, equals(Status.running)); first = false; } else { expect(state.status, equals(Status.complete)); } expect(state.result, equals(Result.success)); }, count: 2, max: 2, ), ); return liveTest.run(); }); test('onComplete completes once the test body is done', () { var testRun = false; var liveTest = _localTest(() { testRun = true; }).load(suite); expect( liveTest.onComplete.then((_) { expect(testRun, isTrue); }), completes, ); return liveTest.run(); }); }); group('in a test with failures,', () { test('a synchronous throw is reported and causes the test to fail', () { var liveTest = _localTest(() { throw TestFailure('oh no'); }).load(suite); expectSingleFailure(liveTest); return liveTest.run(); }); test('a synchronous reported failure causes the test to fail', () { var liveTest = _localTest(() { registerException(TestFailure('oh no')); }).load(suite); expectSingleFailure(liveTest); return liveTest.run(); }); test( 'a failure reported asynchronously during the test causes it to fail', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future( () => registerException(TestFailure('oh no')), ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectSingleFailure(liveTest); return liveTest.run(); }, ); test( 'a failure thrown asynchronously during the test causes it to fail', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future( () => throw TestFailure('oh no'), ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectSingleFailure(liveTest); return liveTest.run(); }, ); test( 'a failure reported asynchronously after the test causes it to error', () { var liveTest = _localTest(() { Future(() => registerException(TestFailure('oh no'))); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.success), const State(Status.complete, Result.failure), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect( lastState, equals(const State(Status.complete, Result.failure)), ); expect(error, isTestFailure('oh no')); }, (error) { expect( lastState, equals(const State(Status.complete, Result.error)), ); expect( error, equals( 'This test failed after it had already completed.\n' 'Make sure to use a matching library which informs the ' 'test runner\nof pending async work.', ), ); }, ]); return liveTest.run(); }, ); test('multiple asynchronous failures are reported', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future(() => throw TestFailure('one')); Future(() => throw TestFailure('two')); Future(() => throw TestFailure('three')); Future( () => throw TestFailure('four'), ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.failure), ]); expectErrors(liveTest, [ (error) { expect(lastState?.status, equals(Status.complete)); expect(error, isTestFailure('one')); }, (error) { expect(error, isTestFailure('two')); }, (error) { expect(error, isTestFailure('three')); }, (error) { expect(error, isTestFailure('four')); }, ]); return liveTest.run(); }); test("a failure after an error doesn't change the state of the test", () { var liveTest = _localTest(() { Future(() => throw TestFailure('fail')); throw 'error'; }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect(lastState, equals(const State(Status.complete, Result.error))); expect(error, equals('error')); }, (error) { expect(error, isTestFailure('fail')); }, ]); return liveTest.run(); }); }); group('in a test with errors,', () { test('a synchronous throw is reported and causes the test to error', () { var liveTest = _localTest(() { throw 'oh no'; }).load(suite); expectSingleError(liveTest); return liveTest.run(); }); test('a synchronous reported error causes the test to error', () { var liveTest = _localTest(() { registerException('oh no'); }).load(suite); expectSingleError(liveTest); return liveTest.run(); }); test( 'an error reported asynchronously during the test causes it to error', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future( () => registerException('oh no'), ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectSingleError(liveTest); return liveTest.run(); }, ); test( 'an error thrown asynchronously during the test causes it to error', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future( () => throw 'oh no', ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectSingleError(liveTest); return liveTest.run(); }, ); test( 'an error reported asynchronously after the test causes it to error', () { var liveTest = _localTest(() { Future(() => registerException('oh no')); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.success), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect( lastState, equals(const State(Status.complete, Result.error)), ); expect(error, equals('oh no')); }, (error) { expect( error, equals( 'This test failed after it had already completed.\n' 'Make sure to use a matching library which informs the ' 'test runner\nof pending async work.', ), ); }, ]); return liveTest.run(); }, ); test('multiple asynchronous errors are reported', () { var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); Future(() => throw 'one'); Future(() => throw 'two'); Future(() => throw 'three'); Future( () => throw 'four', ).whenComplete(Invoker.current!.removeOutstandingCallback); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect(lastState?.status, equals(Status.complete)); expect(error, equals('one')); }, (error) { expect(error, equals('two')); }, (error) { expect(error, equals('three')); }, (error) { expect(error, equals('four')); }, ]); return liveTest.run(); }); test('an error after a failure changes the state of the test', () { var liveTest = _localTest(() { Future(() => throw 'error'); throw TestFailure('fail'); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.failure), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect( lastState, equals(const State(Status.complete, Result.failure)), ); expect(error, isTestFailure('fail')); }, (error) { expect(lastState, equals(const State(Status.complete, Result.error))); expect(error, equals('error')); }, ]); return liveTest.run(); }); }); test( "a test doesn't complete until there are no outstanding callbacks", () async { var outstandingCallbackRemoved = false; var liveTest = _localTest(() { Invoker.current!.addOutstandingCallback(); // Pump the event queue to make sure the test isn't coincidentally // completing after the outstanding callback is removed. pumpEventQueue().then((_) { outstandingCallbackRemoved = true; Invoker.current!.removeOutstandingCallback(); }); }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); await liveTest.run(); expect(outstandingCallbackRemoved, isTrue); }, ); test("a test's prints are captured and reported", () { expect(() { var liveTest = _localTest(() { print('Hello,'); return Future(() => print('world!')); }).load(suite); expect( liveTest.onMessage.take(2).toList().then((messages) { expect(messages[0].type, equals(MessageType.print)); expect(messages[0].text, equals('Hello,')); expect(messages[1].type, equals(MessageType.print)); expect(messages[1].text, equals('world!')); }), completes, ); return liveTest.run(); }, prints(isEmpty)); }); group('timeout:', () { test('A test can be timed out', () { var liveTest = _localTest( () { Invoker.current!.addOutstandingCallback(); }, metadata: Metadata( chainStackTraces: true, timeout: const Timeout(Duration.zero), ), ).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.error), ]); expectErrors(liveTest, [ (error) { expect(lastState!.status, equals(Status.complete)); expect(error, const TypeMatcher()); }, ]); liveTest.run(); }); test('can be ignored', () { suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: true); var liveTest = _localTest( () async { await Future.delayed(const Duration(milliseconds: 10)); }, metadata: Metadata( chainStackTraces: true, timeout: const Timeout(Duration.zero), ), ).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.success), ]); liveTest.run(); }); }); group('runTearDowns', () { test('runs multiple tear downs', () async { var firstTearDownStarted = false; var secondTearDownStarted = false; await Invoker.current!.runTearDowns([ () { firstTearDownStarted = true; }, () { secondTearDownStarted = true; }, ]); expect(secondTearDownStarted, isTrue); expect(firstTearDownStarted, isTrue); }); test('waits for the future returned tear downs to complete', () async { var firstTearDownWork = Completer(); var secondTearDownStarted = false; var result = Invoker.current!.runTearDowns([ () { secondTearDownStarted = true; }, () async { await firstTearDownWork.future; }, ]); await pumpEventQueue(); expect(secondTearDownStarted, isFalse); firstTearDownWork.complete(); await result; expect(secondTearDownStarted, isTrue); }); test( 'allows next tear down to run while there are still prior callbacks', () async { var firstTearDownAsyncWork = Completer(); var secondTearDownStarted = false; unawaited( Invoker.current!.runTearDowns([ () { secondTearDownStarted = true; }, () { Invoker.current!.addOutstandingCallback(); firstTearDownAsyncWork.future.whenComplete( Invoker.current!.removeOutstandingCallback, ); }, ]), ); await pumpEventQueue(); expect(secondTearDownStarted, isTrue); firstTearDownAsyncWork.complete(); }, ); test('forwards errors to the enclosing test but does not end it', () async { var liveTest = _localTest(() async { Invoker.current!.addOutstandingCallback(); await Invoker.current!.runTearDowns([ () { throw 'oh no'; }, ]); }).load(suite); expectStates(liveTest, [ const State(Status.running, Result.success), const State(Status.complete, Result.error), ]); var isComplete = false; unawaited(liveTest.run().then((_) => isComplete = true)); await pumpEventQueue(); expect(liveTest.state.status, equals(Status.complete)); expect(isComplete, isFalse); }); }); group('printOnFailure:', () { test("doesn't print anything if the test succeeds", () async { var liveTest = _localTest(() { Invoker.current!.printOnFailure('only on failure'); }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 0)); liveTest.onMessage.listen(expectAsync1((_) {}, count: 0)); await liveTest.run(); }); test('prints if the test fails', () async { var liveTest = _localTest(() { Invoker.current!.printOnFailure('only on failure'); expect(true, isFalse); }).load(suite); liveTest.onError.listen(expectAsync1((_) {}, count: 1)); liveTest.onMessage.listen( expectAsync1((message) { expect(message.type, equals(MessageType.print)); expect(message.text, equals('only on failure')); }, count: 1), ); await liveTest.run(); }); }); } LocalTest _localTest(dynamic Function() body, {Metadata? metadata}) { metadata ??= Metadata(chainStackTraces: true); return LocalTest('test', metadata, body); }