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']);
});