Skip to content

Allow similar use between sync and async checks - #2683

Merged
natebosch merged 10 commits into
masterfrom
sync-condition-callbacks
Sep 4, 2026
Merged

Allow similar use between sync and async checks#2683
natebosch merged 10 commits into
masterfrom
sync-condition-callbacks

Conversation

@natebosch

@natebosch natebosch commented Jun 23, 2026

Copy link
Copy Markdown
Member

Closes #2695

Originally async expectations which extract a value also returned a
Subject like synchronous extensions still do, but they were changed to
accept AsyncCondition callback arguments for ergonomics. Add support
for synchronous Condition callbacks on a few expectations that return
subjects for authors who prefer to use the async style for synchronous
checks too.

Affected APIs:

  • Subject.isA
  • Subject.isNotNull
  • Subject<T Function()>.throws
  • Subject<T Function()>.returnsNormally

Restore the Future<Subject> return value from async check extensions
that accept an optional AsyncCondition argument. This allows usage of
other patterns which some authors may feel are better aligned with the
sync checks of the original pattern.

Affected APIs:

  • Subject<Future>.completes
  • Subject<Future>.throws
  • Subject<StreamQueue>.emits
  • Subject<StreamQueue>.emitsError

Refactor tests into finer grained cases and add cases for the new
arguments.

Towards #2639

Originally async expectations which extract a value also returned a
`Subject` like synchronous extensions still do, but they were changed to
accept `AsyncCondition` callback arguments for ergonomics. Add support
for synchronous `Condition` callbacks on a few expectations that return
subjects for authors who prefer to use the async style for synchronous
checks too.

Affected APIs:
- `Subject.isA`
- `Subject.isNotNull`
- `Subject<T Function()>.throws`
- `Subject<T Function()>.returnsNormally`

Restore the `Future<Subject>` return value from async check extensions
that accept an optional `AsyncCondition` argument. This allows usage of
other patterns which some authors may feel are better aligned with the
sync checks of the original pattern.

Affected APIs:
- `Subject<Future>.completes`
- `Subject<Future>.throws`
- `Subject<StreamQueue>.emits`
- `Subject<StreamQueue>.emitsError`

Refactor tests into finer grained cases and add cases for the new
arguments.
@natebosch
natebosch requested a review from a team as a code owner June 23, 2026 00:16
@github-actions github-actions Bot added the package:checks Issues related to pkg:checks label Jun 23, 2026
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

PR Health

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

@natebosch

Copy link
Copy Markdown
Member Author

The ergonomics of this is also impacted by #2697 - I'm more inclined to land this one if we'll also be landing the other (or if I rework it to land first). I implemented #2697 on top of this but it could also be landed separately.

@jonasfj jonasfj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, write some examples with old and new things, it a bit hard to follow the explanations.

From what I understand, and this is just my personal feelings.
I would probably rather avoid (await check(...), but I also see little harm in allowing it.

I would probably also prefer check(...).isA<T>().something(..) over check(...).isA<T>((it) => it.something(..)).

But I can see no harm in allowing both things. And it kind of makes both sync and async APIs behave similar, even if the async API still requires an await inside the expression.


There is obviously no way we are going to do an extension on Future<Subject<int>> that will allow people to do:

await check(Future.value(42)).lessThan(100);

That would be cool, but subclassing Future is probably a crime. and making a global expando mapping from Future<Subject> to SomeKindOfHack, is probably borderline insane 🤣🤣🤣

So even if it could be done, it probably shouldn't be done 🤣
-- But mentally that's what I would prefer to write, but if I had to reason about how it works, I probably wouldn't prefer it.

Comment on lines +90 to +92
(await check(_futureFail()).throws<UnimplementedError>())
.has((p0) => p0.message, 'message')
.isNull();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just "feeling", feel free to ignore.

I suspect I would rarely use (await check(...)).whatever(), because inline await is just ugly when it has to be wrapped that way.

I suppose I might find myself occasionally wanting to do:

final it = await check(...);
it.has(...);

I could imagine that being attractive in some complicated scenarios.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect I would rarely use (await check(...)).whatever(), because inline await is just ugly when it has to be wrapped that way.

Yes, this is why I moved await from this pattern and didn't keep it as an option. We've since had at least a few requests to include it. I suspect it will bear out that it won't get used much in practice, but I also think that the corresponding synchronous API that will get used drastically reduces the cost of having 2 ways to do this.

Comment thread pkgs/checks/CHANGELOG.md Outdated
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider showing examples like:

 - Updated `Subject.isA` to allow `check(42).isA<int>((it) => it.lessThan(100));`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

});
test('evaluates condition', () {
check(() => 1).isRejectedBy(
(it) => it.returnsNormally((it) => it.equals(2)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new thing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the new Condition syntax. I'll land that after this one.

# Conflicts:
#	pkgs/checks/lib/src/checks.dart
#	pkgs/checks/lib/src/extensions/async.dart
#	pkgs/checks/lib/src/extensions/function.dart
#	pkgs/checks/test/extensions/function_test.dart
@natebosch

Copy link
Copy Markdown
Member Author

After discussions we plan on moving forward with this and #2697

Adds `FutureSubjectExtension` on `Future<Subject<T>>` with a `which`
method that applies a `Condition<T>` to the subject upon future
completion. This mirrors the `which` method from `CoreChecks` on `Subject`.

TAG=agy
CONV=f78192e2-b9b4-48ef-a615-31061b25ec89

@brianquinlan brianquinlan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mainly looked at the tests, which seem reasonable.

Remove some unnecessary changes to minimize diff
@natebosch
natebosch merged commit 21a02b7 into master Sep 4, 2026
71 checks passed
@natebosch
natebosch deleted the sync-condition-callbacks branch September 4, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:checks Issues related to pkg:checks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow multiple patterns for nested checks to allow higher consistency between synchronous and asynchronous checks

3 participants