diff --git a/CHANGELOG.md b/CHANGELOG.md
index a67f797..05c3e78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.15-dev
+
+* Add `package:matcher/expect.dart` library. Copies the implementation of
+  `expect` and the asynchronous matchers from `package:test`.
+
 ## 0.12.14
 
 * Add `containsOnce` matcher.
diff --git a/README.md b/README.md
index 9244b2a..9f44237 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,233 @@
 The matcher library provides a third-generation assertion mechanism, drawing
 inspiration from [Hamcrest](https://code.google.com/p/hamcrest/).
 
-For more information, see
+For more information on testing, see
 [Unit Testing with Dart](https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#writing-tests).
 
-# Best Practices
+## Using matcher
 
-## Prefer semantically meaningful matchers to comparing derived values
+Expectations start with a call to [`expect()`] or [`expectAsync()`].
+
+[`expect()`]: https://pub.dev/documentation/matcher/latest/expect/expect.html
+[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html
+
+Any matchers package can be used with `expect()` to do
+complex validations:
+
+[`matcher`]: https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  test('.split() splits the string on the delimiter', () {
+    expect('foo,bar,baz', allOf([
+      contains('foo'),
+      isNot(startsWith('bar')),
+      endsWith('baz')
+    ]));
+  });
+}
+```
+
+If a non-matcher value is passed, it will be wrapped with [`equals()`].
+
+[`equals()`]: https://pub.dev/documentation/matcher/latest/expect/equals.html
+
+## Exception matchers
+
+You can also test exceptions with the [`throwsA()`] function or a matcher such
+as [`throwsFormatException`]:
+
+[`throwsA()`]: https://pub.dev/documentation/matcher/latest/expect/throwsA.html
+[`throwsFormatException`]: https://pub.dev/documentation/matcher/latest/expect/throwsFormatException-constant.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  test('.parse() fails on invalid input', () {
+    expect(() => int.parse('X'), throwsFormatException);
+  });
+}
+```
+
+### Future Matchers
+
+There are a number of useful functions and matchers for more advanced
+asynchrony. The [`completion()`] matcher can be used to test `Futures`; it
+ensures that the test doesn't finish until the `Future` completes, and runs a
+matcher against that `Future`'s value.
+
+[`completion()`]: https://pub.dev/documentation/matcher/latest/expect/completion.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Future.value() returns the value', () {
+    expect(Future.value(10), completion(equals(10)));
+  });
+}
+```
+
+The [`throwsA()`] matcher and the various [`throwsExceptionType`] matchers work
+with both synchronous callbacks and asynchronous `Future`s. They ensure that a
+particular type of exception is thrown:
+
+[`throwsExceptionType`]: https://pub.dev/documentation/matcher/latest/expect/throwsException-constant.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Future.error() throws the error', () {
+    expect(Future.error('oh no'), throwsA(equals('oh no')));
+    expect(Future.error(StateError('bad state')), throwsStateError);
+  });
+}
+```
+
+The [`expectAsync()`] function wraps another function and has two jobs. First,
+it asserts that the wrapped function is called a certain number of times, and
+will cause the test to fail if it's called too often; second, it keeps the test
+from finishing until the function is called the requisite number of times.
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Stream.fromIterable() emits the values in the iterable', () {
+    var stream = Stream.fromIterable([1, 2, 3]);
+
+    stream.listen(expectAsync1((number) {
+      expect(number, inInclusiveRange(1, 3));
+    }, count: 3));
+  });
+}
+```
+
+[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html
+
+### Stream Matchers
+
+The `test` package provides a suite of powerful matchers for dealing with
+[asynchronous streams][Stream]. They're expressive and composable, and make it
+easy to write complex expectations about the values emitted by a stream. For
+example:
+
+[Stream]: https://api.dart.dev/stable/dart-async/Stream-class.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('process emits status messages', () {
+    // Dummy data to mimic something that might be emitted by a process.
+    var stdoutLines = Stream.fromIterable([
+      'Ready.',
+      'Loading took 150ms.',
+      'Succeeded!'
+    ]);
+
+    expect(stdoutLines, emitsInOrder([
+      // Values match individual events.
+      'Ready.',
+
+      // Matchers also run against individual events.
+      startsWith('Loading took'),
+
+      // Stream matchers can be nested. This asserts that one of two events are
+      // emitted after the "Loading took" line.
+      emitsAnyOf(['Succeeded!', 'Failed!']),
+
+      // By default, more events are allowed after the matcher finishes
+      // matching. This asserts instead that the stream emits a done event and
+      // nothing else.
+      emitsDone
+    ]));
+  });
+}
+```
+
+A stream matcher can also match the [`async`] package's [`StreamQueue`] class,
+which allows events to be requested from a stream rather than pushed to the
+consumer. The matcher will consume the matched events, but leave the rest of the
+queue alone so that it can still be used by the test, unlike a normal `Stream`
+which can only have one subscriber. For example:
+
+[`async`]: https://pub.dev/packages/async
+[`StreamQueue`]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html
+
+```dart
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('process emits a WebSocket URL', () async {
+    // Wrap the Stream in a StreamQueue so that we can request events.
+    var stdout = StreamQueue(Stream.fromIterable([
+      'WebSocket URL:',
+      'ws://localhost:1234/',
+      'Waiting for connection...'
+    ]));
+
+    // Ignore lines from the process until it's about to emit the URL.
+    await expectLater(stdout, emitsThrough('WebSocket URL:'));
+
+    // Parse the next line as a URL.
+    var url = Uri.parse(await stdout.next);
+    expect(url.host, equals('localhost'));
+
+    // You can match against the same StreamQueue multiple times.
+    await expectLater(stdout, emits('Waiting for connection...'));
+  });
+}
+```
+
+The following built-in stream matchers are available:
+
+*   [`emits()`] matches a single data event.
+*   [`emitsError()`] matches a single error event.
+*   [`emitsDone`] matches a single done event.
+*   [`mayEmit()`] consumes events if they match an inner matcher, without
+    requiring them to match.
+*   [`mayEmitMultiple()`] works like `mayEmit()`, but it matches events against
+    the matcher as many times as possible.
+*   [`emitsAnyOf()`] consumes events matching one (or more) of several possible
+    matchers.
+*   [`emitsInOrder()`] consumes events matching multiple matchers in a row.
+*   [`emitsInAnyOrder()`] works like `emitsInOrder()`, but it allows the
+    matchers to match in any order.
+*   [`neverEmits()`] matches a stream that finishes *without* matching an inner
+    matcher.
+
+You can also define your own custom stream matchers with [`StreamMatcher()`].
+
+[`emits()`]: https://pub.dev/documentation/matcher/latest/expect/emits.html
+[`emitsError()`]: https://pub.dev/documentation/matcher/latest/expect/emitsError.html
+[`emitsDone`]: https://pub.dev/documentation/matcher/latest/expect/emitsDone.html
+[`mayEmit()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmit.html
+[`mayEmitMultiple()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmitMultiple.html
+[`emitsAnyOf()`]: https://pub.dev/documentation/matcher/latest/expect/emitsAnyOf.html
+[`emitsInOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInOrder.html
+[`emitsInAnyOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInAnyOrder.html
+[`neverEmits()`]: https://pub.dev/documentation/matcher/latest/expect/neverEmits.html
+[`StreamMatcher()`]: https://pub.dev/documentation/matcher/latest/expect/StreamMatcher-class.html
+
+## Best Practices
+
+### Prefer semantically meaningful matchers to comparing derived values
 
 Matchers which have knowledge of the semantics that are tested are able to emit
 more meaningful messages which don't require reading test source to understand
diff --git a/lib/expect.dart b/lib/expect.dart
new file mode 100644
index 0000000..c842d30
--- /dev/null
+++ b/lib/expect.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2021, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+export 'matcher.dart';
+
+export 'src/expect/expect.dart' show ErrorFormatter, expect, expectLater, fail;
+export 'src/expect/expect_async.dart'
+    show
+        Func0,
+        Func1,
+        Func2,
+        Func3,
+        Func4,
+        Func5,
+        Func6,
+        expectAsync,
+        expectAsync0,
+        expectAsync1,
+        expectAsync2,
+        expectAsync3,
+        expectAsync4,
+        expectAsync5,
+        expectAsync6,
+        expectAsyncUntil0,
+        expectAsyncUntil1,
+        expectAsyncUntil2,
+        expectAsyncUntil3,
+        expectAsyncUntil4,
+        expectAsyncUntil5,
+        expectAsyncUntil6;
+export 'src/expect/future_matchers.dart'
+    show completes, completion, doesNotComplete;
+export 'src/expect/never_called.dart' show neverCalled;
+export 'src/expect/prints_matcher.dart' show prints;
+export 'src/expect/stream_matcher.dart' show StreamMatcher;
+export 'src/expect/stream_matchers.dart'
+    show
+        emitsDone,
+        emits,
+        emitsError,
+        mayEmit,
+        emitsAnyOf,
+        emitsInOrder,
+        emitsInAnyOrder,
+        emitsThrough,
+        mayEmitMultiple,
+        neverEmits;
+export 'src/expect/throws_matcher.dart' show Throws, throws, throwsA;
+export 'src/expect/throws_matchers.dart'
+    show
+        throwsArgumentError,
+        throwsConcurrentModificationError,
+        throwsCyclicInitializationError,
+        throwsException,
+        throwsFormatException,
+        throwsNoSuchMethodError,
+        throwsNullThrownError,
+        throwsRangeError,
+        throwsStateError,
+        throwsUnimplementedError,
+        throwsUnsupportedError;
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
index 42fc2df..45d1f27 100644
--- a/lib/src/core_matchers.dart
+++ b/lib/src/core_matchers.dart
@@ -252,8 +252,9 @@
   Description describeMismatch(Object? item, Description mismatchDescription,
       Map matchState, bool verbose) {
     if (item is String || item is Iterable || item is Map) {
-      return super
-          .describeMismatch(item, mismatchDescription, matchState, verbose);
+      super.describeMismatch(item, mismatchDescription, matchState, verbose);
+      mismatchDescription.add('does not contain ').addDescriptionOf(_expected);
+      return mismatchDescription;
     } else {
       return mismatchDescription.add('is not a string, map or iterable');
     }
diff --git a/lib/src/expect/async_matcher.dart b/lib/src/expect/async_matcher.dart
new file mode 100644
index 0000000..854151d
--- /dev/null
+++ b/lib/src/expect/async_matcher.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../equals_matcher.dart';
+import '../interfaces.dart';
+import '../operator_matchers.dart';
+import '../type_matcher.dart';
+import 'expect.dart';
+
+/// A matcher that does asynchronous computation.
+///
+/// Rather than implementing [matches], subclasses implement [matchAsync].
+/// [AsyncMatcher.matches] ensures that the test doesn't complete until the
+/// returned future completes, and [expect] returns a future that completes when
+/// the returned future completes so that tests can wait for it.
+abstract class AsyncMatcher extends Matcher {
+  const AsyncMatcher();
+
+  /// Returns `null` if this matches [item], or a [String] description of the
+  /// failure if it doesn't match.
+  ///
+  /// This can return a [Future] or a synchronous value. If it returns a
+  /// [Future], neither [expect] nor the test will complete until that [Future]
+  /// completes.
+  ///
+  /// If this returns a [String] synchronously, [expect] will synchronously
+  /// throw a [TestFailure] and [matches] will synchronously return `false`.
+  dynamic /*FutureOr<String>*/ matchAsync(dynamic item);
+
+  @override
+  bool matches(dynamic item, Map matchState) {
+    final result = matchAsync(item);
+    expect(
+        result,
+        anyOf([
+          equals(null),
+          const TypeMatcher<Future>(),
+          const TypeMatcher<String>()
+        ]),
+        reason: 'matchAsync() may only return a String, a Future, or null.');
+
+    if (result is Future) {
+      final outstandingWork = TestHandle.current.markPending();
+      result.then((realResult) {
+        if (realResult != null) {
+          fail(formatFailure(this, item, realResult as String));
+        }
+        outstandingWork.complete();
+      });
+    } else if (result is String) {
+      matchState[this] = result;
+      return false;
+    }
+
+    return true;
+  }
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+          Map matchState, bool verbose) =>
+      StringDescription(matchState[this] as String);
+}
diff --git a/lib/src/expect/expect.dart b/lib/src/expect/expect.dart
new file mode 100644
index 0000000..8dd8cae
--- /dev/null
+++ b/lib/src/expect/expect.dart
@@ -0,0 +1,161 @@
+// 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../equals_matcher.dart';
+import '../interfaces.dart';
+import '../operator_matchers.dart';
+import '../type_matcher.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'future_matchers.dart';
+import 'prints_matcher.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// The type used for functions that can be used to build up error reports
+/// upon failures in [expect].
+@Deprecated('Will be removed in 0.13.0.')
+typedef ErrorFormatter = String Function(Object? actual, Matcher matcher,
+    String? reason, Map matchState, bool verbose);
+
+/// Assert that [actual] matches [matcher].
+///
+/// This is the main assertion function. [reason] is optional and is typically
+/// not supplied, as a reason is generated from [matcher]; if [reason]
+/// is included it is appended to the reason generated by the matcher.
+///
+/// [matcher] can be a value in which case it will be wrapped in an
+/// [equals] matcher.
+///
+/// If the assertion fails a [TestFailure] is thrown.
+///
+/// If [skip] is a String or `true`, the assertion is skipped. The arguments are
+/// still evaluated, but [actual] is not verified to match [matcher]. If
+/// [actual] is a [Future], the test won't complete until the future emits a
+/// value.
+///
+/// If [skip] is a string, it should explain why the assertion is skipped; this
+/// reason will be printed when running the test.
+///
+/// Certain matchers, like [completion] and [throwsA], either match or fail
+/// asynchronously. When you use [expect] with these matchers, it ensures that
+/// the test doesn't complete until the matcher has either matched or failed. If
+/// you want to wait for the matcher to complete before continuing the test, you
+/// can call [expectLater] instead and `await` the result.
+void expect(dynamic actual, dynamic matcher,
+    {String? reason,
+    Object? /* String|bool */ skip,
+    @Deprecated('Will be removed in 0.13.0.') bool verbose = false,
+    @Deprecated('Will be removed in 0.13.0.') ErrorFormatter? formatter}) {
+  _expect(actual, matcher,
+      reason: reason, skip: skip, verbose: verbose, formatter: formatter);
+}
+
+/// Just like [expect], but returns a [Future] that completes when the matcher
+/// has finished matching.
+///
+/// For the [completes] and [completion] matchers, as well as [throwsA] and
+/// related matchers when they're matched against a [Future], the returned
+/// future completes when the matched future completes. For the [prints]
+/// matcher, it completes when the future returned by the callback completes.
+/// Otherwise, it completes immediately.
+///
+/// If the matcher fails asynchronously, that failure is piped to the returned
+/// future where it can be handled by user code.
+Future expectLater(dynamic actual, dynamic matcher,
+        {String? reason, Object? /* String|bool */ skip}) =>
+    _expect(actual, matcher, reason: reason, skip: skip);
+
+/// The implementation of [expect] and [expectLater].
+Future _expect(Object? actual, Object? matcher,
+    {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) {
+  final test = TestHandle.current;
+  formatter ??= (actual, matcher, reason, matchState, verbose) {
+    var mismatchDescription = StringDescription();
+    matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
+
+    return formatFailure(matcher, actual, mismatchDescription.toString(),
+        reason: reason);
+  };
+
+  if (skip != null && skip is! bool && skip is! String) {
+    throw ArgumentError.value(skip, 'skip', 'must be a bool or a String');
+  }
+
+  matcher = wrapMatcher(matcher);
+  if (skip != null && skip != false) {
+    String message;
+    if (skip is String) {
+      message = 'Skip expect: $skip';
+    } else if (reason != null) {
+      message = 'Skip expect ($reason).';
+    } else {
+      var description = StringDescription().addDescriptionOf(matcher);
+      message = 'Skip expect ($description).';
+    }
+
+    test.markSkipped(message);
+    return Future.sync(() {});
+  }
+
+  if (matcher is AsyncMatcher) {
+    // Avoid async/await so that expect() throws synchronously when possible.
+    var result = matcher.matchAsync(actual);
+    expect(
+        result,
+        anyOf([
+          equals(null),
+          const TypeMatcher<Future>(),
+          const TypeMatcher<String>()
+        ]),
+        reason: 'matchAsync() may only return a String, a Future, or null.');
+
+    if (result is String) {
+      fail(formatFailure(matcher, actual, result, reason: reason));
+    } else if (result is Future) {
+      final outstandingWork = test.markPending();
+      return result.then((realResult) {
+        if (realResult == null) return;
+        fail(formatFailure(matcher as Matcher, actual, realResult as String,
+            reason: reason));
+      }).whenComplete(
+          // Always remove this, in case the failure is caught and handled
+          // gracefully.
+          outstandingWork.complete);
+    }
+
+    return Future.sync(() {});
+  }
+
+  var matchState = {};
+  try {
+    if ((matcher as Matcher).matches(actual, matchState)) {
+      return Future.sync(() {});
+    }
+  } catch (e, trace) {
+    reason ??= '$e at $trace';
+  }
+  fail(formatter(actual, matcher as Matcher, reason, matchState, verbose));
+}
+
+/// Convenience method for throwing a new [TestFailure] with the provided
+/// [message].
+Never fail(String message) => throw TestFailure(message);
+
+// The default error formatter.
+@Deprecated('Will be removed in 0.13.0.')
+String formatFailure(Matcher expected, Object? actual, String which,
+    {String? reason}) {
+  var buffer = StringBuffer();
+  buffer.writeln(indent(prettyPrint(expected), first: 'Expected: '));
+  buffer.writeln(indent(prettyPrint(actual), first: '  Actual: '));
+  if (which.isNotEmpty) buffer.writeln(indent(which, first: '   Which: '));
+  if (reason != null) buffer.writeln(reason);
+  return buffer.toString();
+}
diff --git a/lib/src/expect/expect_async.dart b/lib/src/expect/expect_async.dart
new file mode 100644
index 0000000..7571383
--- /dev/null
+++ b/lib/src/expect/expect_async.dart
@@ -0,0 +1,586 @@
+// 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 'package:test_api/hooks.dart';
+
+import 'util/placeholder.dart';
+
+// Function types returned by expectAsync# methods.
+
+typedef Func0<T> = T Function();
+typedef Func1<T, A> = T Function([A a]);
+typedef Func2<T, A, B> = T Function([A a, B b]);
+typedef Func3<T, A, B, C> = T Function([A a, B b, C c]);
+typedef Func4<T, A, B, C, D> = T Function([A a, B b, C c, D d]);
+typedef Func5<T, A, B, C, D, E> = T Function([A a, B b, C c, D d, E e]);
+typedef Func6<T, A, B, C, D, E, F> = T Function([A a, B b, C c, D d, E e, F f]);
+
+/// A wrapper for a function that ensures that it's called the appropriate
+/// number of times.
+///
+/// The containing test won't be considered to have completed successfully until
+/// this function has been called the appropriate number of times.
+///
+/// The wrapper function is accessible via [func]. It supports up to six
+/// optional and/or required positional arguments, but no named arguments.
+class _ExpectedFunction<T> {
+  /// The wrapped callback.
+  final Function _callback;
+
+  /// The minimum number of calls that are expected to be made to the function.
+  ///
+  /// If fewer calls than this are made, the test will fail.
+  final int _minExpectedCalls;
+
+  /// The maximum number of calls that are expected to be made to the function.
+  ///
+  /// If more calls than this are made, the test will fail.
+  final int _maxExpectedCalls;
+
+  /// A callback that should return whether the function is not expected to have
+  /// any more calls.
+  ///
+  /// This will be called after every time the function is run. The test case
+  /// won't be allowed to terminate until it returns `true`.
+  ///
+  /// This may be `null`. If so, the function is considered to be done after
+  /// it's been run once.
+  final bool Function()? _isDone;
+
+  /// A descriptive name for the function.
+  final String _id;
+
+  /// An optional description of why the function is expected to be called.
+  ///
+  /// If not passed, this will be an empty string.
+  final String _reason;
+
+  /// The number of times the function has been called.
+  int _actualCalls = 0;
+
+  /// The test in which this function was wrapped.
+  late final TestHandle _test;
+
+  /// Whether this function has been called the requisite number of times.
+  late bool _complete;
+
+  OutstandingWork? _outstandingWork;
+
+  /// Wraps [callback] in a function that asserts that it's called at least
+  /// [minExpected] times and no more than [maxExpected] times.
+  ///
+  /// If passed, [id] is used as a descriptive name fo the function and [reason]
+  /// as a reason it's expected to be called. If [isDone] is passed, the test
+  /// won't be allowed to complete until it returns `true`.
+  _ExpectedFunction(Function callback, int minExpected, int maxExpected,
+      {String? id, String? reason, bool Function()? isDone})
+      : _callback = callback,
+        _minExpectedCalls = minExpected,
+        _maxExpectedCalls =
+            (maxExpected == 0 && minExpected > 0) ? minExpected : maxExpected,
+        _isDone = isDone,
+        _reason = reason == null ? '' : '\n$reason',
+        _id = _makeCallbackId(id, callback) {
+    try {
+      _test = TestHandle.current;
+    } on OutsideTestException {
+      throw StateError('`expectAsync` must be called within a test.');
+    }
+
+    if (maxExpected > 0 && minExpected > maxExpected) {
+      throw ArgumentError('max ($maxExpected) may not be less than count '
+          '($minExpected).');
+    }
+
+    if (isDone != null || minExpected > 0) {
+      _outstandingWork = _test.markPending();
+      _complete = false;
+    } else {
+      _complete = true;
+    }
+  }
+
+  /// Tries to find a reasonable name for [callback].
+  ///
+  /// If [id] is passed, uses that. Otherwise, tries to determine a name from
+  /// calling `toString`. If no name can be found, returns the empty string.
+  static String _makeCallbackId(String? id, Function callback) {
+    if (id != null) return '$id ';
+
+    // If the callback is not an anonymous closure, try to get the
+    // name.
+    var toString = callback.toString();
+    var prefix = "Function '";
+    var start = toString.indexOf(prefix);
+    if (start == -1) return '';
+
+    start += prefix.length;
+    var end = toString.indexOf("'", start);
+    if (end == -1) return '';
+    return '${toString.substring(start, end)} ';
+  }
+
+  /// Returns a function that has the same number of positional arguments as the
+  /// wrapped function (up to a total of 6).
+  Function get func {
+    if (_callback is Function(Never, Never, Never, Never, Never, Never)) {
+      return max6;
+    }
+    if (_callback is Function(Never, Never, Never, Never, Never)) return max5;
+    if (_callback is Function(Never, Never, Never, Never)) return max4;
+    if (_callback is Function(Never, Never, Never)) return max3;
+    if (_callback is Function(Never, Never)) return max2;
+    if (_callback is Function(Never)) return max1;
+    if (_callback is Function()) return max0;
+
+    _outstandingWork?.complete();
+    throw ArgumentError(
+        'The wrapped function has more than 6 required arguments');
+  }
+
+  // This indirection is critical. It ensures the returned function has an
+  // argument count of zero.
+  T max0() => max6();
+
+  T max1([Object? a0 = placeholder]) => max6(a0);
+
+  T max2([Object? a0 = placeholder, Object? a1 = placeholder]) => max6(a0, a1);
+
+  T max3(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder]) =>
+      max6(a0, a1, a2);
+
+  T max4(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder]) =>
+      max6(a0, a1, a2, a3);
+
+  T max5(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder]) =>
+      max6(a0, a1, a2, a3, a4);
+
+  T max6(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder,
+          Object? a5 = placeholder]) =>
+      _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder));
+
+  /// Runs the wrapped function with [args] and returns its return value.
+  T _run(Iterable args) {
+    // Note that in the old test, this returned `null` if it encountered an
+    // error, where now it just re-throws that error because Zone machinery will
+    // pass it to the invoker anyway.
+    try {
+      _actualCalls++;
+      if (_test.shouldBeDone) {
+        throw TestFailure(
+            'Callback ${_id}called ($_actualCalls) after test case '
+            '${_test.name} had already completed.$_reason');
+      } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
+        throw TestFailure('Callback ${_id}called more times than expected '
+            '($_maxExpectedCalls).$_reason');
+      }
+
+      return Function.apply(_callback, args.toList()) as T;
+    } finally {
+      _afterRun();
+    }
+  }
+
+  /// After each time the function is run, check to see if it's complete.
+  void _afterRun() {
+    if (_complete) return;
+    if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return;
+    if (_isDone != null && !_isDone!()) return;
+
+    // Mark this callback as complete and remove it from the test case's
+    // outstanding callback count; if that hits zero the test is done.
+    _complete = true;
+    _outstandingWork?.complete();
+  }
+}
+
+/// This function is deprecated because it doesn't work well with strong mode.
+/// Use [expectAsync0], [expectAsync1],
+/// [expectAsync2], [expectAsync3], [expectAsync4], [expectAsync5], or
+/// [expectAsync6] instead.
+@Deprecated('Will be removed in 0.13.0')
+Function expectAsync(Function callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction(callback, count, max, id: id, reason: reason).func;
+
+/// Informs the framework that the given [callback] of arity 0 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with zero arguments. See also
+/// [expectAsync1], [expectAsync2], [expectAsync3], [expectAsync4],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func0<T> expectAsync0<T>(T Function() callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max0;
+
+/// Informs the framework that the given [callback] of arity 1 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with one argument. See also
+/// [expectAsync0], [expectAsync2], [expectAsync3], [expectAsync4],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func1<T, A> expectAsync1<T, A>(T Function(A) callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max1;
+
+/// Informs the framework that the given [callback] of arity 2 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with two arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync3], [expectAsync4],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func2<T, A, B> expectAsync2<T, A, B>(T Function(A, B) callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max2;
+
+/// Informs the framework that the given [callback] of arity 3 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with three arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync4],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func3<T, A, B, C> expectAsync3<T, A, B, C>(T Function(A, B, C) callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max3;
+
+/// Informs the framework that the given [callback] of arity 4 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with four arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func4<T, A, B, C, D> expectAsync4<T, A, B, C, D>(
+        T Function(A, B, C, D) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max4;
+
+/// Informs the framework that the given [callback] of arity 5 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with five arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync4], and [expectAsync6] for callbacks with different arity.
+Func5<T, A, B, C, D, E> expectAsync5<T, A, B, C, D, E>(
+        T Function(A, B, C, D, E) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max5;
+
+/// Informs the framework that the given [callback] of arity 6 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with six arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync4], and [expectAsync5] for callbacks with different arity.
+Func6<T, A, B, C, D, E, F> expectAsync6<T, A, B, C, D, E, F>(
+        T Function(A, B, C, D, E, F) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max6;
+
+/// This function is deprecated because it doesn't work well with strong mode.
+/// Use [expectAsyncUntil0], [expectAsyncUntil1],
+/// [expectAsyncUntil2], [expectAsyncUntil3], [expectAsyncUntil4],
+/// [expectAsyncUntil5], or [expectAsyncUntil6] instead.
+@Deprecated('Will be removed in 0.13.0')
+Function expectAsyncUntil(Function callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction(callback, 0, -1, id: id, reason: reason, isDone: isDone)
+        .func;
+
+/// Informs the framework that the given [callback] of arity 0 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with zero arguments. See also
+/// [expectAsyncUntil1], [expectAsyncUntil2], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func0<T> expectAsyncUntil0<T>(T Function() callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max0;
+
+/// Informs the framework that the given [callback] of arity 1 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with one argument. See also
+/// [expectAsyncUntil0], [expectAsyncUntil2], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func1<T, A> expectAsyncUntil1<T, A>(
+        T Function(A) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max1;
+
+/// Informs the framework that the given [callback] of arity 2 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with two arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func2<T, A, B> expectAsyncUntil2<T, A, B>(
+        T Function(A, B) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max2;
+
+/// Informs the framework that the given [callback] of arity 3 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with three arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func3<T, A, B, C> expectAsyncUntil3<T, A, B, C>(
+        T Function(A, B, C) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max3;
+
+/// Informs the framework that the given [callback] of arity 4 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with four arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func4<T, A, B, C, D> expectAsyncUntil4<T, A, B, C, D>(
+        T Function(A, B, C, D) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max4;
+
+/// Informs the framework that the given [callback] of arity 5 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with five arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func5<T, A, B, C, D, E> expectAsyncUntil5<T, A, B, C, D, E>(
+        T Function(A, B, C, D, E) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max5;
+
+/// Informs the framework that the given [callback] of arity 6 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with six arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil5] for
+/// callbacks with different arity.
+Func6<T, A, B, C, D, E, F> expectAsyncUntil6<T, A, B, C, D, E, F>(
+        T Function(A, B, C, D, E, F) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max6;
diff --git a/lib/src/expect/future_matchers.dart b/lib/src/expect/future_matchers.dart
new file mode 100644
index 0000000..e1328b6
--- /dev/null
+++ b/lib/src/expect/future_matchers.dart
@@ -0,0 +1,119 @@
+// Copyright (c) 2012, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart' show pumpEventQueue;
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// Matches a [Future] that completes successfully with any value.
+///
+/// This creates an asynchronous expectation. The call to [expect] will return
+/// immediately and execution will continue. To wait for the future to
+/// complete and the expectation to run use [expectLater] and wait on the
+/// returned future.
+///
+/// To test that a Future completes with an exception, you can use [throws] and
+/// [throwsA].
+final Matcher completes = const _Completes(null);
+
+/// Matches a [Future] that completes successfully with a value that matches
+/// [matcher].
+///
+/// This creates an asynchronous expectation. The call to [expect] will return
+/// immediately and execution will continue. Later, when the future completes,
+/// the expectation against [matcher] will run. To wait for the future to
+/// complete and the expectation to run use [expectLater] and wait on the
+/// returned future.
+///
+/// To test that a Future completes with an exception, you can use [throws] and
+/// [throwsA].
+Matcher completion(Object? matcher,
+        [@Deprecated('this parameter is ignored') String? description]) =>
+    _Completes(wrapMatcher(matcher));
+
+class _Completes extends AsyncMatcher {
+  final Matcher? _matcher;
+
+  const _Completes(this._matcher);
+
+  // Avoid async/await so we synchronously start listening to [item].
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Future) return 'was not a Future';
+
+    return item.then((value) async {
+      if (_matcher == null) return null;
+
+      String? result;
+      if (_matcher is AsyncMatcher) {
+        result = await (_matcher as AsyncMatcher).matchAsync(value) as String?;
+        if (result == null) return null;
+      } else {
+        var matchState = {};
+        if (_matcher!.matches(value, matchState)) return null;
+        result = _matcher!
+            .describeMismatch(value, StringDescription(), matchState, false)
+            .toString();
+      }
+
+      var buffer = StringBuffer();
+      buffer.writeln(indent(prettyPrint(value), first: 'emitted '));
+      if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+      return buffer.toString().trimRight();
+    });
+  }
+
+  @override
+  Description describe(Description description) {
+    if (_matcher == null) {
+      description.add('completes successfully');
+    } else {
+      description.add('completes to a value that ').addDescriptionOf(_matcher);
+    }
+    return description;
+  }
+}
+
+/// Matches a [Future] that does not complete.
+///
+/// Note that this creates an asynchronous expectation. The call to
+/// `expect()` that includes this will return immediately and execution will
+/// continue.
+final Matcher doesNotComplete = const _DoesNotComplete();
+
+class _DoesNotComplete extends Matcher {
+  const _DoesNotComplete();
+
+  @override
+  Description describe(Description description) {
+    description.add('does not complete');
+    return description;
+  }
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    if (item is! Future) return false;
+    item.then((value) {
+      fail('Future was not expected to complete but completed with a value of '
+          '$value');
+    });
+    expect(pumpEventQueue(), completes);
+    return true;
+  }
+
+  @override
+  Description describeMismatch(
+      Object? item, Description description, Map matchState, bool verbose) {
+    if (item is! Future) return description.add('$item is not a Future');
+    return description;
+  }
+}
diff --git a/lib/src/expect/never_called.dart b/lib/src/expect/never_called.dart
new file mode 100644
index 0000000..46d27f0
--- /dev/null
+++ b/lib/src/expect/never_called.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2017, 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:stack_trace/stack_trace.dart';
+import 'package:test_api/hooks.dart';
+
+import 'expect.dart';
+import 'future_matchers.dart';
+import 'util/placeholder.dart';
+import 'util/pretty_print.dart';
+
+/// Returns a function that causes the test to fail if it's called.
+///
+/// This can safely be passed in place of any callback that takes ten or fewer
+/// positional parameters. For example:
+///
+/// ```
+/// // Asserts that the stream never emits an event.
+/// stream.listen(neverCalled);
+/// ```
+///
+/// This also ensures that the test doesn't complete until a call to
+/// [pumpEventQueue] finishes, so that the callback has a chance to be called.
+Null Function(
+    [Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?]) get neverCalled {
+  // Make sure the test stays alive long enough to call the function if it's
+  // going to.
+  expect(pumpEventQueue(), completes);
+
+  var zone = Zone.current;
+  return (
+      [a1 = placeholder,
+      a2 = placeholder,
+      a3 = placeholder,
+      a4 = placeholder,
+      a5 = placeholder,
+      a6 = placeholder,
+      a7 = placeholder,
+      a8 = placeholder,
+      a9 = placeholder,
+      a10 = placeholder]) {
+    var arguments = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]
+        .where((argument) => argument != placeholder)
+        .toList();
+
+    var argsText = arguments.isEmpty
+        ? ' no arguments.'
+        : ':\n${bullet(arguments.map(prettyPrint))}';
+    zone.handleUncaughtError(
+        TestFailure(
+            'Callback should never have been called, but it was called with'
+            '$argsText'),
+        zone.run(Chain.current));
+    return null;
+  };
+}
diff --git a/lib/src/expect/prints_matcher.dart b/lib/src/expect/prints_matcher.dart
new file mode 100644
index 0000000..57ae95e
--- /dev/null
+++ b/lib/src/expect/prints_matcher.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, 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 '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'util/pretty_print.dart';
+
+/// Matches a [Function] that prints text that matches [matcher].
+///
+/// [matcher] may be a String or a [Matcher].
+///
+/// If the function this runs against returns a [Future], all text printed by
+/// the function (using [Zone] scoping) until that Future completes is matched.
+///
+/// This only tracks text printed using the [print] function.
+///
+/// This returns an [AsyncMatcher], so [expect] won't complete until the matched
+/// function does.
+Matcher prints(Object? matcher) => _Prints(wrapMatcher(matcher));
+
+class _Prints extends AsyncMatcher {
+  final Matcher _matcher;
+
+  _Prints(this._matcher);
+
+  // Avoid async/await so we synchronously fail if the function is
+  // synchronous.
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Function()) return 'was not a unary Function';
+
+    var buffer = StringBuffer();
+    var result = runZoned(item,
+        zoneSpecification: ZoneSpecification(print: (_, __, ____, line) {
+      buffer.writeln(line);
+    }));
+
+    return result is Future
+        ? result.then((_) => _check(buffer.toString()))
+        : _check(buffer.toString());
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('prints ').addDescriptionOf(_matcher);
+
+  /// Verifies that [actual] matches [_matcher] and returns a [String]
+  /// description of the failure if it doesn't.
+  String? _check(String actual) {
+    var matchState = {};
+    if (_matcher.matches(actual, matchState)) return null;
+
+    var result = _matcher
+        .describeMismatch(actual, StringDescription(), matchState, false)
+        .toString();
+
+    var buffer = StringBuffer();
+    if (actual.isEmpty) {
+      buffer.writeln('printed nothing');
+    } else {
+      buffer.writeln(indent(prettyPrint(actual), first: 'printed '));
+    }
+    if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+    return buffer.toString().trimRight();
+  }
+}
diff --git a/lib/src/expect/stream_matcher.dart b/lib/src/expect/stream_matcher.dart
new file mode 100644
index 0000000..0c1d852
--- /dev/null
+++ b/lib/src/expect/stream_matcher.dart
@@ -0,0 +1,196 @@
+// Copyright (c) 2017, 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 'package:async/async.dart';
+import 'package:test_api/hooks.dart';
+
+import '../interfaces.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'util/pretty_print.dart';
+
+/// A matcher that matches events from [Stream]s or [StreamQueue]s.
+///
+/// Stream matchers are designed to make it straightforward to create complex
+/// expectations for streams, and to interleave expectations with the rest of a
+/// test. They can be used on a [Stream] to match all events it emits:
+///
+/// ```dart
+/// expect(stream, emitsInOrder([
+///   // Values match individual events.
+///   "Ready.",
+///
+///   // Matchers also run against individual events.
+///   startsWith("Loading took"),
+///
+///   // Stream matchers can be nested. This asserts that one of two events are
+///   // emitted after the "Loading took" line.
+///   emitsAnyOf(["Succeeded!", "Failed!"]),
+///
+///   // By default, more events are allowed after the matcher finishes
+///   // matching. This asserts instead that the stream emits a done event and
+///   // nothing else.
+///   emitsDone
+/// ]));
+/// ```
+///
+/// It can also match a [StreamQueue], in which case it consumes the matched
+/// events. The call to [expect] returns a [Future] that completes when the
+/// matcher is done matching. You can `await` this to consume different events
+/// at different times:
+///
+/// ```dart
+/// var stdout = StreamQueue(stdoutLineStream);
+///
+/// // Ignore lines from the process until it's about to emit the URL.
+/// await expectLater(stdout, emitsThrough('WebSocket URL:'));
+///
+/// // Parse the next line as a URL.
+/// var url = Uri.parse(await stdout.next);
+/// expect(url.host, equals('localhost'));
+///
+/// // You can match against the same StreamQueue multiple times.
+/// await expectLater(stdout, emits('Waiting for connection...'));
+/// ```
+///
+/// Users can call [StreamMatcher] to create custom matchers.
+abstract class StreamMatcher extends Matcher {
+  /// The description of this matcher.
+  ///
+  /// This is in the subjunctive mood, which means it can be used after the word
+  /// "should". For example, it might be "emit the right events".
+  String get description;
+
+  /// Creates a new [StreamMatcher] described by [description] that matches
+  /// events with [matchQueue].
+  ///
+  /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue],
+  /// and should follow all the guarantees of that method. In particular:
+  ///
+  /// * If it matches successfully, it should return `null` and possibly consume
+  ///   events.
+  /// * If it fails to match, consume no events and return a description of the
+  ///   failure.
+  /// * The description should be in past tense.
+  /// * The description should be grammatically valid when used after "the
+  ///   stream"—"emitted the wrong events", for example.
+  ///
+  /// The [matchQueue] callback may return the empty string to indicate a
+  /// failure if it has no information to add beyond the description of the
+  /// failure and the events actually emitted by the stream.
+  ///
+  /// The [description] should be in the subjunctive mood. This means that it
+  /// should be grammatically valid when used after the word "should". For
+  /// example, it might be "emit the right events".
+  factory StreamMatcher(Future<String?> Function(StreamQueue) matchQueue,
+      String description) = _StreamMatcher;
+
+  /// Tries to match events emitted by [queue].
+  ///
+  /// If this matches successfully, it consumes the matching events from [queue]
+  /// and returns `null`.
+  ///
+  /// If this fails to match, it doesn't consume any events and returns a
+  /// description of the failure. This description is in the past tense, and
+  /// could grammatically be used after "the stream". For example, it might
+  /// return "emitted the wrong events".
+  ///
+  /// The description string may also be empty, which indicates that the
+  /// matcher's description and the events actually emitted by the stream are
+  /// enough to understand the failure.
+  ///
+  /// If the queue emits an error, that error is re-thrown unless otherwise
+  /// indicated by the matcher.
+  Future<String?> matchQueue(StreamQueue queue);
+}
+
+/// A concrete implementation of [StreamMatcher].
+///
+/// This is separate from the original type to hide the private [AsyncMatcher]
+/// interface.
+class _StreamMatcher extends AsyncMatcher implements StreamMatcher {
+  @override
+  final String description;
+
+  /// The callback used to implement [matchQueue].
+  final Future<String?> Function(StreamQueue) _matchQueue;
+
+  _StreamMatcher(this._matchQueue, this.description);
+
+  @override
+  Future<String?> matchQueue(StreamQueue queue) => _matchQueue(queue);
+
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    StreamQueue queue;
+    var shouldCancelQueue = false;
+    if (item is StreamQueue) {
+      queue = item;
+    } else if (item is Stream) {
+      queue = StreamQueue(item);
+      shouldCancelQueue = true;
+    } else {
+      return 'was not a Stream or a StreamQueue';
+    }
+
+    // Avoid async/await in the outer method so that we synchronously error out
+    // for an invalid argument type.
+    var transaction = queue.startTransaction();
+    var copy = transaction.newQueue();
+    return matchQueue(copy).then((result) async {
+      // Accept the transaction if the result is null, indicating that the match
+      // succeeded.
+      if (result == null) {
+        transaction.commit(copy);
+        return null;
+      }
+
+      // Get a list of events emitted by the stream so we can emit them as part
+      // of the error message.
+      var replay = transaction.newQueue();
+      var events = <Result?>[];
+      var subscription = Result.captureStreamTransformer
+          .bind(replay.rest.cast())
+          .listen(events.add, onDone: () => events.add(null));
+
+      // Wait on a timer tick so all buffered events are emitted.
+      await Future.delayed(Duration.zero);
+      _unawaited(subscription.cancel());
+
+      var eventsString = events.map((event) {
+        if (event == null) {
+          return 'x Stream closed.';
+        } else if (event.isValue) {
+          return addBullet(event.asValue!.value.toString());
+        } else {
+          var error = event.asError!;
+          var chain = TestHandle.current.formatStackTrace(error.stackTrace);
+          var text = '${error.error}\n$chain';
+          return indent(text, first: '! ');
+        }
+      }).join('\n');
+      if (eventsString.isEmpty) eventsString = 'no events';
+
+      transaction.reject();
+
+      var buffer = StringBuffer();
+      buffer.writeln(indent(eventsString, first: 'emitted '));
+      if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+      return buffer.toString().trimRight();
+    }, onError: (Object error) {
+      transaction.reject();
+      // ignore: only_throw_errors
+      throw error;
+    }).then((result) {
+      if (shouldCancelQueue) queue.cancel();
+      return result;
+    });
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('should ').add(this.description);
+}
+
+void _unawaited(Future<void> f) {}
diff --git a/lib/src/expect/stream_matchers.dart b/lib/src/expect/stream_matchers.dart
new file mode 100644
index 0000000..02efff3
--- /dev/null
+++ b/lib/src/expect/stream_matchers.dart
@@ -0,0 +1,377 @@
+// Copyright (c) 2017, 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 'package:async/async.dart';
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'stream_matcher.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// Returns a [StreamMatcher] that asserts that the stream emits a "done" event.
+final emitsDone = StreamMatcher(
+    (queue) async => (await queue.hasNext) ? '' : null, 'be done');
+
+/// Returns a [StreamMatcher] for [matcher].
+///
+/// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any
+/// other [Matcher], this matches a single event that matches that matcher. If
+/// it's any other Object, this matches a single event that's equal to that
+/// object.
+///
+/// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any
+/// matcher-like value into a proper [StreamMatcher].
+StreamMatcher emits(Object? matcher) {
+  if (matcher is StreamMatcher) return matcher;
+  var wrapped = wrapMatcher(matcher);
+
+  var matcherDescription = wrapped.describe(StringDescription());
+
+  return StreamMatcher((queue) async {
+    if (!await queue.hasNext) return '';
+
+    var matchState = {};
+    var actual = await queue.next;
+    if (wrapped.matches(actual, matchState)) return null;
+
+    var mismatchDescription = StringDescription();
+    wrapped.describeMismatch(actual, mismatchDescription, matchState, false);
+
+    if (mismatchDescription.length == 0) return '';
+    return 'emitted an event that $mismatchDescription';
+  },
+      // TODO(nweiz): add "should" once matcher#42 is fixed.
+      'emit an event that $matcherDescription');
+}
+
+/// Returns a [StreamMatcher] that matches a single error event that matches
+/// [matcher].
+StreamMatcher emitsError(Object? matcher) {
+  var wrapped = wrapMatcher(matcher);
+  var matcherDescription = wrapped.describe(StringDescription());
+  var throwsMatcher = throwsA(wrapped) as AsyncMatcher;
+
+  return StreamMatcher(
+      (queue) => throwsMatcher.matchAsync(queue.next) as Future<String?>,
+      // TODO(nweiz): add "should" once matcher#42 is fixed.
+      'emit an error that $matcherDescription');
+}
+
+/// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to
+/// match the stream.
+///
+/// This matcher always succeeds; if [matcher] doesn't match, this just consumes
+/// no events.
+StreamMatcher mayEmit(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    await queue.withTransaction(
+        (copy) async => (await streamMatcher.matchQueue(copy)) == null);
+    return null;
+  }, 'maybe ${streamMatcher.description}');
+}
+
+/// Returns a [StreamMatcher] that matches the stream if at least one of
+/// [matchers] matches.
+///
+/// If multiple matchers match the stream, this chooses the matcher that
+/// consumes as many events as possible.
+///
+/// If any matchers match the stream, no errors from other matchers are thrown.
+/// If no matchers match and multiple matchers threw errors, the first error is
+/// re-thrown.
+StreamMatcher emitsAnyOf(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toList();
+  if (streamMatchers.isEmpty) {
+    throw ArgumentError('matcher may not be empty');
+  }
+
+  if (streamMatchers.length == 1) return streamMatchers.first;
+  var description = 'do one of the following:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher((queue) async {
+    var transaction = queue.startTransaction();
+
+    // Allocate the failures list ahead of time so that its order matches the
+    // order of [matchers], and thus the order the matchers will be listed in
+    // the description.
+    var failures = List<String?>.filled(matchers.length, null);
+
+    // The first error thrown. If no matchers match and this exists, we rethrow
+    // it.
+    Object? firstError;
+    StackTrace? firstStackTrace;
+
+    var futures = <Future>[];
+    StreamQueue? consumedMost;
+    for (var i = 0; i < matchers.length; i++) {
+      futures.add(() async {
+        var copy = transaction.newQueue();
+
+        String? result;
+        try {
+          result = await streamMatchers[i].matchQueue(copy);
+        } catch (error, stackTrace) {
+          if (firstError == null) {
+            firstError = error;
+            firstStackTrace = stackTrace;
+          }
+          return;
+        }
+
+        if (result != null) {
+          failures[i] = result;
+        } else if (consumedMost == null ||
+            consumedMost!.eventsDispatched < copy.eventsDispatched) {
+          consumedMost = copy;
+        }
+      }());
+    }
+
+    await Future.wait(futures);
+
+    if (consumedMost == null) {
+      transaction.reject();
+      if (firstError != null) {
+        await Future.error(firstError!, firstStackTrace);
+      }
+
+      var failureMessages = <String>[];
+      for (var i = 0; i < matchers.length; i++) {
+        var message = 'failed to ${streamMatchers[i].description}';
+        if (failures[i]!.isNotEmpty) {
+          message += message.contains('\n') ? '\n' : ' ';
+          message += 'because it ${failures[i]}';
+        }
+
+        failureMessages.add(message);
+      }
+
+      return 'failed all options:\n${bullet(failureMessages)}';
+    } else {
+      transaction.commit(consumedMost!);
+      return null;
+    }
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches the stream if each matcher in
+/// [matchers] matches, one after another.
+///
+/// If any matcher fails to match, this fails and consumes no events.
+StreamMatcher emitsInOrder(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toList();
+  if (streamMatchers.length == 1) return streamMatchers.first;
+
+  var description = 'do the following in order:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher((queue) async {
+    for (var i = 0; i < streamMatchers.length; i++) {
+      var matcher = streamMatchers[i];
+      var result = await matcher.matchQueue(queue);
+      if (result == null) continue;
+
+      var newResult = "didn't ${matcher.description}";
+      if (result.isNotEmpty) {
+        newResult += newResult.contains('\n') ? '\n' : ' ';
+        newResult += 'because it $result';
+      }
+      return newResult;
+    }
+    return null;
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches any number of events followed by
+/// events that match [matcher].
+///
+/// This consumes all events matched by [matcher], as well as all events before.
+/// If the stream emits a done event without matching [matcher], this fails and
+/// consumes no events.
+StreamMatcher emitsThrough(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    var failures = <String>[];
+
+    Future<bool> tryHere() => queue.withTransaction((copy) async {
+          var result = await streamMatcher.matchQueue(copy);
+          if (result == null) return true;
+          failures.add(result);
+          return false;
+        });
+
+    while (await queue.hasNext) {
+      if (await tryHere()) return null;
+      await queue.next;
+    }
+
+    // Try after the queue is done in case the matcher can match an empty
+    // stream.
+    if (await tryHere()) return null;
+
+    var result = 'never did ${streamMatcher.description}';
+
+    var failureMessages =
+        bullet(failures.where((failure) => failure.isNotEmpty));
+    if (failureMessages.isNotEmpty) {
+      result += result.contains('\n') ? '\n' : ' ';
+      result += 'because it:\n$failureMessages';
+    }
+
+    return result;
+  }, 'eventually ${streamMatcher.description}');
+}
+
+/// Returns a [StreamMatcher] that matches any number of events that match
+/// [matcher].
+///
+/// This consumes events until [matcher] no longer matches. It always succeeds;
+/// if [matcher] doesn't match, this just consumes no events. It never rethrows
+/// errors.
+StreamMatcher mayEmitMultiple(Object? matcher) {
+  var streamMatcher = emits(matcher);
+
+  var description = streamMatcher.description;
+  description += description.contains('\n') ? '\n' : ' ';
+  description += 'zero or more times';
+
+  return StreamMatcher((queue) async {
+    while (await _tryMatch(queue, streamMatcher)) {
+      // Do nothing; the matcher presumably already consumed events.
+    }
+    return null;
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches a stream that never matches
+/// [matcher].
+///
+/// This doesn't complete until the stream emits a done event. It never consumes
+/// any events. It never re-throws errors.
+StreamMatcher neverEmits(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    var events = 0;
+    var matched = false;
+    await queue.withTransaction((copy) async {
+      while (await copy.hasNext) {
+        matched = await _tryMatch(copy, streamMatcher);
+        if (matched) return false;
+
+        events++;
+
+        try {
+          await copy.next;
+        } catch (_) {
+          // Ignore errors events.
+        }
+      }
+
+      matched = await _tryMatch(copy, streamMatcher);
+      return false;
+    });
+
+    if (!matched) return null;
+    return "after $events ${pluralize('event', events)} did "
+        '${streamMatcher.description}';
+  }, 'never ${streamMatcher.description}');
+}
+
+/// Returns whether [matcher] matches [queue] at its current position.
+///
+/// This treats errors as failures to match.
+Future<bool> _tryMatch(StreamQueue queue, StreamMatcher matcher) {
+  return queue.withTransaction((copy) async {
+    try {
+      return (await matcher.matchQueue(copy)) == null;
+    } catch (_) {
+      return false;
+    }
+  });
+}
+
+/// Returns a [StreamMatcher] that matches the stream if each matcher in
+/// [matchers] matches, in any order.
+///
+/// If any matcher fails to match, this fails and consumes no events. If the
+/// matchers match in multiple different possible orders, this chooses the order
+/// that consumes as many events as possible.
+///
+/// If any sequence of matchers matches the stream, no errors from other
+/// sequences are thrown. If no sequences match and multiple sequences throw
+/// errors, the first error is re-thrown.
+///
+/// Note that checking every ordering of [matchers] is O(n!) in the worst case,
+/// so this should only be called when there are very few [matchers].
+StreamMatcher emitsInAnyOrder(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toSet();
+  if (streamMatchers.length == 1) return streamMatchers.first;
+  var description = 'do the following in any order:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher(
+      (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : '',
+      description);
+}
+
+/// Returns whether [queue] matches [matchers] in any order.
+Future<bool> _tryInAnyOrder(
+    StreamQueue queue, Set<StreamMatcher> matchers) async {
+  if (matchers.length == 1) {
+    return await matchers.first.matchQueue(queue) == null;
+  }
+
+  var transaction = queue.startTransaction();
+  StreamQueue? consumedMost;
+
+  // The first error thrown. If no matchers match and this exists, we rethrow
+  // it.
+  Object? firstError;
+  StackTrace? firstStackTrace;
+
+  await Future.wait(matchers.map((matcher) async {
+    var copy = transaction.newQueue();
+    try {
+      if (await matcher.matchQueue(copy) != null) return;
+    } catch (error, stackTrace) {
+      if (firstError == null) {
+        firstError = error;
+        firstStackTrace = stackTrace;
+      }
+      return;
+    }
+
+    var rest = Set<StreamMatcher>.from(matchers);
+    rest.remove(matcher);
+
+    try {
+      if (!await _tryInAnyOrder(copy, rest)) return;
+    } catch (error, stackTrace) {
+      if (firstError == null) {
+        firstError = error;
+        firstStackTrace = stackTrace;
+      }
+      return;
+    }
+
+    if (consumedMost == null ||
+        consumedMost!.eventsDispatched < copy.eventsDispatched) {
+      consumedMost = copy;
+    }
+  }));
+
+  if (consumedMost == null) {
+    transaction.reject();
+    if (firstError != null) await Future.error(firstError!, firstStackTrace);
+    return false;
+  } else {
+    transaction.commit(consumedMost!);
+    return true;
+  }
+}
diff --git a/lib/src/expect/throws_matcher.dart b/lib/src/expect/throws_matcher.dart
new file mode 100644
index 0000000..0ee3144
--- /dev/null
+++ b/lib/src/expect/throws_matcher.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2014, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// This function is deprecated.
+///
+/// Use [throwsA] instead. We strongly recommend that you add assertions about
+/// at least the type of the error, but you can write `throwsA(anything)` to
+/// mimic the behavior of this matcher.
+@Deprecated('Will be removed in 0.13.0')
+const Matcher throws = Throws();
+
+/// This can be used to match three kinds of objects:
+///
+/// * A [Function] that throws an exception when called. The function cannot
+///   take any arguments. If you want to test that a function expecting
+///   arguments throws, wrap it in another zero-argument function that calls
+///   the one you want to test.
+///
+/// * A [Future] that completes with an exception. Note that this creates an
+///   asynchronous expectation. The call to `expect()` that includes this will
+///   return immediately and execution will continue. Later, when the future
+///   completes, the actual expectation will run.
+///
+/// * A [Function] that returns a [Future] that completes with an exception.
+///
+/// In all three cases, when an exception is thrown, this will test that the
+/// exception object matches [matcher]. If [matcher] is not an instance of
+/// [Matcher], it will implicitly be treated as `equals(matcher)`.
+///
+/// Examples:
+/// ```dart
+/// void functionThatThrows() => throw SomeException();
+///
+/// void functionWithArgument(bool shouldThrow) {
+///   if (shouldThrow) {
+///     throw SomeException();
+///   }
+/// }
+///
+/// Future<void> asyncFunctionThatThrows() async => throw SomeException();
+///
+/// expect(functionThatThrows, throwsA(isA<SomeException>()));
+///
+/// expect(() => functionWithArgument(true), throwsA(isA<SomeException>()));
+///
+/// var future = asyncFunctionThatThrows();
+/// await expectLater(future, throwsA(isA<SomeException>()));
+///
+/// await expectLater(
+///     asyncFunctionThatThrows, throwsA(isA<SomeException>()));
+/// ```
+Matcher throwsA(Object? matcher) => Throws(wrapMatcher(matcher));
+
+/// Use the [throwsA] function instead.
+@Deprecated('Will be removed in 0.13.0')
+class Throws extends AsyncMatcher {
+  final Matcher? _matcher;
+
+  const Throws([Matcher? matcher]) : _matcher = matcher;
+
+  // Avoid async/await so we synchronously fail if we match a synchronous
+  // function.
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Function && item is! Future) {
+      return 'was not a Function or Future';
+    }
+
+    if (item is Future) {
+      return _matchFuture(item, 'emitted ');
+    }
+
+    try {
+      item as Function;
+      var value = item();
+      if (value is Future) {
+        return _matchFuture(value, 'returned a Future that emitted ');
+      }
+
+      return indent(prettyPrint(value), first: 'returned ');
+    } catch (error, trace) {
+      return _check(error, trace);
+    }
+  }
+
+  /// Matches [future], using try/catch since `onError` doesn't seem to work
+  /// properly in nnbd.
+  Future<String?> _matchFuture(
+      Future<dynamic> future, String messagePrefix) async {
+    try {
+      var value = await future;
+      return indent(prettyPrint(value), first: messagePrefix);
+    } catch (error, trace) {
+      return _check(error, trace);
+    }
+  }
+
+  @override
+  Description describe(Description description) {
+    if (_matcher == null) {
+      return description.add('throws');
+    } else {
+      return description.add('throws ').addDescriptionOf(_matcher);
+    }
+  }
+
+  /// Verifies that [error] matches [_matcher] and returns a [String]
+  /// description of the failure if it doesn't.
+  String? _check(error, StackTrace? trace) {
+    if (_matcher == null) return null;
+
+    var matchState = {};
+    if (_matcher!.matches(error, matchState)) return null;
+
+    var result = _matcher!
+        .describeMismatch(error, StringDescription(), matchState, false)
+        .toString();
+
+    var buffer = StringBuffer();
+    buffer.writeln(indent(prettyPrint(error), first: 'threw '));
+    if (trace != null) {
+      buffer.writeln(indent(
+          TestHandle.current.formatStackTrace(trace).toString(),
+          first: 'stack '));
+    }
+    if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which '));
+    return buffer.toString().trimRight();
+  }
+}
diff --git a/lib/src/expect/throws_matchers.dart b/lib/src/expect/throws_matchers.dart
new file mode 100644
index 0000000..67d35b7
--- /dev/null
+++ b/lib/src/expect/throws_matchers.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import '../error_matchers.dart';
+import '../interfaces.dart';
+import '../type_matcher.dart';
+import 'throws_matcher.dart';
+
+/// A matcher for functions that throw ArgumentError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsArgumentError = Throws(isArgumentError);
+
+/// A matcher for functions that throw ConcurrentModificationError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsConcurrentModificationError =
+    Throws(isConcurrentModificationError);
+
+/// A matcher for functions that throw CyclicInitializationError.
+///
+/// See [throwsA] for objects that this can be matched against.
+@Deprecated('throwsCyclicInitializationError has been deprecated, because '
+    'the type will longer exists in Dart 3.0. It will now catch any kind of '
+    'error, not only CyclicInitializationError.')
+const Matcher throwsCyclicInitializationError = Throws(TypeMatcher<Error>());
+
+/// A matcher for functions that throw Exception.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsException = Throws(isException);
+
+/// A matcher for functions that throw FormatException.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsFormatException = Throws(isFormatException);
+
+/// A matcher for functions that throw NoSuchMethodError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsNoSuchMethodError = Throws(isNoSuchMethodError);
+
+/// A matcher for functions that throw NullThrownError.
+///
+/// See [throwsA] for objects that this can be matched against.
+@Deprecated('throwsNullThrownError has been deprecated, because '
+    'NullThrownError has been replaced with TypeError. '
+    'Use `throwsA(isA<TypeError>())` instead.')
+const Matcher throwsNullThrownError = Throws(TypeMatcher<TypeError>());
+
+/// A matcher for functions that throw RangeError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsRangeError = Throws(isRangeError);
+
+/// A matcher for functions that throw StateError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsStateError = Throws(isStateError);
+
+/// A matcher for functions that throw Exception.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsUnimplementedError = Throws(isUnimplementedError);
+
+/// A matcher for functions that throw UnsupportedError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsUnsupportedError = Throws(isUnsupportedError);
diff --git a/lib/src/expect/util/placeholder.dart b/lib/src/expect/util/placeholder.dart
new file mode 100644
index 0000000..ee2dc70
--- /dev/null
+++ b/lib/src/expect/util/placeholder.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2017, 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.
+
+/// A class that's used as a default argument to detect whether an argument was
+/// passed.
+///
+/// We use a custom class for this rather than just `const Object()` so that
+/// callers can't accidentally pass the placeholder value.
+class _Placeholder {
+  const _Placeholder();
+}
+
+/// A placeholder to use as a default argument value to detect whether an
+/// argument was passed.
+const placeholder = _Placeholder();
diff --git a/lib/src/expect/util/pretty_print.dart b/lib/src/expect/util/pretty_print.dart
new file mode 100644
index 0000000..de635ba
--- /dev/null
+++ b/lib/src/expect/util/pretty_print.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, 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 'package:term_glyph/term_glyph.dart' as glyph;
+
+import '../../description.dart';
+
+/// Indent each line in [text] by [first] spaces.
+///
+/// [first] is used in place of the first line's indentation.
+String indent(String text, {required String first}) {
+  final prefix = ' ' * first.length;
+  var lines = text.split('\n');
+  if (lines.length == 1) return '$first$text';
+
+  var buffer = StringBuffer('$first${lines.first}\n');
+
+  // Write out all but the first and last lines with [prefix].
+  for (var line in lines.skip(1).take(lines.length - 2)) {
+    buffer.writeln('$prefix$line');
+  }
+  buffer.write('$prefix${lines.last}');
+  return buffer.toString();
+}
+
+/// Returns a pretty-printed representation of [value].
+///
+/// The matcher package doesn't expose its pretty-print function directly, but
+/// we can use it through StringDescription.
+String prettyPrint(Object? value) =>
+    StringDescription().addDescriptionOf(value).toString();
+
+/// Indents [text], and adds a bullet at the beginning.
+String addBullet(String text) => indent(text, first: '${glyph.bullet} ');
+
+/// Converts [strings] to a bulleted list.
+String bullet(Iterable<String> strings) => strings.map(addBullet).join('\n');
+
+/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
+///
+/// By default, this just adds "s" to the end of [name] to get the plural. If
+/// [plural] is passed, that's used instead.
+String pluralize(String name, int number, {String? plural}) {
+  if (number == 1) return name;
+  if (plural != null) return plural;
+  return '${name}s';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 5059e58..87b4cc3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,14 +9,27 @@
   sdk: ">=2.18.0 <3.0.0"
 
 dependencies:
+  async: ^2.10.0
   meta: ^1.8.0
   stack_trace: ^1.10.0
+  term_glyph: ^1.2.0
+  test_api: ^0.5.0
 
 dev_dependencies:
+  fake_async: ^1.3.0
   lints: ^2.0.0
-  test: any
-  test_api: any
-  test_core: any
+  test: ^1.23.0
 
 dependency_overrides:
-  test_api: any
+  test_api:
+    git:
+      url: https://github.com/dart-lang/test
+      path: pkgs/test_api
+  test_core:
+    git:
+      url: https://github.com/dart-lang/test
+      path: pkgs/test_core
+  test:
+    git:
+      url: https://github.com/dart-lang/test
+      path: pkgs/test
diff --git a/test/expect_async_test.dart b/test/expect_async_test.dart
new file mode 100644
index 0000000..991ca28
--- /dev/null
+++ b/test/expect_async_test.dart
@@ -0,0 +1,393 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'dart:async';
+
+import 'package:fake_async/fake_async.dart';
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  group('supports a function with this many arguments:', () {
+    test('0', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync0(() {
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('1', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1((int arg) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })(1);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('2', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync2((arg1, arg2) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('3', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync3((arg1, arg2, arg3) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          callbackRun = true;
+        })(1, 2, 3);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('4', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync4((arg1, arg2, arg3, arg4) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          callbackRun = true;
+        })(1, 2, 3, 4);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('5', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync5((arg1, arg2, arg3, arg4, arg5) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('6', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync6((arg1, arg2, arg3, arg4, arg5, arg6) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          expect(arg6, equals(6));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5, 6);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+  });
+
+  group('with optional arguments', () {
+    test('allows them to be passed', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1(([arg = 1]) {
+          expect(arg, equals(2));
+          callbackRun = true;
+        })(2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('allows them not to be passed', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1(([arg = 1]) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+  });
+
+  group('by default', () {
+    test("won't allow the test to complete until it's called", () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {});
+      });
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+    });
+
+    test('may only be called once', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {});
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (1).');
+    });
+  });
+
+  group('with count', () {
+    test(
+        "won't allow the test to complete until it's called at least that "
+        'many times', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {}, count: 3);
+      });
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+    });
+
+    test("will throw an error if it's called more than that many times",
+        () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {}, count: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (3).');
+    });
+
+    group('0,', () {
+      test("won't block the test's completion", () {
+        expectAsync0(() {}, count: 0);
+      });
+
+      test("will throw an error if it's ever called", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expectAsync0(() {}, count: 0)();
+        });
+
+        expectTestFailed(
+            monitor, 'Callback called more times than expected (0).');
+      });
+    });
+  });
+
+  group('with max', () {
+    test('will allow the callback to be called that many times', () {
+      var callback = expectAsync0(() {}, max: 3);
+      callback();
+      callback();
+      callback();
+    });
+
+    test('will allow the callback to be called fewer than that many times', () {
+      var callback = expectAsync0(() {}, max: 3);
+      callback();
+    });
+
+    test("will throw an error if it's called more than that many times",
+        () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {}, max: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (3).');
+    });
+
+    test('-1, will allow the callback to be called any number of times', () {
+      var callback = expectAsync0(() {}, max: -1);
+      for (var i = 0; i < 20; i++) {
+        callback();
+      }
+    });
+  });
+
+  test('will throw an error if max is less than count', () {
+    expect(() => expectAsync0(() {}, max: 1, count: 2), throwsArgumentError);
+  });
+
+  group('expectAsyncUntil()', () {
+    test("won't allow the test to complete until isDone returns true",
+        () async {
+      late TestCaseMonitor monitor;
+      late Future future;
+      monitor = TestCaseMonitor.start(() {
+        var done = false;
+        var callback = expectAsyncUntil0(() {}, () => done);
+
+        future = () async {
+          await pumpEventQueue();
+          expect(monitor.state, equals(State.running));
+          callback();
+          await pumpEventQueue();
+          expect(monitor.state, equals(State.running));
+          done = true;
+          callback();
+        }();
+      });
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+      // Ensure that the outer test doesn't complete until the inner future
+      // completes.
+      await future;
+    });
+
+    test("doesn't call isDone until after the callback is called", () {
+      var callbackRun = false;
+      expectAsyncUntil0(() => callbackRun = true, () {
+        expect(callbackRun, isTrue);
+        return true;
+      })();
+    });
+  });
+
+  test('allows errors', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      expect(expectAsync0(() => throw 'oh no'), throwsA('oh no'));
+    });
+
+    expectTestPassed(monitor);
+  });
+
+  test('may be called in a non-test zone', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      var callback = expectAsync0(() {});
+      Zone.root.run(callback);
+    });
+    expectTestPassed(monitor);
+  });
+
+  test('may be called in a FakeAsync zone that does not run further', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      FakeAsync().run((_) {
+        var callback = expectAsync0(() {});
+        callback();
+      });
+    });
+    expectTestPassed(monitor);
+  });
+
+  group('old-style expectAsync()', () {
+    test('works with no arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use
+        expectAsync(() {
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with dynamic arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use
+        expectAsync((arg1, arg2) {
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with non-nullable arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use
+        expectAsync((int arg1, int arg2) {
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with 6 arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use
+        expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) {
+          callbackRun = true;
+        })(1, 2, 3, 4, 5, 6);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test("doesn't support a function with 7 arguments", () {
+      // ignore: deprecated_member_use
+      expect(() => expectAsync((a, b, c, d, e, f, g) {}), throwsArgumentError);
+    });
+  });
+}
diff --git a/test/expect_test.dart b/test/expect_test.dart
new file mode 100644
index 0000000..e2ef497
--- /dev/null
+++ b/test/expect_test.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2017, 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 'package:test/test.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  group('returned Future from expectLater()', () {
+    test('completes immediately for a sync matcher', () {
+      expect(expectLater(true, isTrue), completes);
+    });
+
+    test('contains the expect failure', () {
+      expect(expectLater(Future.value(true), completion(isFalse)),
+          throwsA(isTestFailure(anything)));
+    });
+
+    test('contains an async error', () {
+      expect(expectLater(Future.error('oh no'), completion(isFalse)),
+          throwsA('oh no'));
+    });
+  });
+
+  group('an async matcher that fails synchronously', () {
+    test('throws synchronously', () {
+      expect(() => expect(() {}, throwsA(anything)),
+          throwsA(isTestFailure(anything)));
+    });
+
+    test('can be used with synchronous operators', () {
+      expect(() {}, isNot(throwsA(anything)));
+    });
+  });
+}
diff --git a/test/having_test.dart b/test/having_test.dart
index 54b0daf..ddada77 100644
--- a/test/having_test.dart
+++ b/test/having_test.dart
@@ -21,7 +21,8 @@
       "Expected: <Instance of 'RangeError'> with "
       "`message`: contains 'details' and `start`: null and `end`: null "
       'Actual: CustomRangeError:<RangeError: Invalid value: details> '
-      "Which: has `message` with value 'Invalid value'",
+      "Which: has `message` with value 'Invalid value' "
+      "which does not contain 'details'",
     );
   });
 
