Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkgs/checks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
## 0.3.2-wip

- Require Dart 3.11
- Updated `Context.nest` to accept an optional named `nestedCondition` argument
which is executed against the nested subject.
- Updated `Subject.isA`, `Subject.isNotNull`, `Subject.throws` (sync), and
`Subject.returnsNormally` to accept an optional `Condition` callback to apply
to the extracted value.
- Updated `Subject.completes`, `Subject.throws` (async), `StreamChecks.emits`,
and `StreamChecks.emitsError` to return `Future<Subject>` in addition to
accepting an optional `AsyncCondition` callback.
- Improve speed of pretty printing for large collections.
- Improve formatting for failures involving unexpected exceptions.
- Improve formatting for failed String equality checks - indent string diff
Expand Down
56 changes: 36 additions & 20 deletions pkgs/checks/lib/src/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -522,19 +522,25 @@ abstract final class Context<T> {
///
/// {@macro callbacks_may_be_unused}
///
/// If [nestedCondition] is passed it will be executed against the resulting
/// `Subject` before returning it.
/// Nesting extensions which are getters can ignore [nestedCondition],
/// and nesting extensions which are methods should prefer to include it.
///
/// ```dart
/// Subject<Foo> get someDerivedValue =>
/// Subject<Foo> someDerivedValue([Condition<Foo>? that]) =>
/// context.nest(() => ['has someDerivedValue'], (actual) {
/// if (_cannotReadDerivedValue(actual)) {
/// return Extracted.rejection(
/// which: ['cannot read someDerivedValue']);
/// }
/// return Extracted.value(_readDerivedValue(actual));
/// });
/// }, nestedCondition: that);
/// ```
Subject<R> nest<R>(
Iterable<String> Function() label,
Extracted<R> Function(T) extract, {
Condition<R>? nestedCondition,
bool atSameLevel = false,
});

Expand All @@ -544,25 +550,25 @@ abstract final class Context<T> {
/// [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}
///
/// {@macro callbacks_may_be_unused}
///
/// If [nestedCondition] is passed it will be executed against the resulting
/// `Subject` before returning it.
/// Async nesting extensions which are methods should prefer to include it.
///
/// {@macro async_limitations}
///
/// Extensions which use `nestAsync` should always make that call
/// synchronously and return the result so that exceptions stemming from use
/// in a synchronous context can be reported synchronously.
///
/// ```dart
/// Future<void> someAsyncResult(
/// [AsyncCondition<Result> resultCondition]) {
/// Future<Subject<Result>> someAsyncResult(
/// [AsyncCondition<Result>? resultCondition]) {
/// return context.nestAsync(() => ['has someAsyncResult'], (actual) async {
/// if (await _asyncOperationFailed(actual)) {
/// return Extracted.rejection(which: ['cannot read someAsyncResult']);
Expand All @@ -571,11 +577,11 @@ abstract final class Context<T> {
/// }, resultCondition);
/// }
/// ```
Future<void> nestAsync<R>(
Future<Subject<R>> nestAsync<R>(
Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract,
FutureOr<Extracted<R>> Function(T) extract, [
AsyncCondition<R>? nestedCondition,
);
]);
}

/// A property extracted from a value being checked, or a rejection.
Expand Down Expand Up @@ -786,6 +792,7 @@ final class _TestContext<T> implements Context<T>, _ClauseDescription {
Subject<R> nest<R>(
Iterable<String> Function() label,
Extracted<R> Function(T) extract, {
Condition<R>? nestedCondition,
bool atSameLevel = false,
}) {
final result = _value.map((actual) => extract(actual)._fillActual(actual));
Expand All @@ -804,11 +811,13 @@ final class _TestContext<T> implements Context<T>, _ClauseDescription {
context = _TestContext._child(value, label, this);
_clauses.add(context);
}
return Subject._(context);
final subject = Subject<R>._(context);
nestedCondition?.call(subject);
return subject;
}

@override
Future<void> nestAsync<R>(
Future<Subject<R>> nestAsync<R>(
Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract, [
AsyncCondition<R>? nestedCondition,
Expand All @@ -817,7 +826,7 @@ final class _TestContext<T> implements Context<T>, _ClauseDescription {
return _nestAsync(label, extract, nestedCondition);
}

Future<void> _nestAsync<R>(
Future<Subject<R>> _nestAsync<R>(
Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T) extract,
AsyncCondition<R>? nestedCondition,
Expand All @@ -835,7 +844,9 @@ final class _TestContext<T> implements Context<T>, _ClauseDescription {
final value = result._value ?? _Absent<R>();
final context = _TestContext<R>._child(value, label, this);
_clauses.add(context);
await nestedCondition?.call(Subject<R>._(context));
final subject = Subject<R>._(context);
await nestedCondition?.call(subject);
return subject;
} finally {
outstandingWork.complete();
}
Expand Down Expand Up @@ -916,18 +927,23 @@ final class _SkippedContext<T> implements Context<T> {
Subject<R> nest<R>(
Iterable<String> Function() label,
Extracted<R> Function(T p1) extract, {
Condition<R>? nestedCondition,
bool atSameLevel = false,
}) {
return Subject._(_SkippedContext());
final subject = Subject<R>._(_SkippedContext());
nestedCondition?.call(subject);
return subject;
}

@override
Future<void> nestAsync<R>(
Future<Subject<R>> nestAsync<R>(
Iterable<String> Function() label,
FutureOr<Extracted<R>> Function(T p1) extract,
FutureOr<Extracted<R>> Function(T p1) extract, [
AsyncCondition<R>? nestedCondition,
) async {
// no-op
]) async {
final subject = Subject<R>._(_SkippedContext());
await nestedCondition?.call(subject);
return subject;
}
}

Expand Down
184 changes: 90 additions & 94 deletions pkgs/checks/lib/src/extensions/async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,20 @@ extension FutureChecks<T> on Subject<Future<T>> {
///
/// The returned future will complete when the subject future has completed,
/// and [completionCondition] has optionally been checked.
Future<void> completes([AsyncCondition<T>? completionCondition]) {
return context.nestAsync<T>(() => ['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))),
...indent(LineSplitter.split(st.toString())),
],
);
}
}, completionCondition);
}
Future<Subject<T>> completes([AsyncCondition<T>? completionCondition]) =>
context.nestAsync<T>(() => ['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))),
...indent(LineSplitter.split(st.toString())),
],
);
}
}, completionCondition);

/// Expects that the `Future` never completes as a value or an error.
///
Expand Down Expand Up @@ -79,30 +78,30 @@ extension FutureChecks<T> on Subject<Future<T>> {
///
/// The returned future will complete when the subject future has completed,
/// and [errorCondition] has optionally been checked.
Future<void> throws<E extends Object>([AsyncCondition<E>? errorCondition]) {
return context.nestAsync<E>(
() => ['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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
},
errorCondition,
);
}
Future<Subject<E>> throws<E extends Object>([
AsyncCondition<E>? errorCondition,
]) => context.nestAsync<E>(
() => ['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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
},
errorCondition,
);
}

/// Expectations on a [StreamQueue].
Expand Down Expand Up @@ -142,28 +141,27 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
///
/// The returned future will complete when the stream has emitted, errored, or
/// ended, and the [emittedCondition] has optionally been checked.
Future<void> emits([AsyncCondition<T>? emittedCondition]) {
return context.nestAsync<T>(() => ['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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
}, emittedCondition);
}
Future<Subject<T>> emits([AsyncCondition<T>? emittedCondition]) =>
context.nestAsync<T>(() => ['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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
}, emittedCondition);

/// Expects that the stream emits an error of type [E].
///
Expand All @@ -180,40 +178,38 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
///
/// The returned future will complete when the stream has emitted, errored, or
/// ended, and the [errorCondition] has optionally been checked.
Future<void> emitsError<E extends Object>([
Future<Subject<E>> emitsError<E extends Object>([
AsyncCondition<E>? errorCondition,
]) {
return context.nestAsync<E>(
() => ['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<void>((_) {}, 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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
},
errorCondition,
);
}
]) => context.nestAsync<E>(
() => ['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<void>((_) {}, 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:',
...indent(LineSplitter.split(st.toString())),
],
);
}
},
errorCondition,
);

/// Expects that the `Stream` emits any number of events before emitting an
/// event that satisfies [condition].
Expand Down
Loading
Loading