blob: 2c83029cc59bbb1897ae410931539d9a70a5fc23 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'dart:ui';
import 'package:flutter/material.dart' show Tooltip;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'binding.dart';
import 'tree_traversal.dart';
/// Signature for [CommonFinders.byWidgetPredicate].
typedef WidgetPredicate = bool Function(Widget widget);
/// Signature for [CommonFinders.byElementPredicate].
typedef ElementPredicate = bool Function(Element element);
/// Signature for [CommonSemanticsFinders.byPredicate].
typedef SemanticsNodePredicate = bool Function(SemanticsNode node);
/// Signature for [FinderBase.describeMatch].
typedef DescribeMatchCallback = String Function(Plurality plurality);
/// The `CandidateType` of finders that search for and filter substrings,
/// within static text rendered by [RenderParagraph]s.
final class TextRangeContext {
const TextRangeContext._(this.view, this.renderObject, this.textRange);
/// The [View] containing the static text.
///
/// This is used for hit-testing.
final View view;
/// The RenderObject that contains the static text.
final RenderParagraph renderObject;
/// The [TextRange] of the substring within [renderObject]'s text.
final TextRange textRange;
@override
String toString() => 'TextRangeContext($view, $renderObject, $textRange)';
}
/// Some frequently used [Finder]s and [SemanticsFinder]s.
const CommonFinders find = CommonFinders._();
// Examples can assume:
// typedef Button = Placeholder;
// late WidgetTester tester;
// late String filePath;
// late Key backKey;
/// Provides lightweight syntax for getting frequently used [Finder]s and
/// [SemanticsFinder]s through [semantics].
///
/// This class is instantiated once, as [find].
class CommonFinders {
const CommonFinders._();
/// Some frequently used semantics finders.
CommonSemanticsFinders get semantics => const CommonSemanticsFinders._();
/// Some frequently used text range finders.
CommonTextRangeFinders get textRange => const CommonTextRangeFinders._();
/// Finds [Text], [EditableText], and optionally [RichText] widgets
/// containing string equal to the `text` argument.
///
/// If `findRichText` is false, all standalone [RichText] widgets are
/// ignored and `text` is matched with [Text.data] or [Text.textSpan].
/// If `findRichText` is true, [RichText] widgets (and therefore also
/// [Text] and [Text.rich] widgets) are matched by comparing the
/// [InlineSpan.toPlainText] with the given `text`.
///
/// For [EditableText] widgets, the `text` is always compared to the current
/// value of the [EditableText.controller].
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Back'), findsOneWidget);
/// ```
///
/// This will match [Text], [Text.rich], and [EditableText] widgets that
/// contain the "Back" string.
///
/// ```dart
/// expect(find.text('Close', findRichText: true), findsOneWidget);
/// ```
///
/// This will match [Text], [Text.rich], [EditableText], as well as standalone
/// [RichText] widgets that contain the "Close" string.
Finder text(
String text, {
bool findRichText = false,
bool skipOffstage = true,
}) {
return _TextWidgetFinder(
text,
findRichText: findRichText,
skipOffstage: skipOffstage,
);
}
/// Finds [Text] and [EditableText], and optionally [RichText] widgets
/// which contain the given `pattern` argument.
///
/// If `findRichText` is false, all standalone [RichText] widgets are
/// ignored and `pattern` is matched with [Text.data] or [Text.textSpan].
/// If `findRichText` is true, [RichText] widgets (and therefore also
/// [Text] and [Text.rich] widgets) are matched by comparing the
/// [InlineSpan.toPlainText] with the given `pattern`.
///
/// For [EditableText] widgets, the `pattern` is always compared to the current
/// value of the [EditableText.controller].
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
///
/// ## Sample code
///
/// ```dart
/// expect(find.textContaining('Back'), findsOneWidget);
/// expect(find.textContaining(RegExp(r'(\w+)')), findsOneWidget);
/// ```
///
/// This will match [Text], [Text.rich], and [EditableText] widgets that
/// contain the given pattern : 'Back' or RegExp(r'(\w+)').
///
/// ```dart
/// expect(find.textContaining('Close', findRichText: true), findsOneWidget);
/// expect(find.textContaining(RegExp(r'(\w+)'), findRichText: true), findsOneWidget);
/// ```
///
/// This will match [Text], [Text.rich], [EditableText], as well as standalone
/// [RichText] widgets that contain the given pattern : 'Close' or RegExp(r'(\w+)').
Finder textContaining(
Pattern pattern, {
bool findRichText = false,
bool skipOffstage = true,
}) {
return _TextContainingWidgetFinder(
pattern,
findRichText: findRichText,
skipOffstage: skipOffstage
);
}
/// Looks for widgets that contain a [Text] descendant with `text`
/// in it.
///
/// ## Sample code
///
/// ```dart
/// // Suppose there is a button with text 'Update' in it:
/// const Button(
/// child: Text('Update')
/// );
///
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithText(Button, 'Update'));
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder widgetWithText(Type widgetType, String text, { bool skipOffstage = true }) {
return find.ancestor(
of: find.text(text, skipOffstage: skipOffstage),
matching: find.byType(widgetType, skipOffstage: skipOffstage),
);
}
/// Finds [Image] and [FadeInImage] widgets containing `image` equal to the
/// `image` argument.
///
/// ## Sample code
///
/// ```dart
/// expect(find.image(FileImage(File(filePath))), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder image(ImageProvider image, { bool skipOffstage = true }) => _ImageWidgetFinder(image, skipOffstage: skipOffstage);
/// Finds widgets by searching for one with the given `key`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byKey(backKey), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byKey(Key key, { bool skipOffstage = true }) => _KeyWidgetFinder(key, skipOffstage: skipOffstage);
/// Finds widgets by searching for widgets implementing a particular type.
///
/// This matcher accepts subtypes. For example a
/// `bySubtype<StatefulWidget>()` will find any stateful widget.
///
/// ## Sample code
///
/// ```dart
/// expect(find.bySubtype<IconButton>(), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
///
/// See also:
/// * [byType], which does not do subtype tests.
Finder bySubtype<T extends Widget>({ bool skipOffstage = true }) => _SubtypeWidgetFinder<T>(skipOffstage: skipOffstage);
/// Finds widgets by searching for widgets with a particular type.
///
/// This does not do subclass tests, so for example
/// `byType(StatefulWidget)` will never find anything since [StatefulWidget]
/// is an abstract class.
///
/// The `type` argument must be a subclass of [Widget].
///
/// ## Sample code
///
/// ```dart
/// expect(find.byType(IconButton), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
///
/// See also:
/// * [bySubtype], which allows subtype tests.
Finder byType(Type type, { bool skipOffstage = true }) => _TypeWidgetFinder(type, skipOffstage: skipOffstage);
/// Finds [Icon] widgets containing icon data equal to the `icon`
/// argument.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byIcon(Icons.inbox), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byIcon(IconData icon, { bool skipOffstage = true }) => _IconWidgetFinder(icon, skipOffstage: skipOffstage);
/// Looks for widgets that contain an [Icon] descendant displaying [IconData]
/// `icon` in it.
///
/// ## Sample code
///
/// ```dart
/// // Suppose there is a button with icon 'arrow_forward' in it:
/// const Button(
/// child: Icon(Icons.arrow_forward)
/// );
///
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder widgetWithIcon(Type widgetType, IconData icon, { bool skipOffstage = true }) {
return find.ancestor(
of: find.byIcon(icon),
matching: find.byType(widgetType),
);
}
/// Looks for widgets that contain an [Image] descendant displaying
/// [ImageProvider] `image` in it.
///
/// ## Sample code
///
/// ```dart
/// // Suppose there is a button with an image in it:
/// Button(
/// child: Image.file(File(filePath))
/// );
///
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithImage(Button, FileImage(File(filePath))));
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder widgetWithImage(Type widgetType, ImageProvider image, { bool skipOffstage = true }) {
return find.ancestor(
of: find.image(image),
matching: find.byType(widgetType),
);
}
/// Finds widgets by searching for elements with a particular type.
///
/// This does not do subclass tests, so for example
/// `byElementType(VirtualViewportElement)` will never find anything
/// since [RenderObjectElement] is an abstract class.
///
/// The `type` argument must be a subclass of [Element].
///
/// ## Sample code
///
/// ```dart
/// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeWidgetFinder(type, skipOffstage: skipOffstage);
/// Finds widgets whose current widget is the instance given by the `widget`
/// argument.
///
/// ## Sample code
///
/// ```dart
/// // Suppose there is a button created like this:
/// Widget myButton = const Button(
/// child: Text('Update')
/// );
///
/// // It can be found and tapped like this:
/// tester.tap(find.byWidget(myButton));
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byWidget(Widget widget, { bool skipOffstage = true }) => _ExactWidgetFinder(widget, skipOffstage: skipOffstage);
/// Finds widgets using a widget `predicate`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back',
/// description: 'with tooltip "Back"',
/// ), findsOneWidget);
/// ```
///
/// If `description` is provided, then this uses it as the description of the
/// [Finder] and appears, for example, in the error message when the finder
/// fails to locate the desired widget. Otherwise, the description prints the
/// signature of the predicate function.
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byWidgetPredicate(WidgetPredicate predicate, { String? description, bool skipOffstage = true }) {
return _WidgetPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage);
}
/// Finds [Tooltip] widgets with the given `message`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byTooltip('Back'), findsOneWidget);
/// expect(find.byTooltip(RegExp('Back.*')), findsNWidgets(2));
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byTooltip(Pattern message, {bool skipOffstage = true}) {
return byWidgetPredicate(
(Widget widget) {
return widget is Tooltip &&
(message is RegExp
? ((widget.message != null && message.hasMatch(widget.message!)) ||
(widget.richMessage != null && message.hasMatch(widget.richMessage!.toPlainText())))
: ((widget.message ?? widget.richMessage?.toPlainText()) == message));
},
skipOffstage: skipOffstage,
);
}
/// Finds widgets using an element `predicate`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byElementPredicate(
/// // Finds elements of type SingleChildRenderObjectElement, including
/// // those that are actually subclasses of that type.
/// // (contrast with byElementType, which only returns exact matches)
/// (Element element) => element is SingleChildRenderObjectElement,
/// description: '$SingleChildRenderObjectElement element',
/// ), findsOneWidget);
/// ```
///
/// If `description` is provided, then this uses it as the description of the
/// [Finder] and appears, for example, in the error message when the finder
/// fails to locate the desired widget. Otherwise, the description prints the
/// signature of the predicate function.
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementPredicate(ElementPredicate predicate, { String? description, bool skipOffstage = true }) {
return _ElementPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage);
}
/// Finds widgets that are descendants of the `of` parameter and that match
/// the `matching` parameter.
///
/// ## Sample code
///
/// ```dart
/// expect(find.descendant(
/// of: find.widgetWithText(Row, 'label_1'),
/// matching: find.text('value_1'),
/// ), findsOneWidget);
/// ```
///
/// If the `matchRoot` argument is true then the widget(s) specified by `of`
/// will be matched along with the descendants.
///
/// If the `skipOffstage` argument is true (the default), then nodes that are
/// [Offstage] or that are from inactive [Route]s are skipped.
Finder descendant({
required FinderBase<Element> of,
required FinderBase<Element> matching,
bool matchRoot = false,
bool skipOffstage = true,
}) {
return _DescendantWidgetFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
}
/// Finds widgets that are ancestors of the `of` parameter and that match
/// the `matching` parameter.
///
/// ## Sample code
///
/// ```dart
/// // Test if a Text widget that contains 'faded' is the
/// // descendant of an Opacity widget with opacity 0.5:
/// expect(
/// tester.widget<Opacity>(
/// find.ancestor(
/// of: find.text('faded'),
/// matching: find.byType(Opacity),
/// )
/// ).opacity,
/// 0.5
/// );
/// ```
///
/// If the `matchRoot` argument is true then the widget(s) specified by `of`
/// will be matched along with the ancestors.
Finder ancestor({
required FinderBase<Element> of,
required FinderBase<Element> matching,
bool matchRoot = false,
}) {
return _AncestorWidgetFinder(of, matching, matchLeaves: matchRoot);
}
/// Finds [Semantics] widgets matching the given `label`, either by
/// [RegExp.hasMatch] or string equality.
///
/// The framework may combine semantics labels in certain scenarios, such as
/// when multiple [Text] widgets are in a [MaterialButton] widget. In such a
/// case, it may be preferable to match by regular expression. Consumers of
/// this API __must not__ introduce unsuitable content into the semantics tree
/// for the purposes of testing; in particular, you should prefer matching by
/// regular expression rather than by string if the framework has combined
/// your semantics, and not try to force the framework to break up the
/// semantics nodes. Breaking up the nodes would have an undesirable effect on
/// screen readers and other accessibility services.
///
/// ## Sample code
///
/// ```dart
/// expect(find.bySemanticsLabel('Back'), findsOneWidget);
/// ```
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) {
if (!SemanticsBinding.instance.semanticsEnabled) {
throw StateError('Semantics are not enabled. '
'Make sure to call tester.ensureSemantics() before using '
'this finder, and call dispose on its return value after.');
}
return byElementPredicate(
(Element element) {
// Multiple elements can have the same renderObject - we want the "owner"
// of the renderObject, i.e. the RenderObjectElement.
if (element is! RenderObjectElement) {
return false;
}
final String? semanticsLabel = element.renderObject.debugSemantics?.label;
if (semanticsLabel == null) {
return false;
}
return label is RegExp
? label.hasMatch(semanticsLabel)
: label == semanticsLabel;
},
skipOffstage: skipOffstage,
);
}
}
/// Provides lightweight syntax for getting frequently used semantics finders.
///
/// This class is instantiated once, as [CommonFinders.semantics], under [find].
class CommonSemanticsFinders {
const CommonSemanticsFinders._();
/// Finds an ancestor of `of` that matches `matching`.
///
/// If `matchRoot` is true, then the results of `of` are included in the
/// search and results.
FinderBase<SemanticsNode> ancestor({
required FinderBase<SemanticsNode> of,
required FinderBase<SemanticsNode> matching,
bool matchRoot = false,
}) {
return _AncestorSemanticsFinder(of, matching, matchRoot);
}
/// Finds a descendant of `of` that matches `matching`.
///
/// If `matchRoot` is true, then the results of `of` are included in the
/// search and results.
FinderBase<SemanticsNode> descendant({
required FinderBase<SemanticsNode> of,
required FinderBase<SemanticsNode> matching,
bool matchRoot = false,
}) {
return _DescendantSemanticsFinder(of, matching, matchRoot: matchRoot);
}
/// Finds any [SemanticsNode]s matching the given `predicate`.
///
/// If `describeMatch` is provided, it will be used to describe the
/// [FinderBase] and [FinderResult]s.
/// {@macro flutter_test.finders.FinderBase.describeMatch}
///
/// {@template flutter_test.finders.CommonSemanticsFinders.viewParameter}
/// The `view` provided will be used to determine the semantics tree where
/// the search will be evaluated. If not provided, the search will be
/// evaluated against the semantics tree of [WidgetTester.view].
/// {@endtemplate}
SemanticsFinder byPredicate(
SemanticsNodePredicate predicate, {
DescribeMatchCallback? describeMatch,
FlutterView? view,
}) {
return _PredicateSemanticsFinder(
predicate,
describeMatch,
view,
);
}
/// Finds any [SemanticsNode]s that has a [SemanticsNode.label] that matches
/// the given `label`.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byLabel(Pattern label, {FlutterView? view}) {
return byPredicate(
(SemanticsNode node) => _matchesPattern(node.label, label),
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with label "$label"',
view: view,
);
}
/// Finds any [SemanticsNode]s that has a [SemanticsNode.value] that matches
/// the given `value`.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byValue(Pattern value, {FlutterView? view}) {
return byPredicate(
(SemanticsNode node) => _matchesPattern(node.value, value),
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with value "$value"',
view: view,
);
}
/// Finds any [SemanticsNode]s that has a [SemanticsNode.hint] that matches
/// the given `hint`.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byHint(Pattern hint, {FlutterView? view}) {
return byPredicate(
(SemanticsNode node) => _matchesPattern(node.hint, hint),
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with hint "$hint"',
view: view,
);
}
/// Finds any [SemanticsNode]s that has the given [SemanticsAction].
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byAction(SemanticsAction action, {FlutterView? view}) {
return byPredicate(
(SemanticsNode node) => node.getSemanticsData().hasAction(action),
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with action "$action"',
view: view,
);
}
/// Finds any [SemanticsNode]s that has at least one of the given
/// [SemanticsAction]s.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byAnyAction(List<SemanticsAction> actions, {FlutterView? view}) {
final int actionsInt = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
return byPredicate(
(SemanticsNode node) => node.getSemanticsData().actions & actionsInt != 0,
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with any of the following actions: $actions',
view: view,
);
}
/// Finds any [SemanticsNode]s that has the given [SemanticsFlag].
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byFlag(SemanticsFlag flag, {FlutterView? view}) {
return byPredicate(
(SemanticsNode node) => node.hasFlag(flag),
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with flag "$flag"',
view: view,
);
}
/// Finds any [SemanticsNode]s that has at least one of the given
/// [SemanticsFlag]s.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder byAnyFlag(List<SemanticsFlag> flags, {FlutterView? view}) {
final int flagsInt = flags.fold(0, (int value, SemanticsFlag flag) => value | flag.index);
return byPredicate(
(SemanticsNode node) => node.getSemanticsData().flags & flagsInt != 0,
describeMatch: (Plurality plurality) => '${switch (plurality) {
Plurality.one => 'SemanticsNode',
Plurality.zero || Plurality.many => 'SemanticsNodes',
}} with any of the following flags: $flags',
view: view,
);
}
/// Finds any [SemanticsNode]s that can scroll in at least one direction.
///
/// If `axis` is provided, then the search will be limited to scrollable nodes
/// that can scroll in the given axis. If `axis` is not provided, then both
/// horizontal and vertical scrollable nodes will be found.
///
/// {@macro flutter_test.finders.CommonSemanticsFinders.viewParameter}
SemanticsFinder scrollable({Axis? axis, FlutterView? view}) {
return byAnyAction(<SemanticsAction>[
if (axis == null || axis == Axis.vertical) ...<SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
if (axis == null || axis == Axis.horizontal) ...<SemanticsAction>[
SemanticsAction.scrollLeft,
SemanticsAction.scrollRight,
],
]);
}
bool _matchesPattern(String target, Pattern pattern) {
if (pattern is RegExp) {
return pattern.hasMatch(target);
} else {
return pattern == target;
}
}
}
/// Provides lightweight syntax for getting frequently used text range finders.
///
/// This class is instantiated once, as [CommonFinders.textRange], under [find].
final class CommonTextRangeFinders {
const CommonTextRangeFinders._();
/// Finds all non-overlapping occurrences of the given `substring` in the
/// static text widgets and returns the [TextRange]s.
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// static text inside widgets that are [Offstage], or that are from inactive
/// [Route]s.
///
/// If the `descendentOf` argument is non-null, this method only searches in
/// the descendants of that parameter for the given substring.
///
/// This finder uses the [Pattern.allMatches] method to match the substring in
/// the text. After finding a matching substring in the text, the method
/// continues the search from the end of the match, thus skipping overlapping
/// occurrences of the substring.
FinderBase<TextRangeContext> ofSubstring(String substring, { bool skipOffstage = true, FinderBase<Element>? descendentOf }) {
final _TextContainingWidgetFinder textWidgetFinder = _TextContainingWidgetFinder(substring, skipOffstage: skipOffstage, findRichText: true);
final Finder elementFinder = descendentOf == null
? textWidgetFinder
: _DescendantWidgetFinder(descendentOf, textWidgetFinder, matchRoot: true, skipOffstage: skipOffstage);
return _StaticTextRangeFinder(elementFinder, substring);
}
}
/// Describes how a string of text should be pluralized.
enum Plurality {
/// Text should be pluralized to describe zero items.
zero,
/// Text should be pluralized to describe a single item.
one,
/// Text should be pluralized to describe more than one item.
many;
static Plurality _fromNum(num source) {
assert(source >= 0, 'A Plurality can only be created with a positive number.');
return switch (source) {
0 => Plurality.zero,
1 => Plurality.one,
_ => Plurality.many,
};
}
}
/// Encapsulates the logic for searching a list of candidates and filtering the
/// candidates to only those that meet the requirements defined by the finder.
///
/// Implementations will need to implement [allCandidates] to define the total
/// possible search space and [findInCandidates] to define the requirements of
/// the finder.
///
/// This library contains [Finder] and [SemanticsFinder] for searching
/// Flutter's element and semantics trees respectively.
///
/// If the search can be represented as a predicate, then consider using
/// [MatchFinderMixin] along with the [Finder] or [SemanticsFinder] base class.
///
/// If the search further filters the results from another finder, consider using
/// [ChainedFinderMixin] along with the [Finder] or [SemanticsFinder] base class.
abstract class FinderBase<CandidateType> {
bool _cached = false;
/// The results of the latest [evaluate] or [tryEvaluate] call.
///
/// Unlike [evaluate] and [tryEvaluate], [found] will not re-execute the
/// search for this finder. Either [evaluate] or [tryEvaluate] must be called
/// before accessing [found].
FinderResult<CandidateType> get found {
assert(
_found != null,
'No results have been found yet. '
'Either `evaluate` or `tryEvaluate` must be called before accessing `found`',
);
return _found!;
}
FinderResult<CandidateType>? _found;
/// Whether or not this finder has any results in [found].
bool get hasFound => _found != null;
/// Describes zero, one, or more candidates that match the requirements of a
/// finder.
///
/// {@template flutter_test.finders.FinderBase.describeMatch}
/// The description returned should be a brief English phrase describing a
/// matching candidate with the proper plural form. As an example for a string
/// finder that is looking for strings starting with "hello":
///
/// ```dart
/// String describeMatch(Plurality plurality) {
/// return switch (plurality) {
/// Plurality.zero || Plurality.many => 'strings starting with "hello"',
/// Plurality.one => 'string starting with "hello"',
/// };
/// }
/// ```
/// {@endtemplate}
///
/// This will be used both to describe a finder and the results of searching
/// with that finder.
///
/// See also:
///
/// * [FinderBase.toString] where this is used to fully describe the finder
/// * [FinderResult.toString] where this is used to provide context to the
/// results of a search
String describeMatch(Plurality plurality);
/// Returns all of the items that will be considered by this finder.
@protected
Iterable<CandidateType> get allCandidates;
/// Returns a variant of this finder that only matches the first item
/// found by this finder.
FinderBase<CandidateType> get first => _FirstFinder<CandidateType>(this);
/// Returns a variant of this finder that only matches the last item
/// found by this finder.
FinderBase<CandidateType> get last => _LastFinder<CandidateType>(this);
/// Returns a variant of this finder that only matches the item at the
/// given index found by this finder.
FinderBase<CandidateType> at(int index) => _IndexFinder<CandidateType>(this, index);
/// Returns all the items in the given list that match this
/// finder's requirements.
///
/// This is overridden to define the requirements of the finder when
/// implementing finders that directly extend [FinderBase]. If a finder can
/// be efficiently described just in terms of a predicate function, consider
/// mixing in [MatchFinderMixin] and implementing [MatchFinderMixin.matches]
/// instead.
@protected
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates);
/// Searches a set of candidates for those that meet the requirements set by
/// this finder and returns the result of that search.
///
/// See also:
///
/// * [found] which will return the latest results without re-executing the
/// search.
/// * [tryEvaluate] which will indicate whether any results were found rather
/// than directly returning results.
FinderResult<CandidateType> evaluate() {
if (!_cached || _found == null) {
_found = FinderResult<CandidateType>(describeMatch, findInCandidates(allCandidates));
}
return found;
}
/// Searches a set of candidates for those that meet the requirements set by
/// this finder and returns whether the search found any matching candidates.
///
/// This is useful in cases where an action needs to be repeated while or
/// until a finder has results. The results from the search can be accessed
/// using the [found] property without re-executing the search.
///
/// ## Sample code
///
/// ```dart
/// testWidgets('Top text loads first', (WidgetTester tester) async {
/// // Assume a widget is pumped with a top and bottom loading area, with
/// // the texts "Top loaded" and "Bottom loaded" when loading is complete.
/// // await tester.pumpWidget(...)
///
/// // Wait until at least one loaded widget is available
/// Finder loadedFinder = find.textContaining('loaded');
/// while (!loadedFinder.tryEvaluate()) {
/// await tester.pump(const Duration(milliseconds: 100));
/// }
///
/// expect(loadedFinder.found, hasLength(1));
/// expect(tester.widget<Text>(loadedFinder).data, contains('Top'));
/// });
/// ```
bool tryEvaluate() {
evaluate();
return found.isNotEmpty;
}
/// Runs the given callback using cached results.
///
/// While in this callback, this [FinderBase] will cache the results from the
/// next call to [evaluate] or [tryEvaluate] and then no longer evaluate new results
/// until the callback completes. After the first call, all calls to [evaluate],
/// [tryEvaluate] or [found] will return the same results without evaluating.
void runCached(VoidCallback run) {
reset();
_cached = true;
try {
run();
} finally {
reset();
_cached = false;
}
}
/// Resets all state of this [FinderBase].
///
/// Generally used between tests to reset the state of [found] if a finder is
/// used across multiple tests.
void reset() {
_found = null;
}
/// A string representation of this finder or its results.
///
/// By default, this describes the results of the search in order to play
/// nicely with [expect] and its output when a failure occurs. If you wish
/// to get a string representation of the finder itself, pass [describeSelf]
/// as `true`.
@override
String toString({bool describeSelf = false}) {
if (describeSelf) {
return 'A finder that searches for ${describeMatch(Plurality.many)}.';
} else {
if (!hasFound) {
evaluate();
}
return found.toString();
}
}
}
/// The results of searching with a [FinderBase].
class FinderResult<CandidateType> extends Iterable<CandidateType> {
/// Creates a new [FinderResult] that describes the `values` using the given
/// `describeMatch` callback.
///
/// {@macro flutter_test.finders.FinderBase.describeMatch}
FinderResult(DescribeMatchCallback describeMatch, Iterable<CandidateType> values)
: _describeMatch = describeMatch, _values = values;
final DescribeMatchCallback _describeMatch;
final Iterable<CandidateType> _values;
@override
Iterator<CandidateType> get iterator => _values.iterator;
@override
String toString() {
final List<CandidateType> valuesList = _values.toList();
// This will put each value on its own line with a comma and indentation
final String valuesString = valuesList.fold(
'',
(String current, CandidateType candidate) => '$current\n $candidate,',
);
return 'Found ${valuesList.length} ${_describeMatch(Plurality._fromNum(valuesList.length))}: ['
'${valuesString.isNotEmpty ? '$valuesString\n' : ''}'
']';
}
}
/// Provides backwards compatibility with the original [Finder] API.
mixin _LegacyFinderMixin on FinderBase<Element> {
Iterable<Element>? _precacheResults;
/// Describes what the finder is looking for. The description should be
/// a brief English noun phrase describing the finder's requirements.
@Deprecated(
'Use FinderBase.describeMatch instead. '
'FinderBase.describeMatch allows for more readable descriptions and removes ambiguity about pluralization. '
'This feature was deprecated after v3.13.0-0.2.pre.'
)
String get description;
/// Returns all the elements in the given list that match this
/// finder's pattern.
///
/// When implementing Finders that inherit directly from
/// [Finder], [findInCandidates] is the main method to override. This method
/// is maintained for backwards compatibility and will be removed in a future
/// version of Flutter. If the finder can efficiently be described just in
/// terms of a predicate function, consider mixing in [MatchFinderMixin]
/// instead.
@Deprecated(
'Override FinderBase.findInCandidates instead. '
'Using the FinderBase API allows for more consistent caching behavior and cleaner options for interacting with the widget tree. '
'This feature was deprecated after v3.13.0-0.2.pre.'
)
Iterable<Element> apply(Iterable<Element> candidates) {
return findInCandidates(candidates);
}
/// Attempts to evaluate the finder. Returns whether any elements in the tree
/// matched the finder. If any did, then the result is cached and can be obtained
/// from [evaluate].
///
/// If this returns true, you must call [evaluate] before you call [precache] again.
@Deprecated(
'Use FinderBase.tryFind or FinderBase.runCached instead. '
'Using the FinderBase API allows for more consistent caching behavior and cleaner options for interacting with the widget tree. '
'This feature was deprecated after v3.13.0-0.2.pre.'
)
bool precache() {
assert(_precacheResults == null);
if (tryEvaluate()) {
return true;
}
_precacheResults = null;
return false;
}
@override
Iterable<Element> findInCandidates(Iterable<Element> candidates) {
return apply(candidates);
}
}
/// A base class for creating finders that search the [Element] tree for
/// [Widget]s.
///
/// The [findInCandidates] method must be overridden and will be enforced at
/// compilation after [apply] is removed.
abstract class Finder extends FinderBase<Element> with _LegacyFinderMixin {
/// Creates a new [Finder] with the given `skipOffstage` value.
Finder({this.skipOffstage = true});
/// Whether this finder skips nodes that are offstage.
///
/// If this is true, then the elements are walked using
/// [Element.debugVisitOnstageChildren]. This skips offstage children of
/// [Offstage] widgets, as well as children of inactive [Route]s.
final bool skipOffstage;
@override
Finder get first => _FirstWidgetFinder(this);
@override
Finder get last => _LastWidgetFinder(this);
@override
Finder at(int index) => _IndexWidgetFinder(this, index);
@override
Iterable<Element> get allCandidates {
return collectAllElementsFrom(
WidgetsBinding.instance.rootElement!,
skipOffstage: skipOffstage,
);
}
@override
String describeMatch(Plurality plurality) {
return switch (plurality) {
Plurality.zero || Plurality.many => 'widgets with $description',
Plurality.one => 'widget with $description',
};
}
/// Returns a variant of this finder that only matches elements reachable by
/// a hit test.
///
/// The `at` parameter specifies the location relative to the size of the
/// target element where the hit test is performed.
Finder hitTestable({ Alignment at = Alignment.center }) => _HitTestableWidgetFinder(this, at);
}
/// A base class for creating finders that search the semantics tree.
abstract class SemanticsFinder extends FinderBase<SemanticsNode> {
/// Creates a new [SemanticsFinder] that will search within the given [view] or
/// within all views if [view] is null.
SemanticsFinder(this.view);
/// The [FlutterView] whose semantics tree this finder will search.
///
/// If null, the finder will search within all views.
final FlutterView? view;
/// Returns the root [SemanticsNode]s of all the semantics trees that this
/// finder will search.
Iterable<SemanticsNode> get roots {
if (view == null) {
return _allRoots;
}
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
.firstWhere((RenderView r) => r.flutterView == view);
return <SemanticsNode>[
renderView.owner!.semanticsOwner!.rootSemanticsNode!
];
}
@override
Iterable<SemanticsNode> get allCandidates {
return roots.expand((SemanticsNode root) => collectAllSemanticsNodesFrom(root));
}
static Iterable<SemanticsNode> get _allRoots {
final List<SemanticsNode> roots = <SemanticsNode>[];
void collectSemanticsRoots(PipelineOwner owner) {
final SemanticsNode? root = owner.semanticsOwner?.rootSemanticsNode;
if (root != null) {
roots.add(root);
}
owner.visitChildren(collectSemanticsRoots);
}
collectSemanticsRoots(TestWidgetsFlutterBinding.instance.rootPipelineOwner);
return roots;
}
}
/// A base class for creating finders that search for static text rendered by a
/// [RenderParagraph].
class _StaticTextRangeFinder extends FinderBase<TextRangeContext> {
/// Creates a new [_StaticTextRangeFinder] that searches for the given
/// `pattern` in the [Element]s found by `_parent`.
_StaticTextRangeFinder(this._parent, this.pattern);
final FinderBase<Element> _parent;
final Pattern pattern;
Iterable<TextRangeContext> _flatMap(Element from) {
final RenderObject? renderObject = from.renderObject;
// This is currently only exposed on text matchers. Only consider RenderBoxes.
if (renderObject is! RenderBox) {
return const Iterable<TextRangeContext>.empty();
}
final View view = from.findAncestorWidgetOfExactType<View>()!;
final List<RenderParagraph> paragraphs = <RenderParagraph>[];
void visitor(RenderObject child) {
switch (child) {
case RenderParagraph():
paragraphs.add(child);
// No need to continue, we are piggybacking off of a text matcher, so
// inline text widgets will be reported separately.
case RenderBox():
child.visitChildren(visitor);
case _:
}
}
visitor(renderObject);
Iterable<TextRangeContext> searchInParagraph(RenderParagraph paragraph) {
final String text = paragraph.text.toPlainText(includeSemanticsLabels: false);
return pattern.allMatches(text)
.map((Match match) => TextRangeContext._(view, paragraph, TextRange(start: match.start, end: match.end)));
}
return paragraphs.expand(searchInParagraph);
}
@override
Iterable<TextRangeContext> findInCandidates(Iterable<TextRangeContext> candidates) => candidates;
@override
Iterable<TextRangeContext> get allCandidates => _parent.evaluate().expand(_flatMap);
@override
String describeMatch(Plurality plurality) {
return switch (plurality) {
Plurality.zero || Plurality.many => 'non-overlapping TextRanges that match the Pattern "$pattern"',
Plurality.one => 'non-overlapping TextRange that matches the Pattern "$pattern"',
};
}
}
/// A mixin that applies additional filtering to the results of a parent [Finder].
mixin ChainedFinderMixin<CandidateType> on FinderBase<CandidateType> {
/// Another finder whose results will be further filtered.
FinderBase<CandidateType> get parent;
/// Return another [Iterable] when given an [Iterable] of candidates from a
/// parent [FinderBase].
///
/// This is the main method to implement when mixing in [ChainedFinderMixin].
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates);
@override
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) {
return filter(parent.findInCandidates(candidates));
}
@override
Iterable<CandidateType> get allCandidates => parent.allCandidates;
}
/// Applies additional filtering against a [parent] widget finder.
abstract class ChainedFinder extends Finder with ChainedFinderMixin<Element> {
/// Create a Finder chained against the candidates of another `parent` [Finder].
ChainedFinder(this.parent);
@override
final FinderBase<Element> parent;
}
mixin _FirstFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType>{
@override
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} (ignoring all but first)';
}
@override
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* {
yield parentCandidates.first;
}
}
class _FirstFinder<CandidateType> extends FinderBase<CandidateType>
with ChainedFinderMixin<CandidateType>, _FirstFinderMixin<CandidateType> {
_FirstFinder(this.parent);
@override
final FinderBase<CandidateType> parent;
}
class _FirstWidgetFinder extends ChainedFinder with _FirstFinderMixin<Element> {
_FirstWidgetFinder(super.parent);
@override
String get description => describeMatch(Plurality.many);
}
mixin _LastFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType> {
@override
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} (ignoring all but first)';
}
@override
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* {
yield parentCandidates.last;
}
}
class _LastFinder<CandidateType> extends FinderBase<CandidateType>
with ChainedFinderMixin<CandidateType>, _LastFinderMixin<CandidateType>{
_LastFinder(this.parent);
@override
final FinderBase<CandidateType> parent;
}
class _LastWidgetFinder extends ChainedFinder with _LastFinderMixin<Element> {
_LastWidgetFinder(super.parent);
@override
String get description => describeMatch(Plurality.many);
}
mixin _IndexFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType> {
int get index;
@override
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} (ignoring all but index $index)';
}
@override
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* {
yield parentCandidates.elementAt(index);
}
}
class _IndexFinder<CandidateType> extends FinderBase<CandidateType>
with ChainedFinderMixin<CandidateType>, _IndexFinderMixin<CandidateType> {
_IndexFinder(this.parent, this.index);
@override
final int index;
@override
final FinderBase<CandidateType> parent;
}
class _IndexWidgetFinder extends ChainedFinder with _IndexFinderMixin<Element> {
_IndexWidgetFinder(super.parent, this.index);
@override
final int index;
@override
String get description => describeMatch(Plurality.many);
}
class _HitTestableWidgetFinder extends ChainedFinder {
_HitTestableWidgetFinder(super.parent, this.alignment);
final Alignment alignment;
@override
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} (considering only hit-testable ones)';
}
@override
String get description => describeMatch(Plurality.many);
@override
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
for (final Element candidate in parentCandidates) {
final int viewId = candidate.findAncestorWidgetOfExactType<View>()!.view.viewId;
final RenderBox box = candidate.renderObject! as RenderBox;
final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
final HitTestResult hitResult = HitTestResult();
WidgetsBinding.instance.hitTestInView(hitResult, absoluteOffset, viewId);
for (final HitTestEntry entry in hitResult.path) {
if (entry.target == candidate.renderObject) {
yield candidate;
break;
}
}
}
}
}
/// A mixin for creating finders that search candidates for those that match
/// a given pattern.
mixin MatchFinderMixin<CandidateType> on FinderBase<CandidateType> {
/// Returns true if the given element matches the pattern.
///
/// When implementing a MatchFinder, this is the main method to override.
bool matches(CandidateType candidate);
@override
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) {
return candidates.where(matches);
}
}
/// Searches candidates for any that match a particular pattern.
abstract class MatchFinder extends Finder with MatchFinderMixin<Element> {
/// Initializes a predicate-based Finder. Used by subclasses to initialize the
/// `skipOffstage` property.
MatchFinder({ super.skipOffstage });
}
abstract class _MatchTextFinder extends MatchFinder {
_MatchTextFinder({
this.findRichText = false,
super.skipOffstage,
});
/// Whether standalone [RichText] widgets should be found or not.
///
/// Defaults to `false`.
///
/// If disabled, only [Text] widgets will be matched. [RichText] widgets
/// *without* a [Text] ancestor will be ignored.
/// If enabled, only [RichText] widgets will be matched. This *implicitly*
/// matches [Text] widgets as well since they always insert a [RichText]
/// child.
///
/// In either case, [EditableText] widgets will also be matched.
final bool findRichText;
bool matchesText(String textToMatch);
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
if (widget is EditableText) {
return _matchesEditableText(widget);
}
if (!findRichText) {
return _matchesNonRichText(widget);
}
// It would be sufficient to always use _matchesRichText if we wanted to
// match both standalone RichText widgets as well as Text widgets. However,
// the find.text() finder used to always ignore standalone RichText widgets,
// which is why we need the _matchesNonRichText method in order to not be
// backwards-compatible and not break existing tests.
return _matchesRichText(widget);
}
bool _matchesRichText(Widget widget) {
if (widget is RichText) {
return matchesText(widget.text.toPlainText());
}
return false;
}
bool _matchesNonRichText(Widget widget) {
if (widget is Text) {
if (widget.data != null) {
return matchesText(widget.data!);
}
assert(widget.textSpan != null);
return matchesText(widget.textSpan!.toPlainText());
}
return false;
}
bool _matchesEditableText(EditableText widget) {
return matchesText(widget.controller.text);
}
}
class _TextWidgetFinder extends _MatchTextFinder {
_TextWidgetFinder(
this.text, {
super.findRichText,
super.skipOffstage,
});
final String text;
@override
String get description => 'text "$text"';
@override
bool matchesText(String textToMatch) {
return textToMatch == text;
}
}
class _TextContainingWidgetFinder extends _MatchTextFinder {
_TextContainingWidgetFinder(
this.pattern, {
super.findRichText,
super.skipOffstage,
});
final Pattern pattern;
@override
String get description => 'text containing $pattern';
@override
bool matchesText(String textToMatch) {
return textToMatch.contains(pattern);
}
}
class _KeyWidgetFinder extends MatchFinder {
_KeyWidgetFinder(this.key, { super.skipOffstage });
final Key key;
@override
String get description => 'key $key';
@override
bool matches(Element candidate) {
return candidate.widget.key == key;
}
}
class _SubtypeWidgetFinder<T extends Widget> extends MatchFinder {
_SubtypeWidgetFinder({ super.skipOffstage });
@override
String get description => 'is "$T"';
@override
bool matches(Element candidate) {
return candidate.widget is T;
}
}
class _TypeWidgetFinder extends MatchFinder {
_TypeWidgetFinder(this.widgetType, { super.skipOffstage });
final Type widgetType;
@override
String get description => 'type "$widgetType"';
@override
bool matches(Element candidate) {
return candidate.widget.runtimeType == widgetType;
}
}
class _ImageWidgetFinder extends MatchFinder {
_ImageWidgetFinder(this.image, { super.skipOffstage });
final ImageProvider image;
@override
String get description => 'image "$image"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
if (widget is Image) {
return widget.image == image;
} else if (widget is FadeInImage) {
return widget.image == image;
}
return false;
}
}
class _IconWidgetFinder extends MatchFinder {
_IconWidgetFinder(this.icon, { super.skipOffstage });
final IconData icon;
@override
String get description => 'icon "$icon"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is Icon && widget.icon == icon;
}
}
class _ElementTypeWidgetFinder extends MatchFinder {
_ElementTypeWidgetFinder(this.elementType, { super.skipOffstage });
final Type elementType;
@override
String get description => 'type "$elementType"';
@override
bool matches(Element candidate) {
return candidate.runtimeType == elementType;
}
}
class _ExactWidgetFinder extends MatchFinder {
_ExactWidgetFinder(this.widget, { super.skipOffstage });
final Widget widget;
@override
String get description => 'the given widget ($widget)';
@override
bool matches(Element candidate) {
return candidate.widget == widget;
}
}
class _WidgetPredicateWidgetFinder extends MatchFinder {
_WidgetPredicateWidgetFinder(this.predicate, { String? description, super.skipOffstage })
: _description = description;
final WidgetPredicate predicate;
final String? _description;
@override
String get description => _description ?? 'widget matching predicate';
@override
bool matches(Element candidate) {
return predicate(candidate.widget);
}
}
class _ElementPredicateWidgetFinder extends MatchFinder {
_ElementPredicateWidgetFinder(this.predicate, { String? description, super.skipOffstage })
: _description = description;
final ElementPredicate predicate;
final String? _description;
@override
String get description => _description ?? 'element matching predicate';
@override
bool matches(Element candidate) {
return predicate(candidate);
}
}
class _PredicateSemanticsFinder extends SemanticsFinder
with MatchFinderMixin<SemanticsNode> {
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.view)
: _describeMatch = describeMatch;
final SemanticsNodePredicate predicate;
final DescribeMatchCallback? _describeMatch;
@override
String describeMatch(Plurality plurality) {
return _describeMatch?.call(plurality) ??
'matching semantics predicate';
}
@override
bool matches(SemanticsNode candidate) {
return predicate(candidate);
}
}
mixin _DescendantFinderMixin<CandidateType> on FinderBase<CandidateType> {
FinderBase<CandidateType> get ancestor;
FinderBase<CandidateType> get descendant;
bool get matchRoot;
@override
String describeMatch(Plurality plurality) {
return '${descendant.describeMatch(plurality)} descending from '
'${ancestor.describeMatch(plurality)}'
'${matchRoot ? ' inclusive' : ''}';
}
@override
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) {
final Iterable<CandidateType> descendants = descendant.evaluate();
return candidates.where((CandidateType candidate) => descendants.contains(candidate));
}
@override
Iterable<CandidateType> get allCandidates {
final Iterable<CandidateType> ancestors = ancestor.evaluate();
final List<CandidateType> candidates = ancestors.expand<CandidateType>(
(CandidateType ancestor) => _collectDescendants(ancestor)
).toSet().toList();
if (matchRoot) {
candidates.insertAll(0, ancestors);
}
return candidates;
}
Iterable<CandidateType> _collectDescendants(CandidateType root);
}
class _DescendantWidgetFinder extends Finder
with _DescendantFinderMixin<Element> {
_DescendantWidgetFinder(
this.ancestor,
this.descendant, {
this.matchRoot = false,
super.skipOffstage,
});
@override
final FinderBase<Element> ancestor;
@override
final FinderBase<Element> descendant;
@override
final bool matchRoot;
@override
String get description => describeMatch(Plurality.many);
@override
Iterable<Element> _collectDescendants(Element root) {
return collectAllElementsFrom(root, skipOffstage: skipOffstage);
}
}
class _DescendantSemanticsFinder extends FinderBase<SemanticsNode>
with _DescendantFinderMixin<SemanticsNode> {
_DescendantSemanticsFinder(this.ancestor, this.descendant, {this.matchRoot = false});
@override
final FinderBase<SemanticsNode> ancestor;
@override
final FinderBase<SemanticsNode> descendant;
@override
final bool matchRoot;
@override
Iterable<SemanticsNode> _collectDescendants(SemanticsNode root) {
return collectAllSemanticsNodesFrom(root);
}
}
mixin _AncestorFinderMixin<CandidateType> on FinderBase<CandidateType> {
FinderBase<CandidateType> get ancestor;
FinderBase<CandidateType> get descendant;
bool get matchLeaves;
@override
String describeMatch(Plurality plurality) {
return '${ancestor.describeMatch(plurality)} that are ancestors of '
'${descendant.describeMatch(plurality)}'
'${matchLeaves ? ' inclusive' : ''}';
}
@override
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) {
final Iterable<CandidateType> ancestors = ancestor.evaluate();
return candidates.where((CandidateType element) => ancestors.contains(element));
}
@override
Iterable<CandidateType> get allCandidates {
final List<CandidateType> candidates = <CandidateType>[];
for (final CandidateType leaf in descendant.evaluate()) {
if (matchLeaves) {
candidates.add(leaf);
}
candidates.addAll(_collectAncestors(leaf));
}
return candidates;
}
Iterable<CandidateType> _collectAncestors(CandidateType child);
}
class _AncestorWidgetFinder extends Finder
with _AncestorFinderMixin<Element> {
_AncestorWidgetFinder(this.descendant, this.ancestor, { this.matchLeaves = false }) : super(skipOffstage: false);
@override
final FinderBase<Element> ancestor;
@override
final FinderBase<Element> descendant;
@override
final bool matchLeaves;
@override
String get description => describeMatch(Plurality.many);
@override
Iterable<Element> _collectAncestors(Element child) {
final List<Element> ancestors = <Element>[];
child.visitAncestorElements((Element element) {
ancestors.add(element);
return true;
});
return ancestors;
}
}
class _AncestorSemanticsFinder extends FinderBase<SemanticsNode>
with _AncestorFinderMixin<SemanticsNode> {
_AncestorSemanticsFinder(this.descendant, this.ancestor, this.matchLeaves);
@override
final FinderBase<SemanticsNode> ancestor;
@override
final FinderBase<SemanticsNode> descendant;
@override
final bool matchLeaves;
@override
Iterable<SemanticsNode> _collectAncestors(SemanticsNode child) {
final List<SemanticsNode> ancestors = <SemanticsNode>[];
while (child.parent != null) {
ancestors.add(child.parent!);
child = child.parent!;
}
return ancestors;
}
}