Add containsMatchingInOrder  containsEqualInOrder (#2284)

The joined behavior in `containsInOrder` has some usability issues:
- It mimics the arguments for `deepEquals`, but it doesn't have the same
  behavior for collection typed elements. Checking that a nested
  collection is contained in order requires a `Condition` callback that
  uses `.deepEquals` explicitly.
- The `Object?` signature throws away inference on the `Condition`
  callback arguments. With a method that supports only conditions the
  argument type can be tightened and allow inference.


Deprecate the old `containsInOrder` and plan to remove it before stable.
This is a bit more restrictive, but it's not too noisy to fit a few
`(it) => it.equals(foo)` in a collection that needs mixed behavior and
the collection of two methods is less confusing to document than the
joined behavior.

Lean on the "Matches" verb for cases that check a `Condition` callback
and rename `pairwiseComparesTo` as `pairwiseMatches`.

Fix a type check when pretty printing `Condition` callbacks. Match
more than `Condition<dynamic>` by checking `Condition<Never>`.
diff --git a/pkgs/checks/CHANGELOG.md b/pkgs/checks/CHANGELOG.md
index e52a3cb..f0c3efb 100644
--- a/pkgs/checks/CHANGELOG.md
+++ b/pkgs/checks/CHANGELOG.md
@@ -5,6 +5,9 @@
     for equality. This maintains the path into a nested collection for typical
     cases of checking for equality against a purely value collection.
 -   Always wrap Condition descriptions in angle brackets.
+-   Add `containsMatchingInOrder` and `containsEqualInOrder` to replace the
+    combined functionality in `containsInOrder`.
+-   Replace `pairwiseComparesTo` with `pairwiseMatches`.
 
 ## 0.3.0
 
diff --git a/pkgs/checks/doc/migrating_from_matcher.md b/pkgs/checks/doc/migrating_from_matcher.md
index 82c4e3c..b358e63 100644
--- a/pkgs/checks/doc/migrating_from_matcher.md
+++ b/pkgs/checks/doc/migrating_from_matcher.md
@@ -122,9 +122,14 @@
 -   `containsPair(key, value)` -> Use `Subject<Map>[key].equals(value)`
 -   `hasLength(expected)` -> `length.equals(expected)`
 -   `isNot(Matcher)` -> `not(conditionCallback)`
--   `pairwiseCompare` -> `pairwiseComparesTo`
+-   `pairwiseCompare` -> `pairwiseMatches`
 -   `same` -> `identicalTo`
 -   `stringContainsInOrder` -> `Subject<String>.containsInOrder`
+-   `containsAllInOrder(iterable)` ->
+    `Subject<Iterable>.containsMatchingInOrder(iterable)` to compare with
+    conditions other than equals,
+    `Subject<Iterable>.containsEqualInOrder(iterable)` to compare each index
+    with the equality operator (`==`).
 
 ### Members from `package:test/expect.dart` without a direct replacement
 
diff --git a/pkgs/checks/lib/src/describe.dart b/pkgs/checks/lib/src/describe.dart
index 22eb90e..f05a164 100644
--- a/pkgs/checks/lib/src/describe.dart
+++ b/pkgs/checks/lib/src/describe.dart
@@ -61,7 +61,7 @@
         .map((line) => line.replaceAll("'", r"\'"))
         .toList();
     return prefixFirst("'", postfixLast("'", escaped));
-  } else if (object is Condition) {
+  } else if (object is Condition<Never>) {
     return ['<A value that:', ...postfixLast('>', describe(object))];
   } else {
     final value = const LineSplitter().convert(object.toString());
diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart
index 2ab1bfa..ecfa9c0 100644
--- a/pkgs/checks/lib/src/extensions/iterable.dart
+++ b/pkgs/checks/lib/src/extensions/iterable.dart
@@ -89,6 +89,8 @@
   /// check([1, 0, 2, 0, 3])
   ///   .containsInOrder([1, (Subject<int> v) => v.isGreaterThan(1), 3]);
   /// ```
+  @Deprecated('Use `containsEqualInOrder` for expectations with values compared'
+      ' with `==` or `containsMatchingInOrder` for other expectations')
   void containsInOrder(Iterable<Object?> elements) {
     context.expect(() => prefixFirst('contains, in order: ', literal(elements)),
         (actual) {
@@ -115,6 +117,74 @@
     });
   }
 
+  /// Expects that the iterable contains a value matching each condition in
+  /// [conditions] in the given order, with any extra elements between them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  /// ```dart
+  /// check([1, 10, 2, 10, 3]).containsMatchingInOrder([
+  ///   (it) => it.isLessThan(2),
+  ///   (it) => it.isLessThan(3),
+  ///   (it) => it.isLessThan(4),
+  /// ]);
+  /// ```
+  void containsMatchingInOrder(Iterable<Condition<T>> conditions) {
+    context
+        .expect(() => prefixFirst('contains, in order: ', literal(conditions)),
+            (actual) {
+      final expected = conditions.toList();
+      if (expected.isEmpty) {
+        throw ArgumentError('expected may not be empty');
+      }
+      var expectedIndex = 0;
+      for (final element in actual) {
+        final currentExpected = expected[expectedIndex];
+        final matches = softCheck(element, currentExpected) == null;
+        if (matches && ++expectedIndex >= expected.length) return null;
+      }
+      return Rejection(which: [
+        ...prefixFirst(
+            'did not have an element matching the expectation at index '
+            '$expectedIndex ',
+            literal(expected[expectedIndex])),
+      ]);
+    });
+  }
+
+  /// Expects that the iterable contains a value equals to each expected value
+  /// from [elements] in the given order, with any extra elements between
+  /// them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  /// ```dart
+  /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
+  /// ```
+  ///
+  /// Values, will be compared with the equality operator.
+  void containsEqualInOrder(Iterable<T> elements) {
+    context.expect(() => prefixFirst('contains, in order: ', literal(elements)),
+        (actual) {
+      final expected = elements.toList();
+      if (expected.isEmpty) {
+        throw ArgumentError('expected may not be empty');
+      }
+      var expectedIndex = 0;
+      for (final element in actual) {
+        final currentExpected = expected[expectedIndex];
+        final matches = currentExpected == element;
+        if (matches && ++expectedIndex >= expected.length) return null;
+      }
+      return Rejection(which: [
+        ...prefixFirst(
+            'did not have an element equal to the expectation at index '
+            '$expectedIndex ',
+            literal(expected[expectedIndex])),
+      ]);
+    });
+  }
+
   /// Expects that the iterable contains at least on element such that
   /// [elementCondition] is satisfied.
   void any(Condition<T> elementCondition) {
@@ -250,7 +320,24 @@
   /// [description] is used in the Expected clause. It should be a predicate
   /// without the object, for example with the description 'is less than' the
   /// full expectation will be: "pairwise is less than $expected"
+  @Deprecated('Use `pairwiseMatches`')
   void pairwiseComparesTo<S>(List<S> expected,
+          Condition<T> Function(S) elementCondition, String description) =>
+      pairwiseMatches(expected, elementCondition, description);
+
+  /// Expects that the iterable contains elements that correspond by the
+  /// [elementCondition] exactly to each element in [expected].
+  ///
+  /// Fails if the iterable has a different length than [expected].
+  ///
+  /// For each element in the iterable, calls [elementCondition] with the
+  /// corresponding element from [expected] to get the specific condition for
+  /// that index.
+  ///
+  /// [description] is used in the Expected clause. It should be a predicate
+  /// without the object, for example with the description 'is less than' the
+  /// full expectation will be: "pairwise is less than $expected"
+  void pairwiseMatches<S>(List<S> expected,
       Condition<T> Function(S) elementCondition, String description) {
     context.expect(() {
       return prefixFirst('pairwise $description ', literal(expected));
diff --git a/pkgs/checks/test/extensions/iterable_test.dart b/pkgs/checks/test/extensions/iterable_test.dart
index 175919d..dafd84d 100644
--- a/pkgs/checks/test/extensions/iterable_test.dart
+++ b/pkgs/checks/test/extensions/iterable_test.dart
@@ -112,6 +112,69 @@
       ]);
     });
   });
