| // Copyright 2016 The Chromium Authors. 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:flutter/material.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'finders.dart'; |
| |
| /// Asserts that the [Finder] matches no widgets in the widget tree. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// expect(find.text('Save'), findsNothing); |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [findsWidgets], when you want the finder to find one or more widgets. |
| /// * [findsOneWidget], when you want the finder to find exactly one widget. |
| /// * [findsNWidgets], when you want the finder to find a specific number of widgets. |
| const Matcher findsNothing = const _FindsWidgetMatcher(null, 0); |
| |
| /// Asserts that the [Finder] locates at least one widget in the widget tree. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// expect(find.text('Save'), findsWidgets); |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [findsNothing], when you want the finder to not find anything. |
| /// * [findsOneWidget], when you want the finder to find exactly one widget. |
| /// * [findsNWidgets], when you want the finder to find a specific number of widgets. |
| const Matcher findsWidgets = const _FindsWidgetMatcher(1, null); |
| |
| /// Asserts that the [Finder] locates at exactly one widget in the widget tree. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// expect(find.text('Save'), findsOneWidget); |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [findsNothing], when you want the finder to not find anything. |
| /// * [findsWidgets], when you want the finder to find one or more widgets. |
| /// * [findsNWidgets], when you want the finder to find a specific number of widgets. |
| const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1); |
| |
| /// Asserts that the [Finder] locates the specified number of widgets in the widget tree. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// expect(find.text('Save'), findsNWidgets(2)); |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [findsNothing], when you want the finder to not find anything. |
| /// * [findsWidgets], when you want the finder to find one or more widgets. |
| /// * [findsOneWidget], when you want the finder to find exactly one widget. |
| Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n); |
| |
| /// Asserts that the [Finder] locates the a single widget that has at |
| /// least one [Offstage] widget ancestor. |
| /// |
| /// It's important to use a full finder, since by default finders exclude |
| /// offstage widgets. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// expect(find.text('Save', skipOffstage: false), isOffstage); |
| /// ``` |
| /// |
| /// See also: |
| /// |
| /// * [isOnstage], the opposite. |
| const Matcher isOffstage = const _IsOffstage(); |
| |
| /// Asserts that the [Finder] locates the a single widget that has no |
| /// [Offstage] widget ancestors. |
| /// |
| /// See also: |
| /// |
| /// * [isOffstage], the opposite. |
| const Matcher isOnstage = const _IsOnstage(); |
| |
| /// Asserts that the [Finder] locates the a single widget that has at |
| /// least one [Card] widget ancestor. |
| /// |
| /// See also: |
| /// |
| /// * [isNotInCard], the opposite. |
| const Matcher isInCard = const _IsInCard(); |
| |
| /// Asserts that the [Finder] locates the a single widget that has no |
| /// [Card] widget ancestors. |
| /// |
| /// This is equivalent to `isNot(isInCard)`. |
| /// |
| /// See also: |
| /// |
| /// * [isInCard], the opposite. |
| const Matcher isNotInCard = const _IsNotInCard(); |
| |
| /// Asserts that an object's toString() is a plausible one-line description. |
| /// |
| /// Specifically, this matcher checks that the string does not contains newline |
| /// characters, and does not have leading or trailing whitespace, is not |
| /// empty, and does not contain the default `Instance of ...` string. |
| const Matcher hasOneLineDescription = const _HasOneLineDescription(); |
| |
| /// Asserts that an object's toStringDeep() is a plausible multi-line |
| /// description. |
| /// |
| /// Specifically, this matcher checks that an object's |
| /// `toStringDeep(prefixLineOne, prefixOtherLines)`: |
| /// |
| /// * Does not have leading or trailing whitespace. |
| /// * Does not contain the default `Instance of ...` string. |
| /// * The last line has characters other than tree connector characters and |
| /// whitespace. For example: the line ` │ ║ ╎` has only tree connector |
| /// characters and whitespace. |
| /// * Does not contain lines with trailing white space. |
| /// * Has multiple lines. |
| /// * The first line starts with `prefixLineOne` |
| /// * All subsequent lines start with `prefixOtherLines`. |
| const Matcher hasAGoodToStringDeep = const _HasGoodToStringDeep(); |
| |
| /// A matcher for functions that throw [FlutterError]. |
| /// |
| /// This is equivalent to `throwsA(const isInstanceOf<FlutterError>())`. |
| /// |
| /// See also: |
| /// |
| /// * [throwsAssertionError], to test if a function throws any [AssertionError]. |
| /// * [isFlutterError], to test if any object is a [FlutterError]. |
| /// * [isAssertionError], to test if any object is any kind of [AssertionError]. |
| Matcher throwsFlutterError = throwsA(isFlutterError); |
| |
| /// A matcher for functions that throw [AssertionError]. |
| /// |
| /// This is equivalent to `throwsA(const isInstanceOf<AssertionError>())`. |
| /// |
| /// See also: |
| /// |
| /// * [throwsFlutterError], to test if a function throws a [FlutterError]. |
| /// * [isFlutterError], to test if any object is a [FlutterError]. |
| /// * [isAssertionError], to test if any object is any kind of [AssertionError]. |
| Matcher throwsAssertionError = throwsA(isAssertionError); |
| |
| /// A matcher for [FlutterError]. |
| /// |
| /// This is equivalent to `const isInstanceOf<FlutterError>()`. |
| /// |
| /// See also: |
| /// |
| /// * [throwsFlutterError], to test if a function throws a [FlutterError]. |
| /// * [throwsAssertionError], to test if a function throws any [AssertionError]. |
| /// * [isAssertionError], to test if any object is any kind of [AssertionError]. |
| const Matcher isFlutterError = const isInstanceOf<FlutterError>(); |
| |
| /// A matcher for [AssertionError]. |
| /// |
| /// This is equivalent to `const isInstanceOf<AssertionError>()`. |
| /// |
| /// See also: |
| /// |
| /// * [throwsFlutterError], to test if a function throws a [FlutterError]. |
| /// * [throwsAssertionError], to test if a function throws any [AssertionError]. |
| /// * [isFlutterError], to test if any object is a [FlutterError]. |
| const Matcher isAssertionError = const isInstanceOf<AssertionError>(); |
| |
| /// Asserts that two [double]s are equal, within some tolerated error. |
| /// |
| /// Two values are considered equal if the difference between them is within |
| /// 1e-10 of the larger one. This is an arbitrary value which can be adjusted |
| /// using the `epsilon` argument. This matcher is intended to compare floating |
| /// point numbers that are the result of different sequences of operations, such |
| /// that they may have accumulated slightly different errors. |
| /// |
| /// See also: |
| /// |
| /// * [closeTo], which is identical except that the epsilon argument is |
| /// required and not named. |
| /// * [inInclusiveRange], which matches if the argument is in a specified |
| /// range. |
| Matcher moreOrLessEquals(double value, { double epsilon: 1e-10 }) { |
| return new _MoreOrLessEquals(value, epsilon); |
| } |
| |
| /// Asserts that two [String]s are equal after normalizing likely hash codes. |
| /// |
| /// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code |
| /// and is normalized to #00000. |
| /// |
| /// See Also: |
| /// |
| /// * [describeIdentity], a method that generates short descriptions of objects |
| /// with ids that match the pattern #[0-9a-f]{5}. |
| /// * [shortHash], a method that generates a 5 character long hexadecimal |
| /// [String] based on [Object.hashCode]. |
| /// * [TreeDiagnosticsMixin.toStringDeep], a method that returns a [String] |
| /// typically containing multiple hash codes. |
| Matcher equalsIgnoringHashCodes(String value) { |
| return new _EqualsIgnoringHashCodes(value); |
| } |
| |
| class _FindsWidgetMatcher extends Matcher { |
| const _FindsWidgetMatcher(this.min, this.max); |
| |
| final int min; |
| final int max; |
| |
| @override |
| bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { |
| assert(min != null || max != null); |
| assert(min == null || max == null || min <= max); |
| matchState[Finder] = finder; |
| int count = 0; |
| final Iterator<Element> iterator = finder.evaluate().iterator; |
| if (min != null) { |
| while (count < min && iterator.moveNext()) |
| count += 1; |
| if (count < min) |
| return false; |
| } |
| if (max != null) { |
| while (count <= max && iterator.moveNext()) |
| count += 1; |
| if (count > max) |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Description describe(Description description) { |
| assert(min != null || max != null); |
| if (min == max) { |
| if (min == 1) |
| return description.add('exactly one matching node in the widget tree'); |
| return description.add('exactly $min matching nodes in the widget tree'); |
| } |
| if (min == null) { |
| if (max == 0) |
| return description.add('no matching nodes in the widget tree'); |
| if (max == 1) |
| return description.add('at most one matching node in the widget tree'); |
| return description.add('at most $max matching nodes in the widget tree'); |
| } |
| if (max == null) { |
| if (min == 1) |
| return description.add('at least one matching node in the widget tree'); |
| return description.add('at least $min matching nodes in the widget tree'); |
| } |
| return description.add('between $min and $max matching nodes in the widget tree (inclusive)'); |
| } |
| |
| @override |
| Description describeMismatch( |
| dynamic item, |
| Description mismatchDescription, |
| Map<dynamic, dynamic> matchState, |
| bool verbose |
| ) { |
| final Finder finder = matchState[Finder]; |
| final int count = finder.evaluate().length; |
| if (count == 0) { |
| assert(min != null && min > 0); |
| if (min == 1 && max == 1) |
| return mismatchDescription.add('means none were found but one was expected'); |
| return mismatchDescription.add('means none were found but some were expected'); |
| } |
| if (max == 0) { |
| if (count == 1) |
| return mismatchDescription.add('means one was found but none were expected'); |
| return mismatchDescription.add('means some were found but none were expected'); |
| } |
| if (min != null && count < min) |
| return mismatchDescription.add('is not enough'); |
| assert(max != null && count > min); |
| return mismatchDescription.add('is too many'); |
| } |
| } |
| |
| bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) { |
| final Iterable<Element> nodes = finder.evaluate(); |
| if (nodes.length != 1) |
| return false; |
| bool result = false; |
| nodes.single.visitAncestorElements((Element ancestor) { |
| if (predicate(ancestor.widget)) { |
| result = true; |
| return false; |
| } |
| return true; |
| }); |
| return result; |
| } |
| |
| bool _hasAncestorOfType(Finder finder, Type targetType) { |
| return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType); |
| } |
| |
| class _IsOffstage extends Matcher { |
| const _IsOffstage(); |
| |
| @override |
| bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { |
| return _hasAncestorMatching(finder, (Widget widget) { |
| if (widget is Offstage) |
| return widget.offstage; |
| return false; |
| }); |
| } |
| |
| @override |
| Description describe(Description description) => description.add('offstage'); |
| } |
| |
| class _IsOnstage extends Matcher { |
| const _IsOnstage(); |
| |
| @override |
| bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { |
| final Iterable<Element> nodes = finder.evaluate(); |
| if (nodes.length != 1) |
| return false; |
| bool result = true; |
| nodes.single.visitAncestorElements((Element ancestor) { |
| final Widget widget = ancestor.widget; |
| if (widget is Offstage) { |
| result = !widget.offstage; |
| return false; |
| } |
| return true; |
| }); |
| return result; |
| } |
| |
| @override |
| Description describe(Description description) => description.add('onstage'); |
| } |
| |
| class _IsInCard extends Matcher { |
| const _IsInCard(); |
| |
| @override |
| bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card); |
| |
| @override |
| Description describe(Description description) => description.add('in card'); |
| } |
| |
| class _IsNotInCard extends Matcher { |
| const _IsNotInCard(); |
| |
| @override |
| bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card); |
| |
| @override |
| Description describe(Description description) => description.add('not in card'); |
| } |
| |
| class _HasOneLineDescription extends Matcher { |
| const _HasOneLineDescription(); |
| |
| @override |
| bool matches(Object object, Map<dynamic, dynamic> matchState) { |
| final String description = object.toString(); |
| return description.isNotEmpty |
| && !description.contains('\n') |
| && !description.contains('Instance of ') |
| && description.trim() == description; |
| } |
| |
| @override |
| Description describe(Description description) => description.add('one line description'); |
| } |
| |
| class _EqualsIgnoringHashCodes extends Matcher { |
| _EqualsIgnoringHashCodes(String v) : _value = _normalize(v); |
| |
| final String _value; |
| |
| static final Object _mismatchedValueKey = new Object(); |
| |
| static String _normalize(String s) { |
| return s.replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#00000'); |
| } |
| |
| @override |
| bool matches(dynamic object, Map<dynamic, dynamic> matchState) { |
| final String description = _normalize(object); |
| if (_value != description) { |
| matchState[_mismatchedValueKey] = description; |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Description describe(Description description) { |
| return description.add('multi line description equals $_value'); |
| } |
| |
| @override |
| Description describeMismatch( |
| dynamic item, |
| Description mismatchDescription, |
| Map<dynamic, dynamic> matchState, |
| bool verbose |
| ) { |
| if (matchState.containsKey(_mismatchedValueKey)) { |
| final String actualValue = matchState[_mismatchedValueKey]; |
| // Leading whitespace is added so that lines in the multi-line |
| // description returned by addDescriptionOf are all indented equally |
| // which makes the output easier to read for this case. |
| return mismatchDescription |
| .add('expected normalized value\n ') |
| .addDescriptionOf(_value) |
| .add('\nbut got\n ') |
| .addDescriptionOf(actualValue); |
| } |
| return mismatchDescription; |
| } |
| } |
| |
| /// Returns `true` if [c] represents a whitespace code unit. |
| bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020; |
| |
| /// Returns `true` if [c] represents a vertical line unicode line art code unit. |
| /// |
| /// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only |
| /// specifies vertical line art code units currently used by Flutter line art. |
| /// There are other line art characters that technically also represent vertical |
| /// lines. |
| bool _isVerticalLine(int c) { |
| return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e; |
| } |
| |
| /// Returns whether a [line] is all vertical tree connector characters. |
| /// |
| /// Example vertical tree connector characters: `│ ║ ╎`. |
| /// The last line of a text tree contains only vertical tree connector |
| /// characters indicates a poorly formatted tree. |
| bool _isAllTreeConnectorCharacters(String line) { |
| for (int i = 0; i < line.length; ++i) { |
| final int c = line.codeUnitAt(i); |
| if (!_isWhitespace(c) && !_isVerticalLine(c)) |
| return false; |
| } |
| return true; |
| } |
| |
| class _HasGoodToStringDeep extends Matcher { |
| const _HasGoodToStringDeep(); |
| |
| static final Object _toStringDeepErrorDescriptionKey = new Object(); |
| |
| @override |
| bool matches(dynamic object, Map<dynamic, dynamic> matchState) { |
| final List<String> issues = <String>[]; |
| String description = object.toStringDeep(); |
| if (description.endsWith('\n')) { |
| // Trim off trailing \n as the remaining calculations assume |
| // the description does not end with a trailing \n. |
| description = description.substring(0, description.length - 1); |
| } else { |
| issues.add('Not terminated with a line break.'); |
| } |
| |
| if (description.trim() != description) |
| issues.add('Has trailing whitespace.'); |
| |
| final List<String> lines = description.split('\n'); |
| if (lines.length < 2) |
| issues.add('Does not have multiple lines.'); |
| |
| if (description.contains('Instance of ')) |
| issues.add('Contains text "Instance of ".'); |
| |
| for (int i = 0; i < lines.length; ++i) { |
| final String line = lines[i]; |
| if (line.isEmpty) |
| issues.add('Line ${i+1} is empty.'); |
| |
| if (line.trimRight() != line) |
| issues.add('Line ${i+1} has trailing whitespace.'); |
| } |
| |
| if (_isAllTreeConnectorCharacters(lines.last)) |
| issues.add('Last line is all tree connector characters.'); |
| |
| // If a toStringDeep method doesn't properly handle nested values that |
| // contain line breaks it can fail to add the required prefixes to all |
| // lined when toStringDeep is called specifying prefixes. |
| final String prefixLineOne = 'PREFIX_LINE_ONE____'; |
| final String prefixOtherLines = 'PREFIX_OTHER_LINES_'; |
| final List<String> prefixIssues = <String>[]; |
| String descriptionWithPrefixes = |
| object.toStringDeep(prefixLineOne, prefixOtherLines); |
| if (descriptionWithPrefixes.endsWith('\n')) { |
| // Trim off trailing \n as the remaining calculations assume |
| // the description does not end with a trailing \n. |
| descriptionWithPrefixes = descriptionWithPrefixes.substring( |
| 0, descriptionWithPrefixes.length - 1); |
| } |
| final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n'); |
| if (!linesWithPrefixes.first.startsWith(prefixLineOne)) |
| prefixIssues.add('First line does not contain expected prefix.'); |
| |
| for (int i = 1; i < linesWithPrefixes.length; ++i) { |
| if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) |
| prefixIssues.add('Line ${i+1} does not contain the expected prefix.'); |
| } |
| |
| final StringBuffer errorDescription = new StringBuffer(); |
| if (issues.isNotEmpty) { |
| errorDescription.writeln('Bad toStringDeep():'); |
| errorDescription.writeln(description); |
| errorDescription.writeAll(issues, '\n'); |
| } |
| |
| if (prefixIssues.isNotEmpty) { |
| errorDescription.writeln( |
| 'Bad toStringDeep("$prefixLineOne", "$prefixOtherLines"):'); |
| errorDescription.writeln(descriptionWithPrefixes); |
| errorDescription.writeAll(prefixIssues, '\n'); |
| } |
| |
| if (errorDescription.isNotEmpty) { |
| matchState[_toStringDeepErrorDescriptionKey] = |
| errorDescription.toString(); |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Description describeMismatch( |
| dynamic item, |
| Description mismatchDescription, |
| Map<dynamic, dynamic> matchState, |
| bool verbose |
| ) { |
| if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) { |
| return mismatchDescription.add( |
| matchState[_toStringDeepErrorDescriptionKey]); |
| } |
| return mismatchDescription; |
| } |
| |
| @override |
| Description describe(Description description) { |
| return description.add('multi line description'); |
| } |
| } |
| |
| class _MoreOrLessEquals extends Matcher { |
| const _MoreOrLessEquals(this.value, this.epsilon); |
| |
| final double value; |
| final double epsilon; |
| |
| @override |
| bool matches(Object object, Map<dynamic, dynamic> matchState) { |
| if (object is! double) |
| return false; |
| if (object == value) |
| return true; |
| final double test = object; |
| return (test - value).abs() <= epsilon; |
| } |
| |
| @override |
| Description describe(Description description) => description.add('$value (±$epsilon)'); |
| } |