Add containsOnce matcher (#198)
Checks that all elements of an iterable don't match, except one.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6db1541..77c0822 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.12.14
+
+* Add `containsOnce` matcher.
+
## 0.12.13
* Require Dart 2.17 or greater.
diff --git a/lib/src/iterable_matchers.dart b/lib/src/iterable_matchers.dart
index 0345661..918650a 100644
--- a/lib/src/iterable_matchers.dart
+++ b/lib/src/iterable_matchers.dart
@@ -368,3 +368,49 @@
Description mismatchDescription, Map matchState, bool verbose) =>
mismatchDescription.add(_test(item, matchState)!);
}
+
+/// Matches [Iterable]s where exactly one element matches the expected
+/// value, and all other elements don't match.
+Matcher containsOnce(Object? expected) => _ContainsOnce(expected);
+
+class _ContainsOnce extends _IterableMatcher {
+ final Object? _expected;
+
+ _ContainsOnce(this._expected);
+
+ String? _test(Iterable item, Map matchState) {
+ var matcher = wrapMatcher(_expected);
+ var matches = [
+ for (var value in item)
+ if (matcher.matches(value, matchState)) value,
+ ];
+ if (matches.length == 1) {
+ return null;
+ }
+ if (matches.isEmpty) {
+ return StringDescription()
+ .add('did not find a value matching ')
+ .addDescriptionOf(matcher)
+ .toString();
+ }
+ return StringDescription()
+ .add('expected only one value matching ')
+ .addDescriptionOf(matcher)
+ .add(' but found multiple: ')
+ .addAll('', ', ', '', matches)
+ .toString();
+ }
+
+ @override
+ bool typedMatches(Iterable item, Map matchState) =>
+ _test(item, matchState) == null;
+
+ @override
+ Description describe(Description description) =>
+ description.add('contains once(').addDescriptionOf(_expected).add(')');
+
+ @override
+ Description describeTypedMismatch(Iterable item,
+ Description mismatchDescription, Map matchState, bool verbose) =>
+ mismatchDescription.add(_test(item, matchState)!);
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index b5a1606..5059e58 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: matcher
-version: 0.12.13
+version: 0.12.14
description: >-
Support for specifying test expectations via an extensible Matcher class.
Also includes a number of built-in Matcher implementations for common cases.
diff --git a/test/iterable_matchers_test.dart b/test/iterable_matchers_test.dart
index 3eb7591..82d2a76 100644
--- a/test/iterable_matchers_test.dart
+++ b/test/iterable_matchers_test.dart
@@ -288,6 +288,37 @@
"Which: not an <Instance of 'Iterable'>");
});
+ test('containsOnce', () {
+ shouldPass([1, 2, 3, 4], containsOnce(2));
+ shouldPass([1, 2, 11, 3], containsOnce(greaterThan(10)));
+ shouldFail(
+ [1, 2, 3, 4],
+ containsOnce(10),
+ 'Expected: contains once(<10>) '
+ 'Actual: [1, 2, 3, 4] '
+ 'Which: did not find a value matching <10>');
+ shouldFail(
+ [1, 2, 3, 4],
+ containsOnce(greaterThan(10)),
+ 'Expected: contains once(a value greater than <10>) '
+ 'Actual: [1, 2, 3, 4] '
+ 'Which: did not find a value matching a value greater than <10>');
+ shouldFail(
+ [1, 2, 1, 2],
+ containsOnce(2),
+ 'Expected: contains once(<2>) '
+ 'Actual: [1, 2, 1, 2] '
+ 'Which: expected only one value matching <2> '
+ 'but found multiple: <2>, <2>');
+ shouldFail(
+ [1, 2, 10, 20],
+ containsOnce(greaterThan(5)),
+ 'Expected: contains once(a value greater than <5>) '
+ 'Actual: [1, 2, 10, 20] '
+ 'Which: expected only one value matching a value greater than <5> '
+ 'but found multiple: <10>, <20>');
+ });
+
test('pairwise compare', () {
var c = [1, 2];
var d = [1, 2, 3];