+
+  group('containsMatchingInOrder', () {
+    test('succeeds for happy case', () {
+      check([0, 1, 0, 2, 0, 3]).containsMatchingInOrder([
+        (it) => it.isLessThan(2),
+        (it) => it.isLessThan(3),
+        (it) => it.isLessThan(4),
+      ]);
+    });
+    test('fails for not found elements', () async {
+      check([0]).isRejectedBy(
+          (it) => it.containsMatchingInOrder([(it) => it.isGreaterThan(0)]),
+          which: [
+            'did not have an element matching the expectation at index 0 '
+                '<A value that:',
+            '  is greater than <0>>'
+          ]);
+    });
+    test('can be described', () {
+      check((Subject<Iterable<int>> it) => it.containsMatchingInOrder([
+            (it) => it.isLessThan(2),
+            (it) => it.isLessThan(3),
+            (it) => it.isLessThan(4),
+          ])).description.deepEquals([
+        '  contains, in order: [<A value that:',
+        '    is less than <2>>,',
+        '  <A value that:',
+        '    is less than <3>>,',
+        '  <A value that:',
+        '    is less than <4>>]',
+      ]);
+      check((Subject<Iterable<int>> it) => it.containsMatchingInOrder(
+              [(it) => it.equals(1), (it) => it.equals(2)]))
+          .description
+          .deepEquals([
+        '  contains, in order: [<A value that:',
+        '    equals <1>>,',
+        '  <A value that:',
+        '    equals <2>>]'
+      ]);
+    });
+  });
+
+  group('containsEqualInOrder', () {
+    test('succeeds for happy case', () {
+      check([0, 1, 0, 2, 0, 3]).containsEqualInOrder([1, 2, 3]);
+    });
+    test('fails for not found elements', () async {
+      check([0]).isRejectedBy((it) => it.containsEqualInOrder([1]), which: [
+        'did not have an element equal to the expectation at index 0 <1>'
+      ]);
+    });
+    test('can be described', () {
+      check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2, 3]))
+          .description
+          .deepEquals(['  contains, in order: [1, 2, 3]']);
+      check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2]))
+          .description
+          .deepEquals([
+        '  contains, in order: [1, 2]',
+      ]);
+    });
+  });
   group('every', () {
     test('succeeds for the happy path', () {
       check(_testIterable).every((it) => it.isGreaterOrEqual(-1));
@@ -178,14 +241,14 @@
     });
   });
 
