Merge branch 'master' into top-level-equals
diff --git a/pkgs/checks/lib/checks.dart b/pkgs/checks/lib/checks.dart
index db8b18e..a390e75 100644
--- a/pkgs/checks/lib/checks.dart
+++ b/pkgs/checks/lib/checks.dart
@@ -7,7 +7,7 @@
 export 'src/extensions/async.dart'
     show FutureChecks, StreamChecks, WithQueueExtension;
 export 'src/extensions/core.dart'
-    show BoolChecks, ComparableChecks, CoreChecks, NullableChecks;
+    show BoolChecks, ComparableChecks, CoreChecks, NullableChecks, equals;
 export 'src/extensions/function.dart' show FunctionChecks;
 export 'src/extensions/iterable.dart' show IterableChecks;
 export 'src/extensions/map.dart' show MapChecks;
diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart
index 1fd360b..8f2f073 100644
--- a/pkgs/checks/lib/src/extensions/async.dart
+++ b/pkgs/checks/lib/src/extensions/async.dart
@@ -230,8 +230,8 @@
   ///
   /// ```dart
   /// await check(someStream).withQueue.inOrder([
-  ///   (s) => s.emits((e) => e.equals(0)),
-  ///   (s) => s.emits((e) => e.equals(1)),
+  ///   (s) => s.emits(equals(0)),
+  ///   (s) => s.emits(equals(1)),
   //  ]);
   /// ```
   ///
diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart
index 6212f91..da1eac1 100644
--- a/pkgs/checks/lib/src/extensions/core.dart
+++ b/pkgs/checks/lib/src/extensions/core.dart
@@ -102,6 +102,19 @@
   }
 }
 
