// 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; } }