-  group('pairwiseComparesTo', () {
+  group('pairwiseMatches', () {
     test('succeeds for the happy path', () {
-      check(_testIterable).pairwiseComparesTo([1, 2],
+      check(_testIterable).pairwiseMatches([1, 2],
           (expected) => (it) => it.isLessThan(expected), 'is less than');
     });
     test('fails for mismatched element', () async {
       check(_testIterable).isRejectedBy(
-          (it) => it.pairwiseComparesTo([1, 1],
+          (it) => it.pairwiseMatches([1, 1],
               (expected) => (it) => it.isLessThan(expected), 'is less than'),
           which: [
             'does not have an element at index 1 that:',
@@ -196,7 +259,7 @@
     });
     test('fails for too few elements', () {
       check(_testIterable).isRejectedBy(
-          (it) => it.pairwiseComparesTo([1, 2, 3],
+          (it) => it.pairwiseMatches([1, 2, 3],
               (expected) => (it) => it.isLessThan(expected), 'is less than'),
           which: [
             'has too few elements, there is no element to match at index 2'
@@ -204,7 +267,7 @@
     });
     test('fails for too many elements', () {
       check(_testIterable).isRejectedBy(
-          (it) => it.pairwiseComparesTo([1],
+          (it) => it.pairwiseMatches([1],
               (expected) => (it) => it.isLessThan(expected), 'is less than'),
           which: ['has too many elements, expected exactly 1']);
     });