+/// Returns a [Condition] checking that the actual value is equal to [expected]
+/// by operator `==`.
+///
+/// This is a shortcut for `(Subject<T> it) => it..equals(expected)`.
+Condition<T> equals<T>(T expected) => (Subject<T> subject) {
+      if (subject is Subject<String> && expected is String) {
+        // String specializes `equals` with a better failure
+        (subject as Subject<String>).equals(expected);
+      } else {
+        subject.equals(expected);
+      }
+    };
+
 extension BoolChecks on Subject<bool> {
   void isTrue() {
     context.expect(
diff --git a/pkgs/checks/test/describe_test.dart b/pkgs/checks/test/describe_test.dart
index d117a7b..fedd620 100644
--- a/pkgs/checks/test/describe_test.dart
+++ b/pkgs/checks/test/describe_test.dart
@@ -12,7 +12,7 @@
       check(describe((_) {})).isEmpty();
     });
     test('includes condition clauses', () {
-      check(describe((it) => it.equals(1))).deepEquals(['  equals <1>']);
+      check(describe(equals(1))).deepEquals(['  equals <1>']);
     });
     test('includes nested clauses', () {
       check(describe<String>((it) => it.length.equals(1))).deepEquals([
diff --git a/pkgs/checks/test/extensions/async_test.dart b/pkgs/checks/test/extensions/async_test.dart
index 43ba5d7..3484fb2 100644
--- a/pkgs/checks/test/extensions/async_test.dart
+++ b/pkgs/checks/test/extensions/async_test.dart
@@ -16,11 +16,11 @@
   group('FutureChecks', () {
     group('completes', () {
       test('succeeds for a future that completes to a value', () async {
-        await check(_futureSuccess()).completes((it) => it.equals(42));
+        await check(_futureSuccess()).completes(equals(42));
       });
       test('rejects futures which complete as errors', () async {
         await check(_futureFail()).isRejectedByAsync(
-          (it) => it.completes((it) => it.equals(1)),
+          (it) => it.completes(equals(1)),
           actual: ['a future that completes as an error'],
           which: ['threw <UnimplementedError> at:', 'fake trace'],
         );
@@ -29,7 +29,7 @@
         await check((Subject<Future> it) => it.completes())
             .hasAsyncDescriptionWhich(
                 (it) => it.deepEquals(['  completes to a value']));
-        await check((Subject<Future> it) => it.completes((it) => it.equals(42)))
+        await check((Subject<Future> it) => it.completes(equals(42)))
             .hasAsyncDescriptionWhich((it) => it.deepEquals([
                   '  completes to a value that:',
                   '    equals <42>',
@@ -127,7 +127,7 @@
   group('StreamChecks', () {
     group('emits', () {
       test('succeeds for a stream that emits a value', () async {
-        await check(_countingStream(5)).emits((it) => it.equals(0));
+        await check(_countingStream(5)).emits(equals(0));
       });
       test('fails for a stream that closes without emitting', () async {
         await check(_countingStream(0)).isRejectedByAsync(
@@ -147,8 +147,7 @@
         await check((Subject<StreamQueue<void>> it) => it.emits())
             .hasAsyncDescriptionWhich(
                 (it) => it.deepEquals(['  emits a value']));
-        await check((Subject<StreamQueue<int>> it) =>
-                it.emits((it) => it.equals(42)))
+        await check((Subject<StreamQueue<int>> it) => it.emits(equals(42)))
             .hasAsyncDescriptionWhich((it) => it.deepEquals([
                   '  emits a value that:',
                   '    equals <42>',
@@ -209,26 +208,26 @@
       test('uses a transaction', () async {
         final queue = _countingStream(1);
         await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emitsError());
-        await check(queue).emits((it) => it.equals(0));
+        await check(queue).emits(equals(0));
       });
     });
 
     group('emitsThrough', () {
       test('succeeds for a stream that eventuall emits a matching value',
           () async {
-        await check(_countingStream(5)).emitsThrough((it) => it.equals(4));
+        await check(_countingStream(5)).emitsThrough(equals(4));
       });
       test('fails for a stream that closes without emitting a matching value',
           () async {
         await check(_countingStream(4)).isRejectedByAsync(
-          (it) => it.emitsThrough((it) => it.equals(5)),
+          (it) => it.emitsThrough(equals(5)),
           actual: ['a stream'],
           which: ['ended after emitting 4 elements with none matching'],
         );
       });
       test('can be described', () async {
-        await check((Subject<StreamQueue<int>> it) =>
-                it.emitsThrough((it) => it.equals(42)))
+        await check(
+                (Subject<StreamQueue<int>> it) => it.emitsThrough(equals(42)))
             .hasAsyncDescriptionWhich((it) => it.deepEquals([
                   '  emits any values then emits a value that:',
                   '    equals <42>'
@@ -236,24 +235,22 @@
       });
       test('uses a transaction', () async {
         final queue = _countingStream(1);
-        await softCheckAsync(
-            queue,
-            (Subject<StreamQueue<int>> it) =>
-                it.emitsThrough((it) => it.equals(42)));
-        check(queue).emits((it) => it.equals(0));
+        await softCheckAsync(queue,
+            (Subject<StreamQueue<int>> it) => it.emitsThrough(equals(42)));
+        check(queue).emits(equals(0));
       });
       test('consumes events', () async {
         final queue = _countingStream(3);
-        await check(queue).emitsThrough((it) => it.equals(1));
-        await check(queue).emits((it) => it.equals(2));
+        await check(queue).emitsThrough(equals(1));
+        await check(queue).emits(equals(2));
       });
     });
 
     group('emitsInOrder', () {
       test('succeeds for happy case', () async {
         await check(_countingStream(2)).inOrder([
-          (it) => it.emits((it) => it.equals(0)),
-          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.emits(equals(0)),
+          (it) => it.emits(equals(1)),
           (it) => it.isDone(),
         ]);
       });
@@ -270,8 +267,7 @@
       });
       test('nestes the report for deep failures', () async {
         await check(_countingStream(2)).isRejectedByAsync(
-          (it) => it.inOrder(
-              [(it) => it.emits(), (it) => it.emits((it) => it.equals(2))]),
+          (it) => it.inOrder([(it) => it.emits(), (it) => it.emits(equals(2))]),
           actual: ['a stream'],
           which: [
             'satisfied 1 conditions then',
@@ -294,21 +290,21 @@
         await softCheckAsync<StreamQueue<int>>(
             queue,
             (it) => it.inOrder([
-                  (it) => it.emits((it) => it.equals(0)),
-                  (it) => it.emits((it) => it.equals(1)),
-                  (it) => it.emits((it) => it.equals(42)),
+                  (it) => it.emits(equals(0)),
+                  (it) => it.emits(equals(1)),
+                  (it) => it.emits(equals(42)),
                 ]));
         await check(queue).inOrder([
-          (it) => it.emits((it) => it.equals(0)),
-          (it) => it.emits((it) => it.equals(1)),
-          (it) => it.emits((it) => it.equals(2)),
+          (it) => it.emits(equals(0)),
+          (it) => it.emits(equals(1)),
+          (it) => it.emits(equals(2)),
           (it) => it.isDone(),
         ]);
       });
       test('consumes events', () async {
         final queue = _countingStream(3);
         await check(queue).inOrder([(it) => it.emits(), (it) => it.emits()]);
-        await check(queue).emits((it) => it.equals(2));
+        await check(queue).emits(equals(2));
       });
     });
 
@@ -316,18 +312,17 @@
       test(
           'succeeds for a stream that closes without emitting a matching value',
           () async {
-        await check(_countingStream(5)).neverEmits((it) => it.equals(5));
+        await check(_countingStream(5)).neverEmits(equals(5));
       });
       test('fails for a stream that emits a matching value', () async {
         await check(_countingStream(6)).isRejectedByAsync(
-          (it) => it.neverEmits((it) => it.equals(5)),
+          (it) => it.neverEmits(equals(5)),
           actual: ['a stream'],
           which: ['emitted <5>', 'following 5 other items'],
         );
       });
       test('can be described', () async {
-        await check((Subject<StreamQueue<int>> it) =>
-                it.neverEmits((it) => it.equals(42)))
+        await check((Subject<StreamQueue<int>> it) => it.neverEmits(equals(42)))
             .hasAsyncDescriptionWhich((it) => it.deepEquals([
                   '  never emits a value that:',
                   '    equals <42>',
@@ -336,10 +331,10 @@
       test('uses a transaction', () async {
         final queue = _countingStream(2);
         await softCheckAsync<StreamQueue<int>>(
-            queue, (it) => it.neverEmits((it) => it.equals(1)));
+            queue, (it) => it.neverEmits(equals(1)));
         await check(queue).inOrder([
-          (it) => it.emits((it) => it.equals(0)),
-          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.emits(equals(0)),
+          (it) => it.emits(equals(1)),
           (it) => it.isDone(),
         ]);
       });
@@ -347,31 +342,30 @@
 
     group('mayEmit', () {
       test('succeeds for a stream that emits a matching value', () async {
-        await check(_countingStream(1)).mayEmit((it) => it.equals(0));
+        await check(_countingStream(1)).mayEmit(equals(0));
       });
       test('succeeds for a stream that emits an error', () async {
-        await check(_countingStream(1, errorAt: 0))
-            .mayEmit((it) => it.equals(0));
+        await check(_countingStream(1, errorAt: 0)).mayEmit(equals(0));
       });
       test('succeeds for a stream that closes', () async {
-        await check(_countingStream(0)).mayEmit((it) => it.equals(42));
+        await check(_countingStream(0)).mayEmit(equals(42));
       });
       test('consumes a matching event', () async {
         final queue = _countingStream(2);
         await softCheckAsync<StreamQueue<int>>(
-            queue, (it) => it.mayEmit((it) => it.equals(0)));
-        await check(queue).emits((it) => it.equals(1));
+            queue, (it) => it.mayEmit(equals(0)));
+        await check(queue).emits(equals(1));
       });
       test('does not consume a non-matching event', () async {
         final queue = _countingStream(2);
         await softCheckAsync<StreamQueue<int>>(
-            queue, (it) => it.mayEmit((it) => it.equals(1)));
-        await check(queue).emits((it) => it.equals(0));
+            queue, (it) => it.mayEmit(equals(1)));
+        await check(queue).emits(equals(0));
       });
       test('does not consume an error', () async {
         final queue = _countingStream(1, errorAt: 0);
         await softCheckAsync<StreamQueue<int>>(
-            queue, (it) => it.mayEmit((it) => it.equals(0)));
+            queue, (it) => it.mayEmit(equals(0)));
         await check(queue).emitsError<UnimplementedError>(
             (it) => it.has((e) => e.message, 'message').equals('Error at 1'));
       });
@@ -379,31 +373,30 @@
 
     group('mayEmitMultiple', () {
       test('succeeds for a stream that emits a matching value', () async {
-        await check(_countingStream(1)).mayEmitMultiple((it) => it.equals(0));
+        await check(_countingStream(1)).mayEmitMultiple(equals(0));
       });
       test('succeeds for a stream that emits an error', () async {
-        await check(_countingStream(1, errorAt: 0))
-            .mayEmitMultiple((it) => it.equals(0));
+        await check(_countingStream(1, errorAt: 0)).mayEmitMultiple(equals(0));
       });
       test('succeeds for a stream that closes', () async {
-        await check(_countingStream(0)).mayEmitMultiple((it) => it.equals(42));
+        await check(_countingStream(0)).mayEmitMultiple(equals(42));
       });
       test('consumes matching events', () async {
         final queue = _countingStream(3);
         await softCheckAsync<StreamQueue<int>>(
             queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(2)));
-        await check(queue).emits((it) => it.equals(2));
+        await check(queue).emits(equals(2));
       });
       test('consumes no events if no events match', () async {
         final queue = _countingStream(2);
         await softCheckAsync<StreamQueue<int>>(
             queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(0)));
-        await check(queue).emits((it) => it.equals(0));
+        await check(queue).emits(equals(0));
       });
       test('does not consume an error', () async {
         final queue = _countingStream(1, errorAt: 0);
         await softCheckAsync<StreamQueue<int>>(
-            queue, (it) => it.mayEmitMultiple((it) => it.equals(0)));
+            queue, (it) => it.mayEmitMultiple(equals(0)));
         await check(queue).emitsError<UnimplementedError>(
             (it) => it.has((e) => e.message, 'message').equals('Error at 1'));
       });
@@ -428,7 +421,7 @@
       test('uses a transaction', () async {
         final queue = _countingStream(1);
         await softCheckAsync<StreamQueue<int>>(queue, (it) => it.isDone());
-        await check(queue).emits((it) => it.equals(0));
+        await check(queue).emits(equals(0));
       });
       test('can be described', () async {
         await check((Subject<StreamQueue<int>> it) => it.isDone())
@@ -438,16 +431,14 @@
 
     group('emitsAnyOf', () {
       test('succeeds for a stream that matches one condition', () async {
-        await check(_countingStream(1)).anyOf([
-          (it) => it.emits((it) => it.equals(42)),
-          (it) => it.emits((it) => it.equals(0))
-        ]);
+        await check(_countingStream(1))
+            .anyOf([(it) => it.emits(equals(42)), (it) => it.emits(equals(0))]);
       });
       test('fails for a stream that matches no conditions', () async {
         await check(_countingStream(0)).isRejectedByAsync(
             (it) => it.anyOf([
                   (it) => it.emits(),
-                  (it) => it.emitsThrough((it) => it.equals(1)),
+                  (it) => it.emitsThrough(equals(1)),
                 ]),
             actual: [
               'a stream'
@@ -463,8 +454,8 @@
       test('includes nested details for nested failures', () async {
         await check(_countingStream(1)).isRejectedByAsync(
             (it) => it.anyOf([
-                  (it) => it.emits((it) => it.equals(42)),
-                  (it) => it.emitsThrough((it) => it.equals(10)),
+                  (it) => it.emits(equals(42)),
+                  (it) => it.emitsThrough(equals(10)),
                 ]),
             actual: [
               'a stream'
@@ -490,18 +481,16 @@
         await softCheckAsync<StreamQueue<int>>(
             queue,
             (it) => it.anyOf([
-                  (it) => it.emits((it) => it.equals(10)),
-                  (it) => it.emitsThrough((it) => it.equals(42)),
+                  (it) => it.emits(equals(10)),
+                  (it) => it.emitsThrough(equals(42)),
                 ]));
-        await check(queue).emits((it) => it.equals(0));
+        await check(queue).emits(equals(0));
       });
       test('consumes events', () async {
         final queue = _countingStream(3);
-        await check(queue).anyOf([
-          (it) => it.emits((it) => it.equals(1)),
-          (it) => it.emitsThrough((it) => it.equals(1))
-        ]);
-        await check(queue).emits((it) => it.equals(2));
+        await check(queue).anyOf(
+            [(it) => it.emits(equals(1)), (it) => it.emitsThrough(equals(1))]);
+        await check(queue).emits(equals(2));
       });
     });
   });
diff --git a/pkgs/checks/test/extensions/iterable_test.dart b/pkgs/checks/test/extensions/iterable_test.dart
index 3d31a81..73c0881 100644
--- a/pkgs/checks/test/extensions/iterable_test.dart
+++ b/pkgs/checks/test/extensions/iterable_test.dart
@@ -66,8 +66,8 @@
         .isRejectedBy((it) => it.contains(2), which: ['does not contain <2>']);
   });
   test('any', () {
-    check(_testIterable).any((it) => it.equals(1));
-    check(_testIterable).isRejectedBy((it) => it.any((it) => it.equals(2)),
+    check(_testIterable).any(equals(1));
+    check(_testIterable).isRejectedBy((it) => it.any(equals(2)),
         which: ['Contains no matching element']);
   });
 
@@ -152,14 +152,14 @@
 
   group('unorderedMatches', () {
     test('success for happy case', () {
-      check(_testIterable).unorderedMatches(
-          _testIterable.toList().reversed.map((i) => (it) => it.equals(i)));
+      check(_testIterable)
+          .unorderedMatches(_testIterable.toList().reversed.map(equals));
     });
 
     test('reports unmatched elements', () {
       check(_testIterable).isRejectedBy(
-          (it) => it.unorderedMatches(_testIterable
-              .followedBy([42, 100]).map((i) => (it) => it.equals(i))),
+          (it) => it.unorderedMatches(
+              _testIterable.followedBy([42, 100]).map(equals)),
           which: [
             'has no element matching the condition at index 2:',
             '  equals <42>',
@@ -169,8 +169,7 @@
 
     test('reports unexpected elements', () {
       check(_testIterable.followedBy([42, 100])).isRejectedBy(
-          (it) => it
-              .unorderedMatches(_testIterable.map((i) => (it) => it.equals(i))),
+          (it) => it.unorderedMatches(_testIterable.map(equals)),
           which: [
             'has an unmatched element at index 2: <42>',
             'and 1 other unmatched elements'
diff --git a/pkgs/checks/test/extensions/map_test.dart b/pkgs/checks/test/extensions/map_test.dart
index 99d1b4f..1b2684c 100644
--- a/pkgs/checks/test/extensions/map_test.dart
+++ b/pkgs/checks/test/extensions/map_test.dart
@@ -83,9 +83,9 @@
     });
   });
   test('containsKeyThat', () {
-    check(_testMap).containsKeyThat((it) => it.equals('a'));
+    check(_testMap).containsKeyThat(equals('a'));
     check(_testMap).isRejectedBy(
-      (it) => it.containsKeyThat((it) => it.equals('c')),
+      (it) => it.containsKeyThat(equals('c')),
       which: ['Contains no matching key'],
     );
   });
@@ -109,9 +109,9 @@
     });
   });
   test('containsValueThat', () {
-    check(_testMap).containsValueThat((it) => it.equals(1));
+    check(_testMap).containsValueThat(equals(1));
     check(_testMap).isRejectedBy(
-      (it) => it.containsValueThat((it) => it.equals(3)),
+      (it) => it.containsValueThat(equals(3)),
       which: ['Contains no matching value'],
     );
   });