@@ -56,7 +57,8 @@
               'Invalid value: Not in inclusive range 1..10: -1] '
               'Which: at location [0] is RangeError:<RangeError: '
               'Invalid value: Not in inclusive range 1..10: -1> '
-              "which has `message` with value 'Invalid value'"),
+              "which has `message` with value 'Invalid value' "
+              "which does not contain 'details'"),
           equalsIgnoringWhitespace(// Older SDKs
               "Expected: [ <<Instance of 'RangeError'> with "
               "`message`: contains 'details' and `start`: null and `end`: null> ] "
@@ -64,7 +66,8 @@
               'Invalid value: Not in range 1..10, inclusive: -1] '
               'Which: at location [0] is RangeError:<RangeError: '
               'Invalid value: Not in range 1..10, inclusive: -1> '
-              "which has `message` with value 'Invalid value'")
+              "which has `message` with value 'Invalid value' "
+              "which does not contain 'details'")
         ]));
   });
 
diff --git a/test/iterable_matchers_test.dart b/test/iterable_matchers_test.dart
index 82d2a76..7607d18 100644
--- a/test/iterable_matchers_test.dart
+++ b/test/iterable_matchers_test.dart
@@ -24,10 +24,14 @@
         d,
         contains(0),
         'Expected: contains <0> '
