| // 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. | |
| import 'package:stack_trace/stack_trace.dart'; | |
| import 'description.dart'; | |
| import 'interfaces.dart'; | |
| import 'util.dart'; | |
| /// A base class for [Matcher] instances that match based on some feature of the | |
| /// value under test. | |
| /// | |
| /// Derived classes should call the base constructor with a feature name and | |
| /// description, and an instance matcher, and should implement the | |
| /// [featureValueOf] abstract method. | |
| /// | |
| /// The feature description will typically describe the item and the feature, | |
| /// while the feature name will just name the feature. For example, we may | |
| /// have a Widget class where each Widget has a price; we could make a | |
| /// [CustomMatcher] that can make assertions about prices with: | |
| /// | |
| /// ```dart | |
| /// class HasPrice extends CustomMatcher { | |
| /// HasPrice(matcher) : super("Widget with price that is", "price", matcher); | |
| /// featureValueOf(actual) => (actual as Widget).price; | |
| /// } | |
| /// ``` | |
| /// | |
| /// and then use this for example like: | |
| /// | |
| /// ```dart | |
| /// expect(inventoryItem, HasPrice(greaterThan(0))); | |
| /// ``` | |
| class CustomMatcher extends Matcher { | |
| final String _featureDescription; | |
| final String _featureName; | |
| final Matcher _matcher; | |
| CustomMatcher( | |
| this._featureDescription, | |
| this._featureName, | |
| Object? valueOrMatcher, | |
| ) : _matcher = wrapMatcher(valueOrMatcher); | |
| /// Override this to extract the interesting feature. | |
| Object? featureValueOf(dynamic actual) => actual; | |
| @override | |
| bool matches(Object? item, Map matchState) { | |
| try { | |
| var f = featureValueOf(item); | |
| if (_matcher.matches(f, matchState)) return true; | |
| addStateInfo(matchState, {'custom.feature': f}); | |
| } catch (exception, stack) { | |
| addStateInfo(matchState, { | |
| 'custom.exception': exception.toString(), | |
| 'custom.stack': Chain.forTrace(stack) | |
| .foldFrames( | |
| (frame) => | |
| frame.package == 'test' || | |
| frame.package == 'stream_channel' || | |
| frame.package == 'matcher', | |
| terse: true, | |
| ) | |
| .toString(), | |
| }); | |
| } | |
| return false; | |
| } | |
| @override | |
| Description describe(Description description) => | |
| description.add(_featureDescription).add(' ').addDescriptionOf(_matcher); | |
| @override | |
| Description describeMismatch( | |
| Object? item, | |
| Description mismatchDescription, | |
| Map matchState, | |
| bool verbose, | |
| ) { | |
| if (matchState['custom.exception'] != null) { | |
| mismatchDescription | |
| .add('threw ') | |
| .addDescriptionOf(matchState['custom.exception']) | |
| .add('\n') | |
| .add(matchState['custom.stack'].toString()); | |
| return mismatchDescription; | |
| } | |
| mismatchDescription | |
| .add('has ') | |
| .add(_featureName) | |
| .add(' with value ') | |
| .addDescriptionOf(matchState['custom.feature']); | |
| var innerDescription = StringDescription(); | |
| _matcher.describeMismatch( | |
| matchState['custom.feature'], | |
| innerDescription, | |
| matchState['state'] as Map, | |
| verbose, | |
| ); | |
| if (innerDescription.length > 0) { | |
| mismatchDescription.add(' which ').add(innerDescription.toString()); | |
| } | |
| return mismatchDescription; | |
| } | |
| } |