diff --git a/pkgs/checks/CHANGELOG.md b/pkgs/checks/CHANGELOG.md index 6b60a8b36..76ce89091 100644 --- a/pkgs/checks/CHANGELOG.md +++ b/pkgs/checks/CHANGELOG.md @@ -4,6 +4,15 @@ - `checkThat` renamed to `check`. - `nest` and `nestAsync` take `Iterable Function()` arguments for `label` instead of `String`. + - Async expectation extensions `completes`, `throws`, `emits`, and + `emitsError` no longer return a `Future`. Instead they take an + optional `Condition` argument which can check expectations that would + have been checked on the returned subject. + - `nestAsync` no longer returns a `Subject`, callers must pass the + followup `Condition` to the nullable argument. + - Remove the `which` extension on `Future`. +- Add a constructor for `Condition` which takes a callback to invoke when + `apply` or `applyAsync` is called. - Added an example. - Include a stack trace in the failure description for unexpected errors from Futures or Streams. diff --git a/pkgs/checks/README.md b/pkgs/checks/README.md index d72bd225f..80987831b 100644 --- a/pkgs/checks/README.md +++ b/pkgs/checks/README.md @@ -50,9 +50,8 @@ condition. The `it()` utility returns a `ConditionSubject`. check(someList).any(it()..isGreaterThan(0)); ``` -Some complicated checks may be difficult to write with parenthesized awaited -expressions, or impossible to write with cascade syntax. There are `which` -utilities for both use cases which take a `Condition`. +Some complicated checks may be not be possible to write with cascade syntax. +There is a `which` utility for this use case which takes a `Condition`. ```dart check(someString) @@ -61,10 +60,6 @@ check(someString) ..length.which(it() ..isGreatherThan(10) ..isLessThan(100)); - -await check(someFuture) - .completes() - .which(it()..equals(expectedCompletion)); ``` # Writing custom expectations diff --git a/pkgs/checks/lib/checks.dart b/pkgs/checks/lib/checks.dart index 2bcb38cdc..930787997 100644 --- a/pkgs/checks/lib/checks.dart +++ b/pkgs/checks/lib/checks.dart @@ -4,7 +4,7 @@ export 'src/checks.dart' show SkipExtension, Subject, check, it; export 'src/extensions/async.dart' - show ChainAsync, FutureChecks, StreamChecks, WithQueueExtension; + show FutureChecks, StreamChecks, WithQueueExtension; export 'src/extensions/core.dart' show BoolChecks, CoreChecks, NullableChecks; export 'src/extensions/function.dart' show FunctionChecks; export 'src/extensions/iterable.dart' show IterableChecks; diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart index 700ddfb1f..364513e1d 100644 --- a/pkgs/checks/lib/src/checks.dart +++ b/pkgs/checks/lib/src/checks.dart @@ -178,8 +178,17 @@ Future> describeAsync(Condition condition) async { /// A set of expectations that are checked against the value when applied to a /// [Subject]. +/// +/// This class should not be implemented or extended. abstract class Condition { + /// Check the expectations of this condition against [subject]. + /// + /// The [subject] should throw if any asynchronous expectations are checked. + /// It is not possible to wait for for asynchronous expectations to be fully + /// applied with this method. void apply(Subject subject); + + /// Check the expectations of this condition against [subject]. Future applyAsync(Subject subject); } @@ -481,6 +490,9 @@ abstract class Context { /// [Extracted.rejection] describing the problem. Otherwise it should return /// an [Extracted.value]. /// + /// Subsequent expectations can be checked for the extracted value on the + /// returned [Subject]. + /// /// {@macro label_description} /// /// If [atSameLevel] is true then the returned [Extracted.value] should hold @@ -518,6 +530,10 @@ abstract class Context { /// [Extracted.rejection] describing the problem. Otherwise it should return /// an [Extracted.value]. /// + /// In contrast to [nest], subsequent expectations need to be passed in + /// [nestedCondition] which will be applied to the subject for the extracted + /// value. + /// /// {@macro label_description} /// /// {@macro description_lines} @@ -527,17 +543,19 @@ abstract class Context { /// {@macro async_limitations} /// /// ```dart - /// Future> get someAsyncValue async => await context - /// .nestAsync(() => ['has someAsyncValue'], (actual) async { - /// if (await _cannotReadAsyncValue(actual)) { - /// return Extracted.rejection( - /// which: ['cannot read someAsyncValue']); - /// } - /// return Extracted.value(await _readAsyncValue(actual)); - /// }); + /// Future someAsyncResult([Condition resultCondition]) async { + /// await context.nestAsync(() => ['has someAsyncResult'], (actual) async { + /// if (await _asyncOperationFailed(actual)) { + /// return Extracted.rejection(which: ['cannot read someAsyncResult']); + /// } + /// return Extracted.value(await _readAsyncResult(actual)); + /// }, resultCondition); + /// } /// ``` - Future> nestAsync(Iterable Function() label, - FutureOr> Function(T) extract); + Future nestAsync( + Iterable Function() label, + FutureOr> Function(T) extract, + Condition? nestedCondition); } /// A property extracted from a value being checked, or a rejection. @@ -752,8 +770,10 @@ class _TestContext implements Context, _ClauseDescription { } @override - Future> nestAsync(Iterable Function() label, - FutureOr> Function(T) extract) async { + Future nestAsync( + Iterable Function() label, + FutureOr> Function(T) extract, + Condition? nestedCondition) async { if (!_allowAsync) { throw StateError( 'Async expectations cannot be used on a synchronous subject'); @@ -770,7 +790,7 @@ class _TestContext implements Context, _ClauseDescription { final value = result._value ?? _Absent(); final context = _TestContext._child(value, label, this); _clauses.add(context); - return Subject._(context); + await nestedCondition?.applyAsync(Subject._(context)); } CheckFailure _failure(Rejection rejection) => @@ -843,9 +863,11 @@ class _SkippedContext implements Context { } @override - Future> nestAsync(Iterable Function() label, - FutureOr> Function(T p1) extract) async { - return Subject._(_SkippedContext()); + Future nestAsync( + Iterable Function() label, + FutureOr> Function(T p1) extract, + Condition? nestedCondition) async { + // no-op } } @@ -1066,13 +1088,12 @@ class _ReplayContext implements Context, Condition { } @override - Future> nestAsync(Iterable Function() label, - FutureOr> Function(T) extract) async { - final nestedContext = _ReplayContext(); + Future nestAsync( + Iterable Function() label, + FutureOr> Function(T) extract, + Condition? nestedCondition) async { _interactions.add((c) async { - var result = await c.nestAsync(label, extract); - await nestedContext.applyAsync(result); + await c.nestAsync(label, extract, nestedCondition); }); - return Subject._(nestedContext); } } diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart index 11c80f5a7..0323b481b 100644 --- a/pkgs/checks/lib/src/extensions/async.dart +++ b/pkgs/checks/lib/src/extensions/async.dart @@ -11,23 +11,26 @@ import 'package:checks/context.dart'; extension FutureChecks on Subject> { /// Expects that the `Future` completes to a value without throwing. /// - /// Returns a future that completes to a [Subject] on the result once the - /// future completes. - /// /// Fails if the future completes as an error. - Future> completes() => - context.nestAsync(() => ['completes to a value'], (actual) async { - try { - return Extracted.value(await actual); - } catch (e, st) { - return Extracted.rejection(actual: [ - 'a future that completes as an error' - ], which: [ - ...prefixFirst('threw ', postfixLast(' at:', literal(e))), - ...(const LineSplitter()).convert(st.toString()) - ]); - } - }); + /// + /// Pass [completionCondition] to check expectations on the completion result. + /// + /// The returned future will complete when the subject future has completed, + /// and [completionCondition] has optionally been checked. + Future completes([Condition? completionCondition]) async { + await context.nestAsync(() => ['completes to a value'], (actual) async { + try { + return Extracted.value(await actual); + } catch (e, st) { + return Extracted.rejection(actual: [ + 'a future that completes as an error' + ], which: [ + ...prefixFirst('threw ', postfixLast(' at:', literal(e))), + ...(const LineSplitter()).convert(st.toString()) + ]); + } + }, completionCondition); + } /// Expects that the `Future` never completes as a value or an error. /// @@ -56,28 +59,33 @@ extension FutureChecks on Subject> { /// Expects that the `Future` completes as an error. /// - /// Returns a future that completes to a [Subject] on the error once the - /// future completes as an error. - /// /// Fails if the future completes to a value. - Future> throws() => context.nestAsync( - () => ['completes to an error${E == Object ? '' : ' of type $E'}'], - (actual) async { - try { - return Extracted.rejection( - actual: prefixFirst('completed to ', literal(await actual)), - which: ['did not throw']); - } on E catch (e) { - return Extracted.value(e); - } catch (e, st) { - return Extracted.rejection( - actual: prefixFirst('completed to error ', literal(e)), - which: [ - 'threw an exception that is not a $E at:', - ...(const LineSplitter()).convert(st.toString()) - ]); - } - }); + /// + /// Pass [errorCondition] to check expectations on the error thrown by the + /// future. + /// + /// The returned future will complete when the subject future has completed, + /// and [errorCondition] has optionally been checked. + Future throws([Condition? errorCondition]) async { + await context.nestAsync( + () => ['completes to an error${E == Object ? '' : ' of type $E'}'], + (actual) async { + try { + return Extracted.rejection( + actual: prefixFirst('completed to ', literal(await actual)), + which: ['did not throw']); + } on E catch (e) { + return Extracted.value(e); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('completed to error ', literal(e)), + which: [ + 'threw an exception that is not a $E at:', + ...(const LineSplitter()).convert(st.toString()) + ]); + } + }, errorCondition); + } } /// Expectations on a [StreamQueue]. @@ -104,67 +112,82 @@ extension StreamChecks on Subject> { /// Expect that the `Stream` emits a value without first emitting an error. /// - /// Returns a `Future` that completes to a [Subject] on the next event emitted - /// by the stream. - /// /// Fails if the stream emits an error instead of a value, or closes without /// emitting a value. - Future> emits() => - context.nestAsync(() => ['emits a value'], (actual) async { - if (!await actual.hasNext) { - return Extracted.rejection( - actual: ['a stream'], - which: ['closed without emitting enough values']); - } - try { - await actual.peek; - return Extracted.value(await actual.next); - } catch (e, st) { - return Extracted.rejection( - actual: prefixFirst('a stream with error ', literal(e)), - which: [ - 'emitted an error instead of a value at:', - ...(const LineSplitter()).convert(st.toString()) - ]); - } - }); + /// + /// If an error is emitted the queue will be left in its original state, the + /// error will not be consumed. + /// If an event is emitted, it will be consumed from the queue. + /// + /// Pass [emittedCondition] to check expectations on the value emitted by the + /// stream. + /// + /// The returned future will complete when the stream has emitted, errored, or + /// ended, and the [emittedCondition] has optionally been checked. + Future emits([Condition? emittedCondition]) async { + await context.nestAsync(() => ['emits a value'], (actual) async { + if (!await actual.hasNext) { + return Extracted.rejection( + actual: ['a stream'], + which: ['closed without emitting enough values']); + } + try { + await actual.peek; + return Extracted.value(await actual.next); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('a stream with error ', literal(e)), + which: [ + 'emitted an error instead of a value at:', + ...(const LineSplitter()).convert(st.toString()) + ]); + } + }, emittedCondition); + } /// Expects that the stream emits an error of type [E]. /// - /// Returns a [Subject] on the error's value. - /// /// Fails if the stream emits any value. /// Fails if the stream emits an error with an incorrect type. /// Fails if the stream closes without emitting an error. /// - /// If this expectation fails, the source queue will be left in it's original - /// state. - /// If this expectation succeeds, consumes the error event. - Future> emitsError() => context.nestAsync( - () => ['emits an error${E == Object ? '' : ' of type $E'}'], - (actual) async { - if (!await actual.hasNext) { - return Extracted.rejection( - actual: ['a stream'], - which: ['closed without emitting an expected error']); - } - try { - final value = await actual.peek; - return Extracted.rejection( - actual: prefixFirst('a stream emitting value ', literal(value)), - which: ['closed without emitting an error']); - } on E catch (e) { - await actual.next.then((_) {}, onError: (_) {}); - return Extracted.value(e); - } catch (e, st) { - return Extracted.rejection( - actual: prefixFirst('a stream with error ', literal(e)), - which: [ - 'emitted an error which is not $E at:', - ...(const LineSplitter()).convert(st.toString()) - ]); - } - }); + /// If an event is emitted the queue will be left in its original state, the + /// event will not be consumed. + /// If an error is emitted, it will be consumed from the queue. + /// + /// Pass [errorCondition] to check expectations on the error emitted by the + /// stream. + /// + /// The returned future will complete when the stream has emitted, errored, or + /// ended, and the [errorCondition] has optionally been checked. + Future emitsError( + [Condition? errorCondition]) async { + await context.nestAsync( + () => ['emits an error${E == Object ? '' : ' of type $E'}'], + (actual) async { + if (!await actual.hasNext) { + return Extracted.rejection( + actual: ['a stream'], + which: ['closed without emitting an expected error']); + } + try { + final value = await actual.peek; + return Extracted.rejection( + actual: prefixFirst('a stream emitting value ', literal(value)), + which: ['closed without emitting an error']); + } on E catch (e) { + await actual.next.then((_) {}, onError: (_) {}); + return Extracted.value(e); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('a stream with error ', literal(e)), + which: [ + 'emitted an error which is not $E at:', + ...(const LineSplitter()).convert(st.toString()) + ]); + } + }, errorCondition); + } /// Expects that the `Stream` emits any number of events before emitting an /// event that satisfies [condition]. @@ -436,24 +459,6 @@ extension StreamChecks on Subject> { } } -extension ChainAsync on Future> { - /// Checks the expectations in [condition] against the result of this - /// `Future`. - /// - /// Extensions written on [Subject] cannot be invoked on a `Future`. - /// This method allows adding expectations for the value without awaiting an - /// expression that would need parenthesis. - /// - /// ```dart - /// await check(someFuture).completes().which(it()..equals('expected')); - /// // or, with the intermediate `await`: - /// (await check(someFuture).completes()).equals('expected'); - /// ``` - Future which(Condition condition) async { - await condition.applyAsync(await this); - } -} - extension WithQueueExtension on Subject> { /// Wrap the stream in a [StreamQueue] to allow using checks from /// [StreamChecks]. diff --git a/pkgs/checks/test/extensions/async_test.dart b/pkgs/checks/test/extensions/async_test.dart index 2c77cd3d9..6d0abe021 100644 --- a/pkgs/checks/test/extensions/async_test.dart +++ b/pkgs/checks/test/extensions/async_test.dart @@ -16,22 +16,20 @@ void main() { group('FutureChecks', () { group('completes', () { test('succeeds for a future that completes to a value', () async { - await check(_futureSuccess()).completes().which(it()..equals(42)); + await check(_futureSuccess()).completes(it()..equals(42)); }); test('rejects futures which complete as errors', () async { await check(_futureFail()).isRejectedByAsync( - it()..completes().which(it()..equals(1)), + it()..completes(it()..equals(1)), actual: ['a future that completes as an error'], which: ['threw at:', 'fake trace'], ); }); test('can be described', () async { - await check(it>()..completes()) - .asyncDescription - .which(it()..deepEquals([' completes to a value'])); - await check(it>()..completes().which(it()..equals(42))) - .asyncDescription - .which(it() + await check(it>()..completes()).hasAsyncDescriptionWhich( + it()..deepEquals([' completes to a value'])); + await check(it>()..completes(it()..equals(42))) + .hasAsyncDescriptionWhich(it() ..deepEquals([ ' completes to a value that:', ' equals <42>', @@ -43,9 +41,8 @@ void main() { test( 'succeeds for a future that compeletes to an error of the expected type', () async { - await check(_futureFail()) - .throws() - .which(it()..has((p0) => p0.message, 'message').isNull()); + await check(_futureFail()).throws( + it()..has((p0) => p0.message, 'message').isNull()); }); test('fails for futures that complete to a value', () async { await check(_futureSuccess()).isRejectedByAsync( @@ -66,12 +63,10 @@ void main() { ); }); test('can be described', () async { - await check(it>()..throws()) - .asyncDescription - .which(it()..deepEquals([' completes to an error'])); + await check(it>()..throws()).hasAsyncDescriptionWhich( + it()..deepEquals([' completes to an error'])); await check(it>()..throws()) - .asyncDescription - .which(it() + .hasAsyncDescriptionWhich(it() ..deepEquals([' completes to an error of type StateError'])); }); }); @@ -122,8 +117,8 @@ fake trace'''); }); test('can be described', () async { await check(it>()..doesNotComplete()) - .asyncDescription - .which(it()..deepEquals([' does not complete'])); + .hasAsyncDescriptionWhich( + it()..deepEquals([' does not complete'])); }); }); }); @@ -131,7 +126,7 @@ fake trace'''); group('StreamChecks', () { group('emits', () { test('succeeds for a stream that emits a value', () async { - await check(_countingStream(5)).emits().which(it()..equals(0)); + await check(_countingStream(5)).emits(it()..equals(0)); }); test('fails for a stream that closes without emitting', () async { await check(_countingStream(0)).isRejectedByAsync( @@ -149,17 +144,15 @@ fake trace'''); }); test('can be described', () async { await check(it>()..emits()) - .asyncDescription - .which(it()..deepEquals([' emits a value'])); - await check(it>()..emits().which(it()..equals(42))) - .asyncDescription - .which(it() + .hasAsyncDescriptionWhich(it()..deepEquals([' emits a value'])); + await check(it>()..emits(it()..equals(42))) + .hasAsyncDescriptionWhich(it() ..deepEquals([ ' emits a value that:', ' equals <42>', ])); }); - test('uses a transaction', () async { + test('does not consume error', () async { final queue = _countingStream(1, errorAt: 0); await softCheckAsync>(queue, it()..emits()); await check(queue).emitsError(); @@ -196,16 +189,14 @@ fake trace'''); }); test('can be described', () async { await check(it>()..emitsError()) - .asyncDescription - .which(it()..deepEquals([' emits an error'])); + .hasAsyncDescriptionWhich(it()..deepEquals([' emits an error'])); await check(it>()..emitsError()) - .asyncDescription - .which(it()..deepEquals([' emits an error of type StateError'])); + .hasAsyncDescriptionWhich( + it()..deepEquals([' emits an error of type StateError'])); await check(it>() - ..emitsError() - .which(it()..has((e) => e.message, 'message').equals('foo'))) - .asyncDescription - .which(it() + ..emitsError( + it()..has((e) => e.message, 'message').equals('foo'))) + .hasAsyncDescriptionWhich(it() ..deepEquals([ ' emits an error of type StateError that:', ' has message that:', @@ -215,7 +206,7 @@ fake trace'''); test('uses a transaction', () async { final queue = _countingStream(1); await softCheckAsync>(queue, it()..emitsError()); - await check(queue).emits().which((it()..equals(0))); + await check(queue).emits((it()..equals(0))); }); }); @@ -234,8 +225,7 @@ fake trace'''); }); test('can be described', () async { await check(it>()..emitsThrough(it()..equals(42))) - .asyncDescription - .which(it() + .hasAsyncDescriptionWhich(it() ..deepEquals([ ' emits any values then emits a value that:', ' equals <42>' @@ -245,20 +235,20 @@ fake trace'''); final queue = _countingStream(1); await softCheckAsync( queue, it>()..emitsThrough(it()..equals(42))); - check(queue).emits().which(it()..equals(0)); + check(queue).emits(it()..equals(0)); }); test('consumes events', () async { final queue = _countingStream(3); await check(queue).emitsThrough(it()..equals(1)); - await check(queue).emits().which((it()..equals(2))); + await check(queue).emits((it()..equals(2))); }); }); group('emitsInOrder', () { test('succeeds for happy case', () async { await check(_countingStream(2)).inOrder([ - it()..emits().which(it()..equals(0)), - it()..emits().which((it()..equals(1))), + it()..emits(it()..equals(0)), + it()..emits(it()..equals(1)), it()..isDone(), ]); }); @@ -275,7 +265,7 @@ fake trace'''); }); test('nestes the report for deep failures', () async { await check(_countingStream(2)).isRejectedByAsync( - it()..inOrder([it()..emits(), it()..emits().which(it()..equals(2))]), + it()..inOrder([it()..emits(), it()..emits(it()..equals(2))]), actual: ['a stream'], which: [ 'satisfied 1 conditions then', @@ -289,8 +279,8 @@ fake trace'''); }); test('gets described with the number of conditions', () async { await check(it>()..inOrder([it(), it()])) - .asyncDescription - .which(it()..deepEquals([' satisfies 2 conditions in order'])); + .hasAsyncDescriptionWhich( + it()..deepEquals([' satisfies 2 conditions in order'])); }); test('uses a transaction', () async { final queue = _countingStream(3); @@ -298,21 +288,21 @@ fake trace'''); queue, it() ..inOrder([ - it()..emits().which(it()..equals(0)), - it()..emits().which(it()..equals(1)), - it()..emits().which(it()..equals(42)), + it()..emits(it()..equals(0)), + it()..emits(it()..equals(1)), + it()..emits(it()..equals(42)), ])); await check(queue).inOrder([ - it()..emits().which(it()..equals(0)), - it()..emits().which(it()..equals(1)), - it()..emits().which(it()..equals(2)), + it()..emits(it()..equals(0)), + it()..emits(it()..equals(1)), + it()..emits(it()..equals(2)), it()..isDone(), ]); }); test('consumes events', () async { final queue = _countingStream(3); await check(queue).inOrder([it()..emits(), it()..emits()]); - await check(queue).emits().which(it()..equals(2)); + await check(queue).emits(it()..equals(2)); }); }); @@ -331,8 +321,7 @@ fake trace'''); }); test('can be described', () async { await check(it>()..neverEmits(it()..equals(42))) - .asyncDescription - .which(it() + .hasAsyncDescriptionWhich(it() ..deepEquals([ ' never emits a value that:', ' equals <42>', @@ -343,8 +332,8 @@ fake trace'''); await softCheckAsync>( queue, it()..neverEmits(it()..equals(1))); await check(queue).inOrder([ - it()..emits().which(it()..equals(0)), - it()..emits().which(it()..equals(1)), + it()..emits(it()..equals(0)), + it()..emits(it()..equals(1)), it()..isDone(), ]); }); @@ -364,21 +353,20 @@ fake trace'''); final queue = _countingStream(2); await softCheckAsync>( queue, it()..mayEmit(it()..equals(0))); - await check(queue).emits().which(it()..equals(1)); + await check(queue).emits(it()..equals(1)); }); test('does not consume a non-matching event', () async { final queue = _countingStream(2); await softCheckAsync>( queue, it()..mayEmit(it()..equals(1))); - await check(queue).emits().which(it()..equals(0)); + await check(queue).emits(it()..equals(0)); }); test('does not consume an error', () async { final queue = _countingStream(1, errorAt: 0); await softCheckAsync>( queue, it()..mayEmit(it()..equals(0))); - await check(queue) - .emitsError() - .which(it()..has((e) => e.message, 'message').equals('Error at 1')); + await check(queue).emitsError( + it()..has((e) => e.message, 'message').equals('Error at 1')); }); }); @@ -397,21 +385,20 @@ fake trace'''); final queue = _countingStream(3); await softCheckAsync>( queue, it()..mayEmitMultiple(it()..isLessThan(2))); - await check(queue).emits().which(it()..equals(2)); + await check(queue).emits(it()..equals(2)); }); test('consumes no events if no events match', () async { final queue = _countingStream(2); await softCheckAsync>( queue, it()..mayEmitMultiple(it()..isLessThan(0))); - await check(queue).emits().which(it()..equals(0)); + await check(queue).emits(it()..equals(0)); }); test('does not consume an error', () async { final queue = _countingStream(1, errorAt: 0); await softCheckAsync>( queue, it()..mayEmitMultiple(it()..equals(0))); - await check(queue) - .emitsError() - .which(it()..has((e) => e.message, 'message').equals('Error at 1')); + await check(queue).emitsError( + it()..has((e) => e.message, 'message').equals('Error at 1')); }); }); @@ -434,21 +421,18 @@ fake trace'''); test('uses a transaction', () async { final queue = _countingStream(1); await softCheckAsync>(queue, it()..isDone()); - await check(queue).emits().which(it()..equals(0)); + await check(queue).emits(it()..equals(0)); }); test('can be described', () async { await check(it>()..isDone()) - .asyncDescription - .which(it()..deepEquals([' is done'])); + .hasAsyncDescriptionWhich(it()..deepEquals([' is done'])); }); }); group('emitsAnyOf', () { test('succeeds for a stream that matches one condition', () async { - await check(_countingStream(1)).anyOf([ - it()..emits().which(it()..equals(42)), - it()..emits().which((it()..equals(0))) - ]); + await check(_countingStream(1)).anyOf( + [it()..emits(it()..equals(42)), it()..emits((it()..equals(0)))]); }); test('fails for a stream that matches no conditions', () async { await check(_countingStream(0)).isRejectedByAsync( @@ -472,7 +456,7 @@ fake trace'''); await check(_countingStream(1)).isRejectedByAsync( it() ..anyOf([ - it()..emits().which(it()..equals(42)), + it()..emits(it()..equals(42)), it()..emitsThrough(it()..equals(10)), ]), actual: [ @@ -491,8 +475,8 @@ fake trace'''); test('gets described with the number of conditions', () async { await check( it>()..anyOf([it()..emits(), it()..emits()])) - .asyncDescription - .which(it()..deepEquals([' satisfies any of 2 conditions'])); + .hasAsyncDescriptionWhich( + it()..deepEquals([' satisfies any of 2 conditions'])); }); test('uses a transaction', () async { final queue = _countingStream(1); @@ -500,28 +484,22 @@ fake trace'''); queue, it() ..anyOf([ - it()..emits().which(it()..equals(10)), + it()..emits(it()..equals(10)), it()..emitsThrough(it()..equals(42)), ])); - await check(queue).emits().which(it()..equals(0)); + await check(queue).emits(it()..equals(0)); }); test('consumes events', () async { final queue = _countingStream(3); await check(queue).anyOf([ - it()..emits().which(it()..equals(1)), + it()..emits(it()..equals(1)), it()..emitsThrough(it()..equals(1)) ]); - await check(queue).emits().which(it()..equals(2)); + await check(queue).emits(it()..equals(2)); }); }); }); - group('ChainAsync', () { - test('which', () async { - await check(_futureSuccess()).completes().which(it()..equals(42)); - }); - }); - group('StreamQueueWrap', () { test('can wrap streams in a queue', () async { await check(Stream.value(1)).withQueue.emits(); diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart index 1a30eb55a..9d15a05c7 100644 --- a/pkgs/checks/test/test_shared.dart +++ b/pkgs/checks/test/test_shared.dart @@ -2,6 +2,8 @@ // 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:checks/checks.dart'; import 'package:checks/context.dart'; @@ -44,7 +46,7 @@ extension RejectionChecks on Subject { {Iterable? actual, Iterable? which}) async { late T actualValue; var didRunCallback = false; - final rejection = (await context.nestAsync( + await context.nestAsync( () => ['does not meet an async condition with a Rejection'], (value) async { actualValue = value; @@ -57,31 +59,54 @@ extension RejectionChecks on Subject { ]); } return Extracted.value(failure.rejection); + }, _LazyCondition((rejection) { + if (didRunCallback) { + rejection + .has((r) => r.actual, 'actual') + .deepEquals(actual ?? literal(actualValue)); + } else { + rejection + .has((r) => r.actual, 'actual') + .context + .expect(() => ['is left default'], (_) => null); + } + if (which == null) { + rejection.has((r) => r.which, 'which').isNull(); + } else { + rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + } })); - if (didRunCallback) { - rejection - .has((r) => r.actual, 'actual') - .deepEquals(actual ?? literal(actualValue)); - } else { - rejection - .has((r) => r.actual, 'actual') - .context - .expect(() => ['is left default'], (_) => null); - } - if (which == null) { - rejection.has((r) => r.which, 'which').isNull(); - } else { - rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); - } } } extension ConditionChecks on Subject> { Subject> get description => has((c) => describe(c), 'description'); - Future>> get asyncDescription async => + Future hasAsyncDescriptionWhich( + Condition> descriptionCondition) => context.nestAsync( () => ['has description'], (condition) async => - Extracted.value(await describeAsync(condition))); + Extracted.value(await describeAsync(condition)), + descriptionCondition); +} + +/// A condition which can be defined at the time it is invoked instead of +/// eagerly. +/// +/// Allows basing the following condition in `isRejectedByAsync` on the actual +/// value. +class _LazyCondition implements Condition { + final FutureOr Function(Subject) _apply; + _LazyCondition(this._apply); + + @override + void apply(Subject subject) { + _apply(subject); + } + + @override + Future applyAsync(Subject subject) async { + await _apply(subject); + } }