-        'Actual: [1, 2]');
+        'Actual: [1, 2] '
+        'Which: does not contain <0>');
 
     shouldFail(
-        'String', contains(42), "Expected: contains <42> Actual: 'String'");
+        'String',
+        contains(42),
+        "Expected: contains <42> Actual: 'String' "
+            'Which: does not contain <42>');
   });
 
   test('equals with matcher element', () {
@@ -109,8 +113,7 @@
         "Expected: every element((contains 'foo' and "
         'an object with length of a value greater than <0>)) '
         "Actual: [['foo', 'bar'], ['foo'], []] "
-        "Which: has value [] which doesn't match (contains 'foo' and "
-        'an object with length of a value greater than <0>) at index 2');
+        "Which: has value [] which does not contain 'foo' at index 2");
     shouldFail(
         e,
         everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))),
@@ -386,6 +389,7 @@
         d,
         contains(5),
         'Expected: contains <5> '
-        'Actual: SimpleIterable:[3, 2, 1]');
+        'Actual: SimpleIterable:[3, 2, 1] '
+        'Which: does not contain <5>');
   });
 }
diff --git a/test/map_matchers_test.dart b/test/map_matchers_test.dart
index 6efbf65..4c699ab 100644
--- a/test/map_matchers_test.dart
+++ b/test/map_matchers_test.dart
@@ -12,13 +12,15 @@
       {'a': 1},
       contains(2),
       'Expected: contains <2> '
-      'Actual: {\'a\': 1}',
+      'Actual: {\'a\': 1} '
+      'Which: does not contain <2>',
     );
     shouldFail(
       {'a': 1},
       contains(null),
       'Expected: contains <null> '
-      'Actual: {\'a\': 1}',
+      'Actual: {\'a\': 1} '
+      'Which: does not contain <null>',
     );
   });
 
diff --git a/test/matcher/completion_test.dart b/test/matcher/completion_test.dart
new file mode 100644
index 0000000..9259cd1
--- /dev/null
+++ b/test/matcher/completion_test.dart
@@ -0,0 +1,192 @@
+// 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/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('[doesNotComplete]', () {
+    test('fails when provided a non future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, doesNotComplete);
+      });
+
+      expectTestFailed(monitor, contains('10 is not a Future'));
+    });
+
+    test('succeeds when a future does not complete', () {
+      var completer = Completer();
+      expect(completer.future, doesNotComplete);
+    });
+
+    test('fails when a future does complete', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        completer.complete(null);
+        expect(completer.future, doesNotComplete);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+
+    test('fails when a future completes after the expect', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        expect(completer.future, doesNotComplete);
+        completer.complete(null);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+
+    test('fails when a future eventually completes', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        expect(completer.future, doesNotComplete);
+        Future(() async {
+          await pumpEventQueue(times: 10);
+        }).then(completer.complete);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+  });
+  group('[completes]', () {
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(completer.future, completes);
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test('with an error', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error('X'), completes);
+      });
+
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
+    });
+
+    test('with a failure', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error(TestFailure('oh no')), completes);
+      });
+
+      expectTestFailed(monitor, 'oh no');
+    });
+
+    test('with a non-future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, completes);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Expected: completes successfully\n'
+          '  Actual: <10>\n'
+          '   Which: was not a Future\n');
+    });
+
+    test('with a successful future', () {
+      expect(Future.value('1'), completes);
+    });
+  });
+
+  group('[completion]', () {
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<Object?>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(completer.future, completion(isNull));
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete(null);
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test('with an error', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error('X'), completion(isNull));
+      });
+
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
+    });
+
+    test('with a failure', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error(TestFailure('oh no')), completion(isNull));
+      });
+
+      expectTestFailed(monitor, 'oh no');
+    });
+
+    test('with a non-future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, completion(equals(10)));
+      });
+
+      expectTestFailed(
+          monitor,
+          'Expected: completes to a value that <10>\n'
+          '  Actual: <10>\n'
+          '   Which: was not a Future\n');
+    });
+
+    test('with an incorrect value', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.value('a'), completion(equals('b')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: completes to a value that 'b'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: emitted 'a'\n"
+                '            which is different.\n'
+                '                  Expected: b\n'
+                '                    Actual: a\n'
+                '                            ^\n'
+                '                   Differ at offset 0\n')
+          ]));
+    });
+
+    test("blocks expectLater's Future", () async {
+      var completer = Completer();
+      var fired = false;
+      unawaited(expectLater(completer.future, completion(equals(1))).then((_) {
+        fired = true;
+      }));
+
+      await pumpEventQueue();
+      expect(fired, isFalse);
+
+      completer.complete(1);
+      await pumpEventQueue();
+      expect(fired, isTrue);
+    });
+  });
+}
diff --git a/test/matcher/prints_test.dart b/test/matcher/prints_test.dart
new file mode 100644
index 0000000..cbdb12a
--- /dev/null
+++ b/test/matcher/prints_test.dart
@@ -0,0 +1,208 @@
+// Copyright (c) 2014, 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/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('synchronous', () {
+    test('passes with an expected print', () {
+      expect(() => print('Hello, world!'), prints('Hello, world!\n'));
+    });
+
+    test('combines multiple prints', () {
+      expect(() {
+        print('Hello');
+        print('World!');
+      }, prints('Hello\nWorld!\n'));
+    });
+
+    test('works with a Matcher', () {
+      expect(() => print('Hello, world!'), prints(contains('Hello')));
+    });
+
+    test('describes a failure nicely', () async {
+      void local() => print('Hello, world!');
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints('Goodbye, world!\n'));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints 'Goodbye, world!\\n'\n"
+                "            ''\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which is different.\n'
+                '                  Expected: Goodbye, w ...\n'
+                '                    Actual: Hello, wor ...\n'
+                '                            ^\n'
+                '                   Differ at offset 0\n')
+          ]));
+    });
+
+    test('describes a failure with a non-descriptive Matcher nicely', () async {
+      void local() => print('Hello, world!');
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which does not contain \'Goodbye\'\n')
+          ]));
+    });
+
+    test('describes a failure with no text nicely', () async {
+      void local() {}
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                '   Which: printed nothing\n'
+                '            which does not contain \'Goodbye\'\n')
+          ]));
+    });
+
+    test('with a non-function', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          "Expected: prints contains 'Goodbye'\n"
+          '  Actual: <10>\n'
+          '   Which: was not a unary Function\n');
+    });
+  });
+
+  group('asynchronous', () {
+    test('passes with an expected print', () {
+      expect(() => Future(() => print('Hello, world!')),
+          prints('Hello, world!\n'));
+    });
+
+    test('combines multiple prints', () {
+      expect(
+          () => Future(() {
+                print('Hello');
+                print('World!');
+              }),
+          prints('Hello\nWorld!\n'));
+    });
+
+    test('works with a Matcher', () {
+      expect(() => Future(() => print('Hello, world!')),
+          prints(contains('Hello')));
+    });
+
+    test('describes a failure nicely', () async {
+      void local() => Future(() => print('Hello, world!'));
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints('Goodbye, world!\n'));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints 'Goodbye, world!\\n'\n"
+                "            ''\n"
+                '  Actual: <'),
+            contains('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which is different.\n'
+                '                  Expected: Goodbye, w ...\n'
+                '                    Actual: Hello, wor ...\n'
+                '                            ^\n'
+                '                   Differ at offset 0')
+          ]));
+    });
+
+    test('describes a failure with a non-descriptive Matcher nicely', () async {
+      void local() => Future(() => print('Hello, world!'));
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            contains('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''")
+          ]));
+    });
+
+    test('describes a failure with no text nicely', () async {
+      void local() => Future.value();
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            contains('>\n'
+                '   Which: printed nothing')
+          ]));
+    });
+
+    test("won't let the test end until the Future completes", () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(() => completer.future, prints(isEmpty));
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test("blocks expectLater's Future", () async {
+      var completer = Completer();
+      var fired = false;
+
+      unawaited(expectLater(() {
+        scheduleMicrotask(() => print('hello!'));
+        return completer.future;
+      }, prints('hello!\n'))
+          .then((_) {
+        fired = true;
+      }));
+
+      await pumpEventQueue();
+      expect(fired, isFalse);
+
+      completer.complete();
+      await pumpEventQueue();
+      expect(fired, isTrue);
+    });
+  });
+}
diff --git a/test/matcher/throws_test.dart b/test/matcher/throws_test.dart
new file mode 100644
index 0000000..c230de1
--- /dev/null
+++ b/test/matcher/throws_test.dart
@@ -0,0 +1,282 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('synchronous', () {
+    group('[throws]', () {
+      test('with a function that throws an error', () {
+        // ignore: deprecated_member_use
+        expect(() => throw 'oh no', throws);
+      });
+
+      test("with a function that doesn't throw", () async {
+        void local() {}
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use
+          expect(local, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned <null>\n')
+            ]));
+      });
+
+      test('with a non-function', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use
+          expect(10, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            'Expected: throws\n'
+            '  Actual: <10>\n'
+            '   Which: was not a Function or Future\n');
+      });
+    });
+
+    group('[throwsA]', () {
+      test('with a function that throws an identical error', () {
+        expect(() => throw 'oh no', throwsA('oh no'));
+      });
+
+      test('with a function that throws a matching error', () {
+        expect(() => throw const FormatException('bad'),
+            throwsA(isFormatException));
+      });
+
+      test("with a function that doesn't throw", () async {
+        void local() {}
+        var monitor = await TestCaseMonitor.run(() {
+          expect(local, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned <null>\n')
+            ]));
+      });
+
+      test('with a non-function', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(10, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            "Expected: throws 'oh no'\n"
+            '  Actual: <10>\n'
+            '   Which: was not a Function or Future\n');
+      });
+
+      test('with a function that throws the wrong error', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(() => throw 'aw dang', throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n"
+                  '          stack'),
+              endsWith('          which is different.\n'
+                  '                Expected: oh no\n'
+                  '                  Actual: aw dang\n'
+                  '                          ^\n'
+                  '                 Differ at offset 0\n')
+            ]));
+      });
+    });
+  });
+
+  group('asynchronous', () {
+    group('[throws]', () {
+      test('with a Future that throws an error', () {
+        // ignore: deprecated_member_use
+        expect(Future.error('oh no'), throws);
+      });
+
+      test("with a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use
+          expect(Future.value(), throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: emitted <null>\n')
+            ]));
+      });
+
+      test('with a closure that returns a Future that throws an error', () {
+        // ignore: deprecated_member_use
+        expect(() => Future.error('oh no'), throws);
+      });
+
+      test("with a closure that returns a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use
+          expect(Future.value, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned a Future that emitted <null>\n')
+            ]));
+      });
+
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
+          // ignore: deprecated_member_use
+          expect(completer.future, throws);
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+        expectTestPassed(monitor);
+      });
+    });
+
+    group('[throwsA]', () {
+      test('with a Future that throws an identical error', () {
+        expect(Future.error('oh no'), throwsA('oh no'));
+      });
+
+      test('with a Future that throws a matching error', () {
+        expect(Future.error(const FormatException('bad')),
+            throwsA(isFormatException));
+      });
+
+      test("with a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.value(), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: emitted <null>\n')
+            ]));
+      });
+
+      test('with a Future that throws the wrong error', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.error('aw dang'), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n")
+            ]));
+      });
+
+      test('with a closure that returns a Future that throws a matching error',
+          () {
+        expect(() => Future.error(const FormatException('bad')),
+            throwsA(isFormatException));
+      });
+
+      test("with a closure that returns a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.value, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned a Future that emitted <null>\n')
+            ]));
+      });
+
+      test('with closure that returns a Future that throws the wrong error',
+          () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(() => Future.error('aw dang'), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n")
+            ]));
+      });
+
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
+          expect(completer.future, throwsA('oh no'));
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+
+        expectTestPassed(monitor);
+      });
+
+      test("blocks expectLater's Future", () async {
+        var completer = Completer();
+        var fired = false;
+        unawaited(expectLater(completer.future, throwsArgumentError).then((_) {
+          fired = true;
+        }));
+
+        await pumpEventQueue();
+        expect(fired, isFalse);
+
+        completer.completeError(ArgumentError('oh no'));
+        await pumpEventQueue();
+        expect(fired, isTrue);
+      });
+    });
+  });
+}
diff --git a/test/matcher/throws_type_test.dart b/test/matcher/throws_type_test.dart
new file mode 100644
index 0000000..1c3d193
--- /dev/null
+++ b/test/matcher/throws_type_test.dart
@@ -0,0 +1,176 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('[throwsArgumentError]', () {
+    test('passes when a ArgumentError is thrown', () {
+      expect(() => throw ArgumentError(''), throwsArgumentError);
+    });
+
+    test('fails when a non-ArgumentError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsArgumentError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'ArgumentError'>"));
+    });
+  });
+
+  group('[throwsConcurrentModificationError]', () {
+    test('passes when a ConcurrentModificationError is thrown', () {
+      expect(() => throw ConcurrentModificationError(''),
+          throwsConcurrentModificationError);
+    });
+
+    test('fails when a non-ConcurrentModificationError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsConcurrentModificationError);
+      });
+
+      expectTestFailed(
+          liveTest,
+          startsWith(
+              "Expected: throws <Instance of 'ConcurrentModificationError'>"));
+    });
+  });
+
+  group('[throwsCyclicInitializationError]', () {
+    test('passes when a CyclicInitializationError is thrown', () {
+      expect(
+          () => _CyclicInitializationFailure().x,
+          // ignore: deprecated_member_use
+          throwsCyclicInitializationError);
+    });
+
+    test('fails when a non-CyclicInitializationError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use
+        expect(() => throw Exception(), throwsCyclicInitializationError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'Error'>"));
+    });
+  });
+
+  group('[throwsException]', () {
+    test('passes when a Exception is thrown', () {
+      expect(() => throw Exception(''), throwsException);
+    });
+
+    test('fails when a non-Exception is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw 'oh no', throwsException);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'Exception'>"));
+    });
+  });
+
+  group('[throwsFormatException]', () {
+    test('passes when a FormatException is thrown', () {
+      expect(() => throw const FormatException(''), throwsFormatException);
+    });
+
+    test('fails when a non-FormatException is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsFormatException);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'FormatException'>"));
+    });
+  });
+
+  group('[throwsNoSuchMethodError]', () {
+    test('passes when a NoSuchMethodError is thrown', () {
+      expect(() {
+        (1 as dynamic).notAMethodOnInt();
+      }, throwsNoSuchMethodError);
+    });
+
+    test('fails when a non-NoSuchMethodError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsNoSuchMethodError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'NoSuchMethodError'>"));
+    });
+  });
+
+  group('[throwsRangeError]', () {
+    test('passes when a RangeError is thrown', () {
+      expect(() => throw RangeError(''), throwsRangeError);
+    });
+
+    test('fails when a non-RangeError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsRangeError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'RangeError'>"));
+    });
+  });
+
+  group('[throwsStateError]', () {
+    test('passes when a StateError is thrown', () {
+      expect(() => throw StateError(''), throwsStateError);
+    });
+
+    test('fails when a non-StateError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsStateError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'StateError'>"));
+    });
+  });
+
+  group('[throwsUnimplementedError]', () {
+    test('passes when a UnimplementedError is thrown', () {
+      expect(() => throw UnimplementedError(''), throwsUnimplementedError);
+    });
+
+    test('fails when a non-UnimplementedError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsUnimplementedError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'UnimplementedError'>"));
+    });
+  });
+
+  group('[throwsUnsupportedError]', () {
+    test('passes when a UnsupportedError is thrown', () {
+      expect(() => throw UnsupportedError(''), throwsUnsupportedError);
+    });
+
+    test('fails when a non-UnsupportedError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsUnsupportedError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'UnsupportedError'>"));
+    });
+  });
+}
+
+class _CyclicInitializationFailure {
+  late int x = y;
+  late int y = x;
+}
diff --git a/test/never_called_test.dart b/test/never_called_test.dart
new file mode 100644
index 0000000..4c83e39
--- /dev/null
+++ b/test/never_called_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2017, 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 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  setUpAll(() {
+    glyph.ascii = true;
+  });
+
+  test("doesn't throw if it isn't called", () async {
+    var monitor = await TestCaseMonitor.run(() {
+      const Stream.empty().listen(neverCalled);
+    });
+
+    expectTestPassed(monitor);
+  });
+
+  group("if it's called", () {
+    test('throws', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        neverCalled();
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with no '
+          'arguments.');
+    });
+
+    test('pretty-prints arguments', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        neverCalled(1, 'foo\nbar');
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with:\n'
+          '* <1>\n'
+          "* 'foo\\n'\n"
+          "    'bar'");
+    });
+
+    test('keeps the test alive', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        pumpEventQueue(times: 10).then(neverCalled);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with:\n'
+          '* <null>');
+    });
+
+    test("can't be caught", () async {
+      var monitor = await TestCaseMonitor.run(() {
+        try {
+          neverCalled();
+        } catch (_) {
+          // Do nothing.
+        }
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with '
+          'no arguments.');
+    });
+  });
+}
diff --git a/test/stream_matcher_test.dart b/test/stream_matcher_test.dart
new file mode 100644
index 0000000..c4af666
--- /dev/null
+++ b/test/stream_matcher_test.dart
@@ -0,0 +1,358 @@
+// Copyright (c) 2017, 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:async/async.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  setUpAll(() {
+    glyph.ascii = true;
+  });
+
+  late Stream stream;
+  late StreamQueue queue;
+  late Stream errorStream;
+  late StreamQueue errorQueue;
+  setUp(() {
+    stream = Stream.fromIterable([1, 2, 3, 4, 5]);
+    queue = StreamQueue(Stream.fromIterable([1, 2, 3, 4, 5]));
+    errorStream = Stream.fromFuture(Future.error('oh no!', StackTrace.current));
+    errorQueue = StreamQueue(
+        Stream.fromFuture(Future.error('oh no!', StackTrace.current)));
+  });
+
+  group('emits()', () {
+    test('matches the first event of a Stream', () {
+      expect(stream, emits(1));
+    });
+
+    test('rejects the first event of a Stream', () {
+      expect(
+          expectLater(stream, emits(2)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+
+    test('matches and consumes the next event of a StreamQueue', () {
+      expect(queue, emits(1));
+      expect(queue.next, completion(equals(2)));
+      expect(queue, emits(3));
+      expect(queue.next, completion(equals(4)));
+    });
+
+    test('rejects and does not consume the first event of a StreamQueue', () {
+      expect(
+          expectLater(queue, emits(2)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+
+      expect(queue, emits(1));
+    });
+
+    test('rejects an empty stream', () {
+      expect(
+          expectLater(const Stream.empty(), emits(1)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <1>\n'),
+            endsWith('   Which: emitted x Stream closed.\n')
+          ])));
+    });
+
+    test('forwards a stream error', () {
+      expect(expectLater(errorStream, emits(1)), throwsA('oh no!'));
+    });
+
+    test('wraps a normal matcher', () {
+      expect(queue, emits(lessThan(5)));
+      expect(expectLater(queue, emits(greaterThan(5))),
+          throwsTestFailure(anything));
+    });
+
+    test('returns a StreamMatcher as-is', () {
+      expect(queue, emits(emitsThrough(4)));
+      expect(queue, emits(5));
+    });
+  });
+
+  group('emitsDone', () {
+    test('succeeds for an empty stream', () {
+      expect(const Stream.empty(), emitsDone);
+    });
+
+    test('fails for a stream with events', () {
+      expect(
+          expectLater(stream, emitsDone),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should be done\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+  });
+
+  group('emitsError()', () {
+    test('consumes a matching error', () {
+      expect(errorQueue, emitsError('oh no!'));
+      expect(errorQueue.hasNext, completion(isFalse));
+    });
+
+    test('fails for a non-matching error', () {
+      expect(
+          expectLater(errorStream, emitsError('oh heck')),
+          throwsTestFailure(allOf([
+            startsWith("Expected: should emit an error that 'oh heck'\n"),
+            contains('   Which: emitted ! oh no!\n'),
+            contains('                  x Stream closed.\n'
+                "            which threw 'oh no!'\n"
+                '                  stack '),
+            endsWith('                  which is different.\n'
+                '                        Expected: oh heck\n'
+                '                          Actual: oh no!\n'
+                '                                     ^\n'
+                '                         Differ at offset 3\n')
+          ])));
+    });
+
+    test('fails for a stream with events', () {
+      expect(
+          expectLater(stream, emitsDone),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should be done\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+  });
+
+  group('mayEmit()', () {
+    test('consumes a matching event', () {
+      expect(queue, mayEmit(1));
+      expect(queue, emits(2));
+    });
+
+    test('allows a non-matching event', () {
+      expect(queue, mayEmit('fish'));
+      expect(queue, emits(1));
+    });
+  });
+
+  group('emitsAnyOf()', () {
+    test('consumes an event that matches a matcher', () {
+      expect(queue, emitsAnyOf([2, 1, 3]));
+      expect(queue, emits(2));
+    });
+
+    test('consumes as many events as possible', () {
+      expect(
+          queue,
+          emitsAnyOf([
+            1,
+            emitsInOrder([1, 2]),
+            emitsInOrder([1, 2, 3])
+          ]));
+
+      expect(queue, emits(4));
+    });
+
+    test('fails if no matchers match', () {
+      expect(
+          expectLater(stream, emitsAnyOf([2, 3, 4])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do one of the following:\n'
+                '          * emit an event that <2>\n'
+                '          * emit an event that <3>\n'
+                '          * emit an event that <4>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which failed all options:\n'
+                '                  * failed to emit an event that <2>\n'
+                '                  * failed to emit an event that <3>\n'
+                '                  * failed to emit an event that <4>\n')
+          ])));
+    });
+
+    test('allows an error if any matcher matches', () {
+      expect(errorStream, emitsAnyOf([1, 2, emitsError('oh no!')]));
+    });
+
+    test('rethrows an error if no matcher matches', () {
+      expect(
+          expectLater(errorStream, emitsAnyOf([1, 2, 3])), throwsA('oh no!'));
+    });
+  });
+
+  group('emitsInOrder()', () {
+    test('consumes matching events', () {
+      expect(queue, emitsInOrder([1, 2, emitsThrough(4)]));
+      expect(queue, emits(5));
+    });
+
+    test("fails if the matchers don't match in order", () {
+      expect(
+          expectLater(queue, emitsInOrder([1, 3, 2])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do the following in order:\n'
+                '          * emit an event that <1>\n'
+                '          * emit an event that <3>\n'
+                '          * emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                "            which didn't emit an event that <3>\n")
+          ])));
+    });
+  });
+
+  group('emitsThrough()', () {
+    test('consumes events including those matching the matcher', () {
+      expect(queue, emitsThrough(emitsInOrder([3, 4])));
+      expect(queue, emits(5));
+    });
+
+    test('consumes the entire queue with emitsDone', () {
+      expect(queue, emitsThrough(emitsDone));
+      expect(queue.hasNext, completion(isFalse));
+    });
+
+    test('fails if the queue never matches the matcher', () {
+      expect(
+          expectLater(queue, emitsThrough(6)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should eventually emit an event that <6>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which never did emit an event that <6>\n')
+          ])));
+    });
+  });
+
+  group('mayEmitMultiple()', () {
+    test('consumes multiple instances of the given matcher', () {
+      expect(queue, mayEmitMultiple(lessThan(3)));
+      expect(queue, emits(3));
+    });
+
+    test('consumes zero instances of the given matcher', () {
+      expect(queue, mayEmitMultiple(6));
+      expect(queue, emits(1));
+    });
+
+    test("doesn't rethrow errors", () {
+      expect(errorQueue, mayEmitMultiple(1));
+      expect(errorQueue, emitsError('oh no!'));
+    });
+  });
+
+  group('neverEmits()', () {
+    test('succeeds if the event never matches', () {
+      expect(queue, neverEmits(6));
+      expect(queue, emits(1));
+    });
+
+    test('fails if the event matches', () {
+      expect(
+          expectLater(stream, neverEmits(4)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should never emit an event that <4>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which after 3 events did emit an event that <4>\n')
+          ])));
+    });
+
+    test('fails if emitsDone matches', () {
+      expect(expectLater(stream, neverEmits(emitsDone)),
+          throwsTestFailure(anything));
+    });
+
+    test("doesn't rethrow errors", () {
+      expect(errorQueue, neverEmits(6));
+      expect(errorQueue, emitsError('oh no!'));
+    });
+  });
+
+  group('emitsInAnyOrder()', () {
+    test('consumes events that match in any order', () {
+      expect(queue, emitsInAnyOrder([3, 1, 2]));
+      expect(queue, emits(4));
+    });
+
+    test("fails if the events don't match in any order", () {
+      expect(
+          expectLater(stream, emitsInAnyOrder([4, 1, 2])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do the following in any order:\n'
+                '          * emit an event that <4>\n'
+                '          * emit an event that <1>\n'
+                '          * emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+
+    test("doesn't rethrow if some ordering matches", () {
+      expect(errorQueue, emitsInAnyOrder([emitsDone, emitsError('oh no!')]));
+    });
+
+    test('rethrows if no ordering matches', () {
+      expect(
+          expectLater(errorQueue, emitsInAnyOrder([1, emitsError('oh no!')])),
+          throwsA('oh no!'));
+    });
+  });
+
+  test('A custom StreamController doesn\'t hang on close', () async {
+    var controller = StreamController<void>();
+    var done = expectLater(controller.stream, emits(null));
+    controller.add(null);
+    await done;
+    await controller.close();
+  });
+}
diff --git a/test/string_matchers_test.dart b/test/string_matchers_test.dart
index 2b42e43..be9e768 100644
--- a/test/string_matchers_test.dart
+++ b/test/string_matchers_test.dart
@@ -103,8 +103,8 @@
     shouldPass('hello', contains('o'));
     shouldPass('hello', contains('hell'));
     shouldPass('hello', contains('hello'));
-    shouldFail(
-        'hello', contains(' '), "Expected: contains ' ' Actual: 'hello'");
+    shouldFail('hello', contains(' '),
+        "Expected: contains ' ' Actual: 'hello' Which: does not contain ' '");
   });
 
   test('stringContainsInOrder', () {
diff --git a/test/utils_new.dart b/test/utils_new.dart
new file mode 100644
index 0000000..7d85a02
--- /dev/null
+++ b/test/utils_new.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, 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:matcher/expect.dart';
+import 'package:test_api/hooks_testing.dart';
+
+/// Asserts that [monitor] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(TestCaseMonitor monitor) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the running test, because they're definitely unexpected and it is most
+  // useful for the error to point directly to the throw point.
+  for (var error in monitor.errors) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  }
+  monitor.onError.listen((error) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  });
+
+  expect(monitor.state, State.passed);
+}
+
+/// Asserts that [monitor] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(TestCaseMonitor monitor, Object? message) {
+  expect(monitor.state, State.failed);
+  expect(monitor.errors, [isAsyncError(isTestFailure(message))]);
+}
+
+/// Returns a matcher that matches a [AsyncError] with an `error` field matching
+/// [errorMatcher].
+Matcher isAsyncError(Matcher errorMatcher) =>
+    isA<AsyncError>().having((e) => e.error, 'error', errorMatcher);
+
+/// Returns a matcher that matches a [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher isTestFailure(Object? message) => const TypeMatcher<TestFailure>()
+    .having((e) => e.message, 'message', message);
+
+/// Returns a matcher that matches a callback or Future that throws a
+/// [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher throwsTestFailure(Object? message) => throwsA(isTestFailure(message));