Unverified Commit 5df1c996 authored by pdblasi-google's avatar pdblasi-google Committed by GitHub

Adds SemanticsNode Finders for searching the semantics tree (#127137)

* Pulled `FinderBase` out of `Finder`
  * `FinderBase` can be used for any object, not just elements
  * Terminology was updated to be more "find" related
* Re-implemented `Finder` using `FinderBase<Element>`
  * Backwards compatibility maintained with `_LegacyFinderMixin`
* Introduced base classes for SemanticsNode finders
* Introduced basic SemanticsNode finders through `find.semantics`
* Updated some relevant matchers to make use of the more generic `FinderBase`

Closes #123634
Closes #115874
parent 73e0dbf5
......@@ -126,11 +126,11 @@ class _LiveWidgetController extends LiveWidgetController {
}
/// Runs `finder` repeatedly until it finds one or more [Element]s.
Future<Finder> _waitForElement(Finder finder) async {
Future<FinderBase<Element>> _waitForElement(FinderBase<Element> finder) async {
if (frameSync) {
await _waitUntilFrame(() => binding.transientCallbackCount == 0);
}
await _waitUntilFrame(() => finder.precache());
await _waitUntilFrame(() => finder.tryEvaluate());
if (frameSync) {
await _waitUntilFrame(() => binding.transientCallbackCount == 0);
}
......@@ -138,12 +138,12 @@ class _LiveWidgetController extends LiveWidgetController {
}
@override
Future<void> tap(Finder finder, { int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true }) async {
Future<void> tap(FinderBase<Element> finder, { int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true }) async {
await super.tap(await _waitForElement(finder), pointer: pointer, buttons: buttons, warnIfMissed: warnIfMissed);
}
Future<void> scrollIntoView(Finder finder, {required double alignment}) async {
final Finder target = await _waitForElement(finder);
Future<void> scrollIntoView(FinderBase<Element> finder, {required double alignment}) async {
final FinderBase<Element> target = await _waitForElement(finder);
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment);
}
}
......@@ -56,7 +56,7 @@ Future<void> main() async {
do {
await controller.drag(list, const Offset(0.0, -30.0));
await Future<void>.delayed(const Duration(milliseconds: 20));
} while (!lastItem.precache());
} while (!lastItem.tryEvaluate());
debugPrint('==== MEMORY BENCHMARK ==== DONE ====');
}
......@@ -60,7 +60,7 @@ Future<void> main() async {
do {
await controller.drag(demoList, const Offset(0.0, -300.0));
await Future<void>.delayed(const Duration(milliseconds: 20));
} while (!demoItem.precache());
} while (!demoItem.tryEvaluate());
// Ensure that the center of the "Text fields" item is visible
// because that's where we're going to tap
......
......@@ -15,13 +15,14 @@ const List<Widget> children = <Widget>[
void expectRects(WidgetTester tester, List<Rect> expected) {
final Finder finder = find.byType(SizedBox);
finder.precache();
final List<Rect> actual = <Rect>[];
for (int i = 0; i < expected.length; ++i) {
final Finder current = finder.at(i);
expect(current, findsOneWidget);
actual.add(tester.getRect(finder.at(i)));
}
finder.runCached(() {
for (int i = 0; i < expected.length; ++i) {
final Finder current = finder.at(i);
expect(current, findsOneWidget);
actual.add(tester.getRect(finder.at(i)));
}
});
expect(() => finder.at(expected.length), throwsRangeError);
expect(actual, equals(expected));
}
......
......@@ -58,7 +58,6 @@ export 'dart:async' show Future;
export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart';
export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart';
export 'src/accessibility.dart';
export 'src/all_elements.dart';
export 'src/animation_sheet.dart';
export 'src/binding.dart';
export 'src/controller.dart';
......@@ -83,5 +82,6 @@ export 'src/test_exception_reporter.dart';
export 'src/test_pointer.dart';
export 'src/test_text_input.dart';
export 'src/test_vsync.dart';
export 'src/tree_traversal.dart';
export 'src/widget_tester.dart';
export 'src/window.dart';
......@@ -9,11 +9,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'all_elements.dart';
import 'event_simulation.dart';
import 'finders.dart';
import 'test_async_utils.dart';
import 'test_pointer.dart';
import 'tree_traversal.dart';
import 'window.dart';
/// The default drag touch slop used to break up a large drag into multiple
......@@ -74,7 +74,7 @@ class SemanticsController {
///
/// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled.
SemanticsNode find(Finder finder) {
SemanticsNode find(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
if (!_controller.binding.semanticsEnabled) {
throw StateError('Semantics are not enabled.');
......@@ -149,7 +149,7 @@ class SemanticsController {
/// parts of the traversal.
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
/// match the order of the traversal.
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end, FlutterView? view}) {
Iterable<SemanticsNode> simulatedAccessibilityTraversal({FinderBase<Element>? start, FinderBase<Element>? end, FlutterView? view}) {
TestAsyncUtils.guardSync();
FlutterView? startView;
FlutterView? endView;
......@@ -158,7 +158,7 @@ class SemanticsController {
if (view != null && startView != view) {
throw StateError(
'The start node is not part of the provided view.\n'
'Finder: ${start.description}\n'
'Finder: ${start.toString(describeSelf: true)}\n'
'View of start node: $startView\n'
'Specified view: $view'
);
......@@ -169,7 +169,7 @@ class SemanticsController {
if (view != null && endView != view) {
throw StateError(
'The end node is not part of the provided view.\n'
'Finder: ${end.description}\n'
'Finder: ${end.toString(describeSelf: true)}\n'
'View of end node: $endView\n'
'Specified view: $view'
);
......@@ -178,8 +178,8 @@ class SemanticsController {
if (endView != null && startView != null && endView != startView) {
throw StateError(
'The start and end node are in different views.\n'
'Start finder: ${start!.description}\n'
'End finder: ${end!.description}\n'
'Start finder: ${start!.toString(describeSelf: true)}\n'
'End finder: ${end!.toString(describeSelf: true)}\n'
'View of start node: $startView\n'
'View of end node: $endView'
);
......@@ -200,7 +200,7 @@ class SemanticsController {
if (startIndex == -1) {
throw StateError(
'The expected starting node was not found.\n'
'Finder: ${start.description}\n\n'
'Finder: ${start.toString(describeSelf: true)}\n\n'
'Expected Start Node: $startNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]');
}
......@@ -212,7 +212,7 @@ class SemanticsController {
if (endIndex == -1) {
throw StateError(
'The expected ending node was not found.\n'
'Finder: ${end.description}\n\n'
'Finder: ${end.toString(describeSelf: true)}\n\n'
'Expected End Node: $endNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]');
}
......@@ -342,11 +342,11 @@ abstract class WidgetController {
///
/// * [view] which returns the [TestFlutterView] used when only a single
/// view is being used.
TestFlutterView viewOf(Finder finder) {
TestFlutterView viewOf(FinderBase<Element> finder) {
return _viewOf(finder) as TestFlutterView;
}
FlutterView _viewOf(Finder finder) {
FlutterView _viewOf(FinderBase<Element> finder) {
return firstWidget<View>(
find.ancestor(
of: finder,
......@@ -356,7 +356,7 @@ abstract class WidgetController {
}
/// Checks if `finder` exists in the tree.
bool any(Finder finder) {
bool any(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().isNotEmpty;
}
......@@ -377,7 +377,7 @@ abstract class WidgetController {
///
/// * Use [firstWidget] if you expect to match several widgets but only want the first.
/// * Use [widgetList] if you expect to match several widgets and want all of them.
T widget<T extends Widget>(Finder finder) {
T widget<T extends Widget>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().single.widget as T;
}
......@@ -388,7 +388,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty.
///
/// * Use [widget] if you only expect to match one widget.
T firstWidget<T extends Widget>(Finder finder) {
T firstWidget<T extends Widget>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().first.widget as T;
}
......@@ -397,7 +397,7 @@ abstract class WidgetController {
///
/// * Use [widget] if you only expect to match one widget.
/// * Use [firstWidget] if you expect to match several but only want the first.
Iterable<T> widgetList<T extends Widget>(Finder finder) {
Iterable<T> widgetList<T extends Widget>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) {
final T result = element.widget as T;
......@@ -408,7 +408,7 @@ abstract class WidgetController {
/// Find all layers that are children of the provided [finder].
///
/// The [finder] must match exactly one element.
Iterable<Layer> layerListOf(Finder finder) {
Iterable<Layer> layerListOf(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
final Element element = finder.evaluate().single;
final RenderObject object = element.renderObject!;
......@@ -437,7 +437,7 @@ abstract class WidgetController {
///
/// * Use [firstElement] if you expect to match several elements but only want the first.
/// * Use [elementList] if you expect to match several elements and want all of them.
T element<T extends Element>(Finder finder) {
T element<T extends Element>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().single as T;
}
......@@ -448,7 +448,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty.
///
/// * Use [element] if you only expect to match one element.
T firstElement<T extends Element>(Finder finder) {
T firstElement<T extends Element>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().first as T;
}
......@@ -457,7 +457,7 @@ abstract class WidgetController {
///
/// * Use [element] if you only expect to match one element.
/// * Use [firstElement] if you expect to match several but only want the first.
Iterable<T> elementList<T extends Element>(Finder finder) {
Iterable<T> elementList<T extends Element>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().cast<T>();
}
......@@ -479,7 +479,7 @@ abstract class WidgetController {
///
/// * Use [firstState] if you expect to match several states but only want the first.
/// * Use [stateList] if you expect to match several states and want all of them.
T state<T extends State>(Finder finder) {
T state<T extends State>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return _stateOf<T>(finder.evaluate().single, finder);
}
......@@ -491,7 +491,7 @@ abstract class WidgetController {
/// matching widget has no state.
///
/// * Use [state] if you only expect to match one state.
T firstState<T extends State>(Finder finder) {
T firstState<T extends State>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return _stateOf<T>(finder.evaluate().first, finder);
}
......@@ -503,17 +503,17 @@ abstract class WidgetController {
///
/// * Use [state] if you only expect to match one state.
/// * Use [firstState] if you expect to match several but only want the first.
Iterable<T> stateList<T extends State>(Finder finder) {
Iterable<T> stateList<T extends State>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
}
T _stateOf<T extends State>(Element element, Finder finder) {
T _stateOf<T extends State>(Element element, FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
if (element is StatefulElement) {
return element.state as T;
}
throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.describeMatch(Plurality.many)}, is not a StatefulWidget.');
}
/// Render objects of all the widgets currently in the widget tree
......@@ -535,7 +535,7 @@ abstract class WidgetController {
///
/// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
/// * Use [renderObjectList] if you expect to match several render objects and want all of them.
T renderObject<T extends RenderObject>(Finder finder) {
T renderObject<T extends RenderObject>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().single.renderObject! as T;
}
......@@ -546,7 +546,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty.
///
/// * Use [renderObject] if you only expect to match one render object.
T firstRenderObject<T extends RenderObject>(Finder finder) {
T firstRenderObject<T extends RenderObject>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().first.renderObject! as T;
}
......@@ -555,7 +555,7 @@ abstract class WidgetController {
///
/// * Use [renderObject] if you only expect to match one render object.
/// * Use [firstRenderObject] if you expect to match several but only want the first.
Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
Iterable<T> renderObjectList<T extends RenderObject>(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) {
final T result = element.renderObject! as T;
......@@ -603,7 +603,7 @@ abstract class WidgetController {
/// For example, a test that verifies that tapping a disabled button does not
/// trigger the button would set `warnIfMissed` to false, because the button
/// would ignore the tap.
Future<void> tap(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
Future<void> tap(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
return tapAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'tap'), pointer: pointer, buttons: buttons);
}
......@@ -628,7 +628,7 @@ abstract class WidgetController {
/// * [tap], which presses and releases a pointer at the given location.
/// * [longPress], which presses and releases a pointer with a gap in
/// between long enough to trigger the long-press gesture.
Future<TestGesture> press(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
Future<TestGesture> press(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
return TestAsyncUtils.guard<TestGesture>(() {
return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons);
});
......@@ -646,7 +646,7 @@ abstract class WidgetController {
/// later verify that long-pressing the same location (using the same finder)
/// has no effect (since the widget is now obscured), setting `warnIfMissed`
/// to false on that second call.
Future<void> longPress(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
Future<void> longPress(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
return longPressAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'longPress'), pointer: pointer, buttons: buttons);
}
......@@ -707,7 +707,7 @@ abstract class WidgetController {
/// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [drag].
Future<void> fling(
Finder finder,
FinderBase<Element> finder,
Offset offset,
double speed, {
int? pointer,
......@@ -787,7 +787,7 @@ abstract class WidgetController {
/// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [drag].
Future<void> trackpadFling(
Finder finder,
FinderBase<Element> finder,
Offset offset,
double speed, {
int? pointer,
......@@ -952,7 +952,7 @@ abstract class WidgetController {
/// should be left to their default values.
/// {@endtemplate}
Future<void> drag(
Finder finder,
FinderBase<Element> finder,
Offset offset, {
int? pointer,
int buttons = kPrimaryButton,
......@@ -1085,7 +1085,7 @@ abstract class WidgetController {
/// more accurate time control.
/// {@endtemplate}
Future<void> timedDrag(
Finder finder,
FinderBase<Element> finder,
Offset offset,
Duration duration, {
int? pointer,
......@@ -1282,14 +1282,14 @@ abstract class WidgetController {
/// this method is being called from another that is forwarding its own
/// `warnIfMissed` parameter (see e.g. the implementation of [tap]).
/// {@endtemplate}
Offset getCenter(Finder finder, { bool warnIfMissed = false, String callee = 'getCenter' }) {
Offset getCenter(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getCenter' }) {
return _getElementPoint(finder, (Size size) => size.center(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
}
/// Returns the point at the top left of the given widget.
///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
Offset getTopLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) {
Offset getTopLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) {
return _getElementPoint(finder, (Size size) => Offset.zero, warnIfMissed: warnIfMissed, callee: callee);
}
......@@ -1297,7 +1297,7 @@ abstract class WidgetController {
/// point is not inside the object's hit test area.
///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
Offset getTopRight(Finder finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) {
Offset getTopRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) {
return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
}
......@@ -1305,7 +1305,7 @@ abstract class WidgetController {
/// point is not inside the object's hit test area.
///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
Offset getBottomLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) {
Offset getBottomLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) {
return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
}
......@@ -1313,7 +1313,7 @@ abstract class WidgetController {
/// point is not inside the object's hit test area.
///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
Offset getBottomRight(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) {
Offset getBottomRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) {
return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
}
......@@ -1340,7 +1340,7 @@ abstract class WidgetController {
/// in the documentation for the [flutter_test] library.
static bool hitTestWarningShouldBeFatal = false;
Offset _getElementPoint(Finder finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
Offset _getElementPoint(FinderBase<Element> finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
TestAsyncUtils.guardSync();
final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) {
......@@ -1411,7 +1411,7 @@ abstract class WidgetController {
/// Returns the size of the given widget. This is only valid once
/// the widget's render object has been laid out at least once.
Size getSize(Finder finder) {
Size getSize(FinderBase<Element> finder) {
TestAsyncUtils.guardSync();
final Element element = finder.evaluate().single;
final RenderBox box = element.renderObject! as RenderBox;
......@@ -1579,7 +1579,7 @@ abstract class WidgetController {
/// Returns the rect of the given widget. This is only valid once
/// the widget's render object has been laid out at least once.
Rect getRect(Finder finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder));
Rect getRect(FinderBase<Element> finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder));
/// Attempts to find the [SemanticsNode] of first result from `finder`.
///
......@@ -1596,7 +1596,7 @@ abstract class WidgetController {
/// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled.
// TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670.
SemanticsNode getSemantics(Finder finder) => semantics.find(finder);
SemanticsNode getSemantics(FinderBase<Element> finder) => semantics.find(finder);
/// Enable semantics in a test by creating a [SemanticsHandle].
///
......@@ -1620,7 +1620,7 @@ abstract class WidgetController {
///
/// * [Scrollable.ensureVisible], which is the production API used to
/// implement this method.
Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder));
Future<void> ensureVisible(FinderBase<Element> finder) => Scrollable.ensureVisible(element(finder));
/// Repeatedly scrolls a [Scrollable] by `delta` in the
/// [Scrollable.axisDirection] direction until a widget matching `finder` is
......@@ -1645,9 +1645,9 @@ abstract class WidgetController {
///
/// * [dragUntilVisible], which implements the body of this method.
Future<void> scrollUntilVisible(
Finder finder,
FinderBase<Element> finder,
double delta, {
Finder? scrollable,
FinderBase<Element>? scrollable,
int maxScrolls = 50,
Duration duration = const Duration(milliseconds: 50),
}
......@@ -1688,8 +1688,8 @@ abstract class WidgetController {
/// * [scrollUntilVisible], which wraps this method with an API that is more
/// convenient when dealing with a [Scrollable].
Future<void> dragUntilVisible(
Finder finder,
Finder view,
FinderBase<Element> finder,
FinderBase<Element> view,
Offset moveStep, {
int maxIteration = 50,
Duration duration = const Duration(milliseconds: 50),
......
......@@ -2,11 +2,14 @@
// 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 'all_elements.dart';
import 'binding.dart';
import 'tree_traversal.dart';
/// Signature for [CommonFinders.byWidgetPredicate].
typedef WidgetPredicate = bool Function(Widget widget);
......@@ -14,7 +17,13 @@ typedef WidgetPredicate = bool Function(Widget widget);
/// Signature for [CommonFinders.byElementPredicate].
typedef ElementPredicate = bool Function(Element element);
/// Some frequently used widget [Finder]s.
/// Signature for [CommonSemanticsFinders.byPredicate].
typedef SemanticsNodePredicate = bool Function(SemanticsNode node);
/// Signature for [FinderBase.describeMatch].
typedef DescribeMatchCallback = String Function(Plurality plurality);
/// Some frequently used [Finder]s and [SemanticsFinder]s.
const CommonFinders find = CommonFinders._();
// Examples can assume:
......@@ -23,12 +32,16 @@ const CommonFinders find = CommonFinders._();
// late String filePath;
// late Key backKey;
/// Provides lightweight syntax for getting frequently used widget [Finder]s.
/// 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._();
/// Finds [Text], [EditableText], and optionally [RichText] widgets
/// containing string equal to the `text` argument.
///
......@@ -64,7 +77,7 @@ class CommonFinders {
bool findRichText = false,
bool skipOffstage = true,
}) {
return _TextFinder(
return _TextWidgetFinder(
text,
findRichText: findRichText,
skipOffstage: skipOffstage,
......@@ -108,7 +121,7 @@ class CommonFinders {
bool findRichText = false,
bool skipOffstage = true,
}) {
return _TextContainingFinder(
return _TextContainingWidgetFinder(
pattern,
findRichText: findRichText,
skipOffstage: skipOffstage
......@@ -121,12 +134,12 @@ class CommonFinders {
/// ## Sample code
///
/// ```dart
/// // Suppose you have a button with text 'Update' in it:
/// // Suppose there is a button with text 'Update' in it:
/// const Button(
/// child: Text('Update')
/// );
///
/// // You can find and tap on it like this:
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithText(Button, 'Update'));
/// ```
///
......@@ -150,9 +163,9 @@ class CommonFinders {
///
/// 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 }) => _WidgetImageFinder(image, skipOffstage: skipOffstage);
Finder image(ImageProvider image, { bool skipOffstage = true }) => _ImageWidgetFinder(image, skipOffstage: skipOffstage);
/// Finds widgets by searching for one with a particular [Key].
/// Finds widgets by searching for one with the given `key`.
///
/// ## Sample code
///
......@@ -162,7 +175,7 @@ class CommonFinders {
///
/// 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 }) => _KeyFinder(key, skipOffstage: skipOffstage);
Finder byKey(Key key, { bool skipOffstage = true }) => _KeyWidgetFinder(key, skipOffstage: skipOffstage);
/// Finds widgets by searching for widgets implementing a particular type.
///
......@@ -180,13 +193,13 @@ class CommonFinders {
///
/// See also:
/// * [byType], which does not do subtype tests.
Finder bySubtype<T extends Widget>({ bool skipOffstage = true }) => _WidgetSubtypeFinder<T>(skipOffstage: skipOffstage);
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 that's
/// an abstract class.
/// `byType(StatefulWidget)` will never find anything since [StatefulWidget]
/// is an abstract class.
///
/// The `type` argument must be a subclass of [Widget].
///
......@@ -201,7 +214,7 @@ class CommonFinders {
///
/// See also:
/// * [bySubtype], which allows subtype tests.
Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage);
Finder byType(Type type, { bool skipOffstage = true }) => _TypeWidgetFinder(type, skipOffstage: skipOffstage);
/// Finds [Icon] widgets containing icon data equal to the `icon`
/// argument.
......@@ -214,7 +227,7 @@ class CommonFinders {
///
/// 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 }) => _WidgetIconFinder(icon, skipOffstage: skipOffstage);
Finder byIcon(IconData icon, { bool skipOffstage = true }) => _IconWidgetFinder(icon, skipOffstage: skipOffstage);
/// Looks for widgets that contain an [Icon] descendant displaying [IconData]
/// `icon` in it.
......@@ -222,12 +235,12 @@ class CommonFinders {
/// ## Sample code
///
/// ```dart
/// // Suppose you have a button with icon 'arrow_forward' in it:
/// // Suppose there is a button with icon 'arrow_forward' in it:
/// const Button(
/// child: Icon(Icons.arrow_forward)
/// );
///
/// // You can find and tap on it like this:
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
/// ```
///
......@@ -240,18 +253,18 @@ class CommonFinders {
);
}
/// Looks for widgets that contain an [Image] descendant displaying [ImageProvider]
/// `image` in it.
/// Looks for widgets that contain an [Image] descendant displaying
/// [ImageProvider] `image` in it.
///
/// ## Sample code
///
/// ```dart
/// // Suppose you have a button with image in it:
/// // Suppose there is a button with an image in it:
/// Button(
/// child: Image.file(File(filePath))
/// );
///
/// // You can find and tap on it like this:
/// // It can be found and tapped like this:
/// tester.tap(find.widgetWithImage(Button, FileImage(File(filePath))));
/// ```
///
......@@ -268,7 +281,7 @@ class CommonFinders {
///
/// This does not do subclass tests, so for example
/// `byElementType(VirtualViewportElement)` will never find anything
/// since that's an abstract class.
/// since [RenderObjectElement] is an abstract class.
///
/// The `type` argument must be a subclass of [Element].
///
......@@ -280,39 +293,39 @@ class CommonFinders {
///
/// 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 }) => _ElementTypeFinder(type, skipOffstage: skipOffstage);
Finder byElementType(Type type, { bool skipOffstage = true }) => _ElementTypeWidgetFinder(type, skipOffstage: skipOffstage);
/// Finds widgets whose current widget is the instance given by the
/// Finds widgets whose current widget is the instance given by the `widget`
/// argument.
///
/// ## Sample code
///
/// ```dart
/// // Suppose you have a button created like this:
/// // Suppose there is a button created like this:
/// Widget myButton = const Button(
/// child: Text('Update')
/// );
///
/// // You can find and tap on it like this:
/// // 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 }) => _WidgetFinder(widget, skipOffstage: skipOffstage);
Finder byWidget(Widget widget, { bool skipOffstage = true }) => _ExactWidgetFinder(widget, skipOffstage: skipOffstage);
/// Finds widgets using a widget [predicate].
/// Finds widgets using a widget `predicate`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back',
/// description: 'widget with tooltip "Back"',
/// description: 'with tooltip "Back"',
/// ), findsOneWidget);
/// ```
///
/// If [description] is provided, then this uses it as the description of the
/// 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.
......@@ -320,10 +333,10 @@ class CommonFinders {
/// 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 _WidgetPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
return _WidgetPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage);
}
/// Finds Tooltip widgets with the given message.
/// Finds [Tooltip] widgets with the given `message`.
///
/// ## Sample code
///
......@@ -340,13 +353,13 @@ class CommonFinders {
);
}
/// Finds widgets using an element [predicate].
/// Finds widgets using an element `predicate`.
///
/// ## Sample code
///
/// ```dart
/// expect(find.byElementPredicate(
/// // finds elements of type SingleChildRenderObjectElement, including
/// // 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,
......@@ -354,7 +367,7 @@ class CommonFinders {
/// ), findsOneWidget);
/// ```
///
/// If [description] is provided, then this uses it as the description of the
/// 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.
......@@ -362,36 +375,37 @@ class CommonFinders {
/// 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 _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage);
return _ElementPredicateWidgetFinder(predicate, description: description, skipOffstage: skipOffstage);
}
/// Finds widgets that are descendants of the [of] parameter and that match
/// the [matching] parameter.
/// 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')
/// 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]
/// 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
/// 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 Finder of,
required Finder matching,
required FinderBase<Element> of,
required FinderBase<Element> matching,
bool matchRoot = false,
bool skipOffstage = true,
}) {
return _DescendantFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
return _DescendantWidgetFinder(of, matching, matchRoot: matchRoot, skipOffstage: skipOffstage);
}
/// Finds widgets that are ancestors of the [of] parameter and that match
/// the [matching] parameter.
/// Finds widgets that are ancestors of the `of` parameter and that match
/// the `matching` parameter.
///
/// ## Sample code
///
......@@ -409,14 +423,14 @@ class CommonFinders {
/// );
/// ```
///
/// If the [matchRoot] argument is true then the widget(s) specified by [of]
/// If the `matchRoot` argument is true then the widget(s) specified by `of`
/// will be matched along with the ancestors.
Finder ancestor({
required Finder of,
required Finder matching,
required FinderBase<Element> of,
required FinderBase<Element> matching,
bool matchRoot = false,
}) {
return _AncestorFinder(of, matching, matchRoot: matchRoot);
return _AncestorWidgetFinder(of, matching, matchLeaves: matchRoot);
}
/// Finds [Semantics] widgets matching the given `label`, either by
......@@ -466,58 +480,441 @@ class CommonFinders {
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class Finder {
/// Initializes a Finder. Used by subclasses to initialize the [skipOffstage]
/// property.
Finder({ this.skipOffstage = true });
/// Describes what the finder is looking for. The description should be
/// a brief English noun phrase describing the finder's pattern.
String get description;
/// Provides lightweight syntax for getting frequently used semantics finders.
///
/// This class is instantiated once, as [CommonFinders.semantics], under [find].
class CommonSemanticsFinders {
const CommonSemanticsFinders._();
/// Returns all the elements in the given list that match this
/// finder's pattern.
/// Finds an ancestor of `of` that matches `matching`.
///
/// When implementing your own Finders that inherit directly from
/// [Finder], this is the main method to override. If your finder
/// can efficiently be described just in terms of a predicate
/// function, consider extending [MatchFinder] instead.
Iterable<Element> apply(Iterable<Element> candidates);
/// 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);
}
/// Whether this finder skips nodes that are offstage.
/// Finds a descendant of `of` that matches `matching`.
///
/// 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;
/// 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,
_rootFromView(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,
);
}
/// Returns all the [Element]s that will be considered by this finder.
/// 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,
);
}
bool _matchesPattern(String target, Pattern pattern) {
if (pattern is RegExp) {
return pattern.hasMatch(target);
} else {
return pattern == target;
}
}
SemanticsNode _rootFromView(FlutterView? view) {
view ??= TestWidgetsFlutterBinding.instance.platformDispatcher.implicitView;
assert(view != null, 'The given view was not available. Ensure WidgetTester.view is available or pass in a specific view using WidgetTester.viewOf.');
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
.firstWhere((RenderView r) => r.flutterView == view);
return renderView.owner!.semanticsOwner!.rootSemanticsNode!;
}
}
/// 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.
///
/// This is the internal API for the [Finder]. To obtain the elements from
/// a [Finder] in a test, consider [WidgetTester.elementList].
/// {@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":
///
/// See [collectAllElementsFrom].
/// ```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<Element> get allCandidates {
return collectAllElementsFrom(
WidgetsBinding.instance.rootElement!,
skipOffstage: skipOffstage,
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;
Iterable<Element>? _cachedResult;
/// 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 the current result. If [precache] was called and returned true, this will
/// cheaply return the result that was computed then. Otherwise, it creates a new
/// iterable to compute the answer.
/// Returns all the elements in the given list that match this
/// finder's pattern.
///
/// Calling this clears the cache from [precache].
Iterable<Element> evaluate() {
final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
_cachedResult = null;
return result;
/// 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
......@@ -525,122 +922,221 @@ abstract class Finder {
/// 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(_cachedResult == null);
final Iterable<Element> result = apply(allCandidates);
if (result.isNotEmpty) {
_cachedResult = result;
assert(_precacheResults == null);
if (tryEvaluate()) {
return true;
}
_cachedResult = null;
_precacheResults = null;
return false;
}
/// Returns a variant of this finder that only matches the first element
/// matched by this finder.
Finder get first => _FirstFinder(this);
@override
Iterable<Element> findInCandidates(Iterable<Element> candidates) {
return apply(candidates);
}
}
/// Returns a variant of this finder that only matches the last element
/// matched by this finder.
Finder get last => _LastFinder(this);
/// A base class for creating finders that search the [Element] tree for
/// [Widget]s.
///
/// The [findInCandidates] method must be overriden 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});
/// Returns a variant of this finder that only matches the element at the
/// given index matched by this finder.
Finder at(int index) => _IndexFinder(this, index);
/// 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
/// 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 }) => _HitTestableFinder(this, at);
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 starting at the given
/// `root`.
SemanticsFinder(this.root);
/// The root of the semantics tree that this finder will search.
final SemanticsNode root;
@override
String toString() {
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
final List<Element> widgets = evaluate().toList();
final int count = widgets.length;
if (count == 0) {
return 'zero widgets with $description$additional';
}
if (count == 1) {
return 'exactly one widget with $description$additional: ${widgets.single}';
}
if (count < 4) {
return '$count widgets with $description$additional: $widgets';
}
return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
Iterable<SemanticsNode> get allCandidates {
return collectAllSemanticsNodesFrom(root);
}
}
/// Applies additional filtering against a [parent] [Finder].
abstract class ChainedFinder extends Finder {
/// Create a Finder chained against the candidates of another [Finder].
ChainedFinder(this.parent);
/// A mixin that applies additional filtering to the results of a parent [Finder].
mixin ChainedFinderMixin<CandidateType> on FinderBase<CandidateType> {
/// Another [Finder] that will run first.
final Finder parent;
/// Another finder whose results will be further filtered.
FinderBase<CandidateType> get parent;
/// Return another [Iterable] when given an [Iterable] of candidates from a
/// parent [Finder].
/// parent [FinderBase].
///
/// This is the method to implement when subclassing [ChainedFinder].
Iterable<Element> filter(Iterable<Element> parentCandidates);
/// This is the main method to implement when mixing in [ChainedFinderMixin].
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates);
@override
Iterable<Element> apply(Iterable<Element> candidates) {
return filter(parent.apply(candidates));
Iterable<CandidateType> findInCandidates(Iterable<CandidateType> candidates) {
return filter(parent.findInCandidates(candidates));
}
@override
Iterable<Element> get allCandidates => parent.allCandidates;
Iterable<CandidateType> get allCandidates => parent.allCandidates;
}
class _FirstFinder extends ChainedFinder {
_FirstFinder(super.parent);
/// 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
String get description => '${parent.description} (ignoring all but first)';
final FinderBase<Element> parent;
}
mixin _FirstFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType>{
@override
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} (ignoring all but first)';
}
@override
Iterable<CandidateType> filter(Iterable<CandidateType> parentCandidates) sync* {
yield parentCandidates.first;
}
}
class _LastFinder extends ChainedFinder {
_LastFinder(super.parent);
class _FirstFinder<CandidateType> extends FinderBase<CandidateType>
with ChainedFinderMixin<CandidateType>, _FirstFinderMixin<CandidateType> {
_FirstFinder(this.parent);
@override
String get description => '${parent.description} (ignoring all but last)';
final FinderBase<CandidateType> parent;
}
class _FirstWidgetFinder extends ChainedFinder with _FirstFinderMixin<Element> {
_FirstWidgetFinder(super.parent);
@override
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
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 _IndexFinder extends ChainedFinder {
_IndexFinder(super.parent, this.index);
class _LastFinder<CandidateType> extends FinderBase<CandidateType>
with ChainedFinderMixin<CandidateType>, _LastFinderMixin<CandidateType>{
_LastFinder(this.parent);
final int index;
@override
final FinderBase<CandidateType> parent;
}
class _LastWidgetFinder extends ChainedFinder with _LastFinderMixin<Element> {
_LastWidgetFinder(super.parent);
@override
String get description => '${parent.description} (ignoring all but index $index)';
String get description => describeMatch(Plurality.many);
}
mixin _IndexFinderMixin<CandidateType> on ChainedFinderMixin<CandidateType> {
int get index;
@override
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
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 _HitTestableFinder extends ChainedFinder {
_HitTestableFinder(super.parent, this.alignment);
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 get description => '${parent.description} (considering only hit-testable ones)';
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* {
......@@ -660,24 +1156,27 @@ class _HitTestableFinder extends ChainedFinder {
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
/// Initializes a predicate-based Finder. Used by subclasses to initialize the
/// [skipOffstage] property.
MatchFinder({ super.skipOffstage });
/// 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 your own MatchFinder, this is the main method to override.
bool matches(Element candidate);
/// When implementing a MatchFinder, this is the main method to override.
bool matches(CandidateType candidate);
@override
Iterable<Element> apply(Iterable<Element> candidates) {
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,
......@@ -740,8 +1239,8 @@ abstract class _MatchTextFinder extends MatchFinder {
}
}
class _TextFinder extends _MatchTextFinder {
_TextFinder(
class _TextWidgetFinder extends _MatchTextFinder {
_TextWidgetFinder(
this.text, {
super.findRichText,
super.skipOffstage,
......@@ -758,8 +1257,8 @@ class _TextFinder extends _MatchTextFinder {
}
}
class _TextContainingFinder extends _MatchTextFinder {
_TextContainingFinder(
class _TextContainingWidgetFinder extends _MatchTextFinder {
_TextContainingWidgetFinder(
this.pattern, {
super.findRichText,
super.skipOffstage,
......@@ -776,8 +1275,8 @@ class _TextContainingFinder extends _MatchTextFinder {
}
}
class _KeyFinder extends MatchFinder {
_KeyFinder(this.key, { super.skipOffstage });
class _KeyWidgetFinder extends MatchFinder {
_KeyWidgetFinder(this.key, { super.skipOffstage });
final Key key;
......@@ -790,8 +1289,8 @@ class _KeyFinder extends MatchFinder {
}
}
class _WidgetSubtypeFinder<T extends Widget> extends MatchFinder {
_WidgetSubtypeFinder({ super.skipOffstage });
class _SubtypeWidgetFinder<T extends Widget> extends MatchFinder {
_SubtypeWidgetFinder({ super.skipOffstage });
@override
String get description => 'is "$T"';
......@@ -802,8 +1301,8 @@ class _WidgetSubtypeFinder<T extends Widget> extends MatchFinder {
}
}
class _WidgetTypeFinder extends MatchFinder {
_WidgetTypeFinder(this.widgetType, { super.skipOffstage });
class _TypeWidgetFinder extends MatchFinder {
_TypeWidgetFinder(this.widgetType, { super.skipOffstage });
final Type widgetType;
......@@ -816,8 +1315,8 @@ class _WidgetTypeFinder extends MatchFinder {
}
}
class _WidgetImageFinder extends MatchFinder {
_WidgetImageFinder(this.image, { super.skipOffstage });
class _ImageWidgetFinder extends MatchFinder {
_ImageWidgetFinder(this.image, { super.skipOffstage });
final ImageProvider image;
......@@ -836,8 +1335,8 @@ class _WidgetImageFinder extends MatchFinder {
}
}
class _WidgetIconFinder extends MatchFinder {
_WidgetIconFinder(this.icon, { super.skipOffstage });
class _IconWidgetFinder extends MatchFinder {
_IconWidgetFinder(this.icon, { super.skipOffstage });
final IconData icon;
......@@ -851,8 +1350,8 @@ class _WidgetIconFinder extends MatchFinder {
}
}
class _ElementTypeFinder extends MatchFinder {
_ElementTypeFinder(this.elementType, { super.skipOffstage });
class _ElementTypeWidgetFinder extends MatchFinder {
_ElementTypeWidgetFinder(this.elementType, { super.skipOffstage });
final Type elementType;
......@@ -865,8 +1364,8 @@ class _ElementTypeFinder extends MatchFinder {
}
}
class _WidgetFinder extends MatchFinder {
_WidgetFinder(this.widget, { super.skipOffstage });
class _ExactWidgetFinder extends MatchFinder {
_ExactWidgetFinder(this.widget, { super.skipOffstage });
final Widget widget;
......@@ -879,15 +1378,15 @@ class _WidgetFinder extends MatchFinder {
}
}
class _WidgetPredicateFinder extends MatchFinder {
_WidgetPredicateFinder(this.predicate, { String? description, super.skipOffstage })
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 ($predicate)';
String get description => _description ?? 'widget matching predicate';
@override
bool matches(Element candidate) {
......@@ -895,15 +1394,15 @@ class _WidgetPredicateFinder extends MatchFinder {
}
}
class _ElementPredicateFinder extends MatchFinder {
_ElementPredicateFinder(this.predicate, { String? description, super.skipOffstage })
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 ($predicate)';
String get description => _description ?? 'element matching predicate';
@override
bool matches(Element candidate) {
......@@ -911,80 +1410,182 @@ class _ElementPredicateFinder extends MatchFinder {
}
}
class _DescendantFinder extends Finder {
_DescendantFinder(
this.ancestor,
this.descendant, {
this.matchRoot = false,
super.skipOffstage,
});
class _PredicateSemanticsFinder extends SemanticsFinder
with MatchFinderMixin<SemanticsNode> {
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.root)
: _describeMatch = describeMatch;
final Finder ancestor;
final Finder descendant;
final bool matchRoot;
final SemanticsNodePredicate predicate;
final DescribeMatchCallback? _describeMatch;
@override
String get description {
if (matchRoot) {
return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}';
}
return '${descendant.description} that has ancestor(s) with ${ancestor.description}';
String describeMatch(Plurality plurality) {
return _describeMatch?.call(plurality) ??
'matching semantics predicate';
}
@override
Iterable<Element> apply(Iterable<Element> candidates) {
final Iterable<Element> descendants = descendant.evaluate();
return candidates.where((Element element) => descendants.contains(element));
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
Iterable<Element> get allCandidates {
final Iterable<Element> ancestorElements = ancestor.evaluate();
final List<Element> candidates = ancestorElements.expand<Element>(
(Element element) => collectAllElementsFrom(element, skipOffstage: skipOffstage)
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, ancestorElements);
candidates.insertAll(0, ancestors);
}
return candidates;
}
Iterable<CandidateType> _collectDescendants(CandidateType root);
}
class _AncestorFinder extends Finder {
_AncestorFinder(this.descendant, this.ancestor, { this.matchRoot = false }) : super(skipOffstage: false);
class _DescendantWidgetFinder extends Finder
with _DescendantFinderMixin<Element> {
_DescendantWidgetFinder(
this.ancestor,
this.descendant, {
this.matchRoot = false,
super.skipOffstage,
});
final Finder ancestor;
final Finder descendant;
@override
final FinderBase<Element> ancestor;
@override
final FinderBase<Element> descendant;
@override
final bool matchRoot;
@override
String get description {
if (matchRoot) {
return 'ancestor ${ancestor.description} beginning with ${descendant.description}';
}
return '${ancestor.description} which is an ancestor of ${descendant.description}';
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
Iterable<Element> apply(Iterable<Element> candidates) {
final Iterable<Element> ancestors = ancestor.evaluate();
return candidates.where((Element element) => ancestors.contains(element));
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
Iterable<Element> get allCandidates {
final List<Element> candidates = <Element>[];
for (final Element root in descendant.evaluate()) {
final List<Element> ancestors = <Element>[];
if (matchRoot) {
ancestors.add(root);
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);
}
root.visitAncestorElements((Element element) {
ancestors.add(element);
return true;
});
candidates.addAll(ancestors);
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;
}
}
......@@ -16,11 +16,12 @@ import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementatio
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
import 'accessibility.dart';
import 'binding.dart';
import 'controller.dart';
import 'finders.dart';
import 'goldens.dart';
import 'widget_tester.dart' show WidgetTester;
/// Asserts that the [Finder] matches no widgets in the widget tree.
/// Asserts that the [FinderBase] matches nothing in the available candidates.
///
/// ## Sample code
///
......@@ -30,14 +31,16 @@ import 'widget_tester.dart' show WidgetTester;
///
/// 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.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsNothing = _FindsCountMatcher(null, 0);
/// Asserts that the [Finder] locates at least one widget in the widget tree.
///
/// This is equivalent to the preferred [findsAny] method.
///
/// ## Sample code
///
/// ```dart
......@@ -47,13 +50,31 @@ const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
/// 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.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
/// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsWidgets = _FindsCountMatcher(1, null);
/// Asserts that the [FinderBase] locates at least one matching candidate.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsAny);
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsAny = _FindsCountMatcher(1, null);
/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
///
/// This is equivalent to the preferred [findsOne] method.
///
/// ## Sample code
///
/// ```dart
......@@ -63,13 +84,31 @@ const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
/// 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.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsOneWidget = _FindsCountMatcher(1, 1);
/// Asserts that the [FinderBase] finds exactly one matching candidate.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsOne);
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsExactly], when you want the finder to find a specific number candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsOne = _FindsCountMatcher(1, 1);
/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
///
/// This is equivalent to the preferred [findsExactly] method.
///
/// ## Sample code
///
/// ```dart
......@@ -79,13 +118,31 @@ const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
/// 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.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
Matcher findsNWidgets(int n) => _FindsCountMatcher(n, n);
/// Asserts that the [FinderBase] locates the specified number of candidates.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsExactly(2));
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOne], when you want the finder to find exactly one candidates.
/// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
Matcher findsExactly(int n) => _FindsCountMatcher(n, n);
/// Asserts that the [Finder] locates at least a number of widgets in the widget tree.
///
/// This is equivalent to the preferred [findsAtLeast] method.
///
/// ## Sample code
///
/// ```dart
......@@ -95,10 +152,26 @@ Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
/// 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.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
Matcher findsAtLeastNWidgets(int n) => _FindsWidgetMatcher(n, null);
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
Matcher findsAtLeastNWidgets(int n) => _FindsCountMatcher(n, null);
/// Asserts that the [FinderBase] locates at least the given number of candidates.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsAtLeast(2));
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOne], when you want the finder to find exactly one candidates.
/// * [findsExactly], when you want the finder to find a specific number of candidates.
Matcher findsAtLeast(int n) => _FindsCountMatcher(n, null);
/// Asserts that the [Finder] locates a single widget that has at
/// least one [Offstage] widget ancestor.
......@@ -527,7 +600,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
///
/// See also:
///
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
/// * [containsSemantics], a similar matcher without default values for flags or actions.
Matcher matchesSemantics({
String? label,
......@@ -707,7 +780,7 @@ Matcher matchesSemantics({
///
/// See also:
///
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
/// * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({
String? label,
......@@ -900,19 +973,19 @@ AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
return _DoesNotMatchAccessibilityGuideline(guideline);
}
class _FindsWidgetMatcher extends Matcher {
const _FindsWidgetMatcher(this.min, this.max);
class _FindsCountMatcher extends Matcher {
const _FindsCountMatcher(this.min, this.max);
final int? min;
final int? max;
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
bool matches(covariant FinderBase<dynamic> finder, Map<dynamic, dynamic> matchState) {
assert(min != null || max != null);
assert(min == null || max == null || min! <= max!);
matchState[Finder] = finder;
matchState[FinderBase] = finder;
int count = 0;
final Iterator<Element> iterator = finder.evaluate().iterator;
final Iterator<dynamic> iterator = finder.evaluate().iterator;
if (min != null) {
while (count < min! && iterator.moveNext()) {
count += 1;
......@@ -937,26 +1010,26 @@ class _FindsWidgetMatcher extends Matcher {
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 one matching candidate');
}
return description.add('exactly $min matching nodes in the widget tree');
return description.add('exactly $min matching candidates');
}
if (min == null) {
if (max == 0) {
return description.add('no matching nodes in the widget tree');
return description.add('no matching candidates');
}
if (max == 1) {
return description.add('at most one matching node in the widget tree');
return description.add('at most one matching candidate');
}
return description.add('at most $max matching nodes in the widget tree');
return description.add('at most $max matching candidates');
}
if (max == null) {
if (min == 1) {
return description.add('at least one matching node in the widget tree');
return description.add('at least one matching candidate');
}
return description.add('at least $min matching nodes in the widget tree');
return description.add('at least $min matching candidates');
}
return description.add('between $min and $max matching nodes in the widget tree (inclusive)');
return description.add('between $min and $max matching candidates (inclusive)');
}
@override
......@@ -966,8 +1039,8 @@ class _FindsWidgetMatcher extends Matcher {
Map<dynamic, dynamic> matchState,
bool verbose,
) {
final Finder finder = matchState[Finder] as Finder;
final int count = finder.evaluate().length;
final FinderBase<dynamic> finder = matchState[FinderBase] as FinderBase<dynamic>;
final int count = finder.found.length;
if (count == 0) {
assert(min != null && min! > 0);
if (min == 1 && max == 1) {
......
......@@ -3,12 +3,13 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/widgets.dart';
/// Provides an iterable that efficiently returns all the elements
/// rooted at the given element. See [CachingIterable] for details.
/// Provides an iterable that efficiently returns all the [Element]s
/// rooted at the given [Element]. See [CachingIterable] for details.
///
/// This method must be called again if the tree changes. You cannot
/// This function must be called again if the tree changes. You cannot
/// call this function once, then reuse the iterable after having
/// changed the state of the tree, because the iterable returned by
/// this function caches the results and only walks the tree once.
......@@ -20,11 +21,84 @@ Iterable<Element> collectAllElementsFrom(
Element rootElement, {
required bool skipOffstage,
}) {
return CachingIterable<Element>(_DepthFirstChildIterator(rootElement, skipOffstage));
return CachingIterable<Element>(_DepthFirstElementTreeIterator(rootElement, !skipOffstage));
}
/// Provides a recursive, efficient, depth first search of an element tree.
/// Provides an iterable that efficiently returns all the [SemanticsNode]s
/// rooted at the given [SemanticsNode]. See [CachingIterable] for details.
///
/// By default, this will traverse the semantics tree in semantic traversal
/// order, but the traversal order can be changed by passing in a different
/// value to `order`.
///
/// This function must be called again if the semantics change. You cannot call
/// this function once, then reuse the iterable after having changed the state
/// of the tree, because the iterable returned by this function caches the
/// results and only walks the tree once.
///
/// The same applies to any iterable obtained indirectly through this
/// one, for example the results of calling `where` on this iterable
/// are also cached.
Iterable<SemanticsNode> collectAllSemanticsNodesFrom(
SemanticsNode root, {
DebugSemanticsDumpOrder order = DebugSemanticsDumpOrder.traversalOrder,
}) {
return CachingIterable<SemanticsNode>(_DepthFirstSemanticsTreeIterator(root, order));
}
/// Provides a recursive, efficient, depth first search of a tree.
///
/// This iterator executes a depth first search as an iterable, and iterates in
/// a left to right order:
///
/// 1
/// / \
/// 2 3
/// / \ / \
/// 4 5 6 7
///
/// Will iterate in order 2, 4, 5, 3, 6, 7. The given root element is not
/// included in the traversal.
abstract class _DepthFirstTreeIterator<ItemType> implements Iterator<ItemType> {
_DepthFirstTreeIterator(ItemType root) {
_fillStack(_collectChildren(root));
}
@override
ItemType get current => _current!;
late ItemType _current;
final List<ItemType> _stack = <ItemType>[];
@override
bool moveNext() {
if (_stack.isEmpty) {
return false;
}
_current = _stack.removeLast();
_fillStack(_collectChildren(_current));
return true;
}
/// Fills the stack in such a way that the next element of a depth first
/// traversal is easily and efficiently accessible when calling `moveNext`.
void _fillStack(List<ItemType> children) {
// We reverse the list of children so we don't have to do use expensive
// `insert` or `remove` operations, and so the order of the traversal
// is depth first when built lazily through the iterator.
//
// This is faster than `_stack.addAll(children.reversed)`, presumably since
// we don't actually care about maintaining an iteration pointer.
while (children.isNotEmpty) {
_stack.add(children.removeLast());
}
}
/// Collect the children from [root] in the order they are expected to traverse.
List<ItemType> _collectChildren(ItemType root);
}
/// [Element.visitChildren] does not guarantee order, but does guarantee stable
/// order. This iterator also guarantees stable order, and iterates in a left
/// to right order:
......@@ -37,7 +111,7 @@ Iterable<Element> collectAllElementsFrom(
///
/// Will iterate in order 2, 4, 5, 3, 6, 7.
///
/// Performance is important here because this method is on the critical path
/// Performance is important here because this class is on the critical path
/// for flutter_driver and package:integration_test performance tests.
/// Performance is measured in the all_elements_bench microbenchmark.
/// Any changes to this implementation should check the before and after numbers
......@@ -46,46 +120,37 @@ Iterable<Element> collectAllElementsFrom(
/// If we could use RTL order, we could save on performance, but numerous tests
/// have been written (and developers clearly expect) that LTR order will be
/// respected.
class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement, this.skipOffstage) {
_fillChildren(rootElement);
}
final bool skipOffstage;
class _DepthFirstElementTreeIterator extends _DepthFirstTreeIterator<Element> {
_DepthFirstElementTreeIterator(super.root, this.includeOffstage);
late Element _current;
final List<Element> _stack = <Element>[];
final bool includeOffstage;
@override
Element get current => _current;
@override
bool moveNext() {
if (_stack.isEmpty) {
return false;
List<Element> _collectChildren(Element root) {
final List<Element> children = <Element>[];
if (includeOffstage) {
root.visitChildren(children.add);
} else {
root.debugVisitOnstageChildren(children.add);
}
_current = _stack.removeLast();
_fillChildren(_current);
return true;
return children;
}
}
void _fillChildren(Element element) {
// If we did not have to follow LTR order and could instead use RTL,
// we could avoid reversing this and the operation would be measurably
// faster. Unfortunately, a lot of tests depend on LTR order.
final List<Element> reversed = <Element>[];
if (skipOffstage) {
element.debugVisitOnstageChildren(reversed.add);
} else {
element.visitChildren(reversed.add);
}
// This is faster than _stack.addAll(reversed.reversed), presumably since
// we don't actually care about maintaining an iteration pointer.
while (reversed.isNotEmpty) {
_stack.add(reversed.removeLast());
}
/// Iterates the semantics tree starting at the given `root`.
///
/// This will iterate in the same order expected from accessibility services,
/// so the results can be used to simulate the same traversal the engine will
/// make. The results are not filtered based on flags or visibility, so they
/// will need to be further filtered to fully simulate an accessiblity service.
class _DepthFirstSemanticsTreeIterator extends _DepthFirstTreeIterator<SemanticsNode> {
_DepthFirstSemanticsTreeIterator(super.root, this.order);
final DebugSemanticsDumpOrder order;
@override
List<SemanticsNode> _collectChildren(SemanticsNode root) {
return root.debugListChildrenInOrder(order);
}
}
......@@ -13,7 +13,6 @@ import 'package:matcher/expect.dart' as matcher_expect;
import 'package:meta/meta.dart';
import 'package:test_api/scaffolding.dart' as test_package;
import 'all_elements.dart';
import 'binding.dart';
import 'controller.dart';
import 'finders.dart';
......@@ -23,6 +22,7 @@ import 'test_async_utils.dart';
import 'test_compat.dart';
import 'test_pointer.dart';
import 'test_text_input.dart';
import 'tree_traversal.dart';
// Keep users from needing multiple imports to test semantics.
export 'package:flutter/rendering.dart' show SemanticsHandle;
......@@ -1089,12 +1089,16 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
///
/// Tests that just need to add text to widgets like [TextField]
/// or [TextFormField] only need to call [enterText].
Future<void> showKeyboard(Finder finder) async {
Future<void> showKeyboard(FinderBase<Element> finder) async {
bool skipOffstage = true;
if (finder is Finder) {
skipOffstage = finder.skipOffstage;
}
return TestAsyncUtils.guard<void>(() async {
final EditableTextState editable = state<EditableTextState>(
find.descendant(
of: finder,
matching: find.byType(EditableText, skipOffstage: finder.skipOffstage),
matching: find.byType(EditableText, skipOffstage: skipOffstage),
matchRoot: true,
),
);
......@@ -1124,7 +1128,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
/// that widget has an open connection (e.g. by using [tap] to focus it),
/// then call `testTextInput.enterText` directly (see
/// [TestTextInput.enterText]).
Future<void> enterText(Finder finder, String text) async {
Future<void> enterText(FinderBase<Element> finder, String text) async {
return TestAsyncUtils.guard<void>(() async {
await showKeyboard(finder);
testTextInput.enterText(text);
......
......@@ -8,6 +8,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
const List<Widget> fooBarTexts = <Text>[
Text('foo', textDirection: TextDirection.ltr),
Text('bar', textDirection: TextDirection.ltr),
];
void main() {
group('image', () {
testWidgets('finds Image widgets', (WidgetTester tester) async {
......@@ -390,6 +395,764 @@ void main() {
find.byWidgetPredicate((_) => true).evaluate().length;
expect(find.bySubtype<Widget>(), findsNWidgets(totalWidgetCount));
});
group('find.byElementPredicate', () {
testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
const String customDescription = 'custom description';
late TestFailure failure;
try {
expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(failure.message, contains('Actual: _ElementPredicateWidgetFinder:<Found 0 widgets with $customDescription'));
});
});
group('find.byWidgetPredicate', () {
testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
const String customDescription = 'custom description';
late TestFailure failure;
try {
expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(failure.message, contains('Actual: _WidgetPredicateWidgetFinder:<Found 0 widgets with $customDescription'));
});
});
group('find.descendant', () {
testWidgets('finds one descendant', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.widgetWithText(Row, 'foo'),
matching: find.text('bar'),
), findsOneWidget);
});
testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.widgetWithText(Column, 'foo'),
matching: find.text('bar'),
), findsNWidgets(2));
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]),
Text('bar', textDirection: TextDirection.ltr),
],
));
late TestFailure failure;
try {
expect(find.descendant(
of: find.widgetWithText(Column, 'foo'),
matching: find.text('bar'),
), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(
failure.message,
contains(
'Actual: _DescendantWidgetFinder:<Found 0 widgets with text "bar" descending from widgets with type "Column" that are ancestors of widgets with text "foo"',
),
);
});
});
group('find.ancestor', () {
testWidgets('finds one ancestor', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.ancestor(
of: find.text('bar'),
matching: find.widgetWithText(Row, 'foo'),
), findsOneWidget);
});
testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Row(children: fooBarTexts),
],
),
),
);
expect(find.ancestor(
of: find.text('bar'),
matching: find.byType(Row),
), findsNWidgets(2));
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]),
Text('bar', textDirection: TextDirection.ltr),
],
));
late TestFailure failure;
try {
expect(find.ancestor(
of: find.text('bar'),
matching: find.widgetWithText(Column, 'foo'),
), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(
failure.message,
contains(
'Actual: _AncestorWidgetFinder:<Found 0 widgets with type "Column" that are ancestors of widgets with text "foo" that are ancestors of widgets with text "bar"',
),
);
});
testWidgets('Root not matched by default', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.ancestor(
of: find.byType(Column),
matching: find.widgetWithText(Column, 'foo'),
), findsNothing);
});
testWidgets('Match the root', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.byType(Column),
matching: find.widgetWithText(Column, 'foo'),
matchRoot: true,
), findsOneWidget);
});
testWidgets('is fast in deep tree', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: _deepWidgetTree(
depth: 1000,
child: Row(
children: <Widget>[
_deepWidgetTree(
depth: 1000,
child: const Column(children: fooBarTexts),
),
],
),
),
),
);
expect(find.ancestor(
of: find.text('bar'),
matching: find.byType(Row),
), findsOneWidget);
});
});
group('CommonSemanticsFinders', () {
final Widget semanticsTree = _boilerplate(
Semantics(
container: true,
header: true,
readOnly: true,
onCopy: () {},
onLongPress: () {},
value: 'value1',
hint: 'hint1',
label: 'label1',
child: Semantics(
container: true,
textField: true,
onSetText: (_) { },
onPaste: () { },
onLongPress: () { },
value: 'value2',
hint: 'hint2',
label: 'label2',
child: Semantics(
container: true,
readOnly: true,
onCopy: () {},
value: 'value3',
hint: 'hint3',
label: 'label3',
child: Semantics(
container: true,
readOnly: true,
onLongPress: () { },
value: 'value4',
hint: 'hint4',
label: 'label4',
child: Semantics(
container: true,
onLongPress: () { },
onCopy: () {},
value: 'value5',
hint: 'hint5',
label: 'label5'
),
),
)
),
),
);
group('ancestor', () {
testWidgets('finds matching ancestor nodes', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final FinderBase<SemanticsNode> finder = find.semantics.ancestor(
of: find.semantics.byLabel('label4'),
matching: find.semantics.byAction(SemanticsAction.copy),
);
expect(finder, findsExactly(2));
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final FinderBase<SemanticsNode> finder = find.semantics.ancestor(
of: find.semantics.byLabel('label4'),
matching: find.semantics.byAction(SemanticsAction.copy),
);
try {
expect(finder, findsExactly(3));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _AncestorSemanticsFinder:<Found 2 SemanticsNodes with action "SemanticsAction.copy" that are ancestors of SemanticsNodes with label "label4"'));
});
});
group('descendant', () {
testWidgets('finds matching descendant nodes', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final FinderBase<SemanticsNode> finder = find.semantics.descendant(
of: find.semantics.byLabel('label4'),
matching: find.semantics.byAction(SemanticsAction.copy),
);
expect(finder, findsOne);
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final FinderBase<SemanticsNode> finder = find.semantics.descendant(
of: find.semantics.byLabel('label4'),
matching: find.semantics.byAction(SemanticsAction.copy),
);
try {
expect(finder, findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _DescendantSemanticsFinder:<Found 1 SemanticsNode with action "SemanticsAction.copy" descending from SemanticsNode with label "label4"'));
});
});
group('byPredicate', () {
testWidgets('finds nodes matching given predicate', (WidgetTester tester) async {
final RegExp replaceRegExp = RegExp(r'^[^\d]+');
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byPredicate(
(SemanticsNode node) {
final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1;
return labelNum > 1;
},
);
expect(finder, findsExactly(4));
});
testWidgets('fails with default message', (WidgetTester tester) async {
late TestFailure failure;
final RegExp replaceRegExp = RegExp(r'^[^\d]+');
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byPredicate(
(SemanticsNode node) {
final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1;
return labelNum > 1;
},
);
try {
expect(finder, findsExactly(5));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 matching semantics predicate'));
});
testWidgets('fails with given message', (WidgetTester tester) async {
late TestFailure failure;
const String expected = 'custom error message';
final RegExp replaceRegExp = RegExp(r'^[^\d]+');
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byPredicate(
(SemanticsNode node) {
final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1;
return labelNum > 1;
},
describeMatch: (_) => expected,
);
try {
expect(finder, findsExactly(5));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains(expected));
});
});
group('byLabel', () {
testWidgets('finds nodes with matching label using String', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byLabel('label3');
expect(finder, findsOne);
expect(finder.found.first.label, 'label3');
});
testWidgets('finds nodes with matching label using RegEx', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byLabel(RegExp('^label.*'));
expect(finder, findsExactly(5));
expect(finder.found.every((SemanticsNode node) => node.label.startsWith('label')), isTrue);
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byLabel('label3');
try {
expect(finder, findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with label "label3"'));
});
});
group('byValue', () {
testWidgets('finds nodes with matching value using String', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byValue('value3');
expect(finder, findsOne);
expect(finder.found.first.value, 'value3');
});
testWidgets('finds nodes with matching value using RegEx', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byValue(RegExp('^value.*'));
expect(finder, findsExactly(5));
expect(finder.found.every((SemanticsNode node) => node.value.startsWith('value')), isTrue);
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byValue('value3');
try {
expect(finder, findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with value "value3"'));
});
});
group('byHint', () {
testWidgets('finds nodes with matching hint using String', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byHint('hint3');
expect(finder, findsOne);
expect(finder.found.first.hint, 'hint3');
});
testWidgets('finds nodes with matching hint using RegEx', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byHint(RegExp('^hint.*'));
expect(finder, findsExactly(5));
expect(finder.found.every((SemanticsNode node) => node.hint.startsWith('hint')), isTrue);
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byHint('hint3');
try {
expect(finder, findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with hint "hint3"'));
});
});
group('byAction', () {
testWidgets('finds nodes with matching action', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy);
expect(finder, findsExactly(3));
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy);
try {
expect(finder, findsExactly(4));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 3 SemanticsNodes with action "SemanticsAction.copy"'));
});
});
group('byAnyAction', () {
testWidgets('finds nodes with any matching actions', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[
SemanticsAction.paste,
SemanticsAction.longPress,
]);
expect(finder, findsExactly(4));
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[
SemanticsAction.paste,
SemanticsAction.longPress,
]);
try {
expect(finder, findsExactly(5));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 SemanticsNodes with any of the following actions: [SemanticsAction.paste, SemanticsAction.longPress]:'));
});
});
group('byFlag', () {
testWidgets('finds nodes with matching flag', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly);
expect(finder, findsExactly(3));
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly);
try {
expect(finder, findsExactly(4));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('_PredicateSemanticsFinder:<Found 3 SemanticsNodes with flag "SemanticsFlag.isReadOnly":'));
});
});
group('byAnyFlag', () {
testWidgets('finds nodes with any matching flag', (WidgetTester tester) async {
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[
SemanticsFlag.isHeader,
SemanticsFlag.isTextField,
]);
expect(finder, findsExactly(2));
});
testWidgets('fails with descriptive message', (WidgetTester tester) async {
late TestFailure failure;
await tester.pumpWidget(semanticsTree);
final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[
SemanticsFlag.isHeader,
SemanticsFlag.isTextField,
]);
try {
expect(finder, findsExactly(3));
} on TestFailure catch (e) {
failure = e;
}
expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 2 SemanticsNodes with any of the following flags: [SemanticsFlag.isHeader, SemanticsFlag.isTextField]:'));
});
});
});
group('FinderBase', () {
group('describeMatch', () {
test('is used for Finder and results', () {
const String expected = 'Fake finder describe match';
final _FakeFinder finder = _FakeFinder(describeMatchCallback: (_) {
return expected;
});
expect(finder.evaluate().toString(), contains(expected));
expect(finder.toString(describeSelf: true), contains(expected));
});
for (int i = 0; i < 4; i++) {
test('gets expected plurality for $i when reporting results from find', () {
final Plurality expected = switch (i) {
0 => Plurality.zero,
1 => Plurality.one,
_ => Plurality.many,
};
late final Plurality actual;
final _FakeFinder finder = _FakeFinder(
describeMatchCallback: (Plurality plurality) {
actual = plurality;
return 'Fake description';
},
findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()),
);
finder.evaluate().toString();
expect(actual, expected);
});
test('gets expected plurality for $i when reporting results from toString', () {
final Plurality expected = switch (i) {
0 => Plurality.zero,
1 => Plurality.one,
_ => Plurality.many,
};
late final Plurality actual;
final _FakeFinder finder = _FakeFinder(
describeMatchCallback: (Plurality plurality) {
actual = plurality;
return 'Fake description';
},
findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()),
);
finder.toString();
expect(actual, expected);
});
test('always gets many when describing finder', () {
const Plurality expected = Plurality.many;
late final Plurality actual;
final _FakeFinder finder = _FakeFinder(
describeMatchCallback: (Plurality plurality) {
actual = plurality;
return 'Fake description';
},
findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()),
);
finder.toString(describeSelf: true);
expect(actual, expected);
});
}
});
test('findInCandidates gets allCandidates', () {
final List<String> expected = <String>['Test1', 'Test2', 'Test3', 'Test4'];
late final List<String> actual;
final _FakeFinder finder = _FakeFinder(
allCandidatesCallback: () => expected,
findInCandidatesCallback: (Iterable<String> candidates) {
actual = candidates.toList();
return candidates;
},
);
finder.evaluate();
expect(actual, expected);
});
test('allCandidates calculated for each find', () {
const int expectedCallCount = 3;
int actualCallCount = 0;
final _FakeFinder finder = _FakeFinder(
allCandidatesCallback: () {
actualCallCount++;
return <String>['test'];
},
);
for (int i = 0; i < expectedCallCount; i++) {
finder.evaluate();
}
expect(actualCallCount, expectedCallCount);
});
test('allCandidates only called once while caching', () {
int actualCallCount = 0;
final _FakeFinder finder = _FakeFinder(
allCandidatesCallback: () {
actualCallCount++;
return <String>['test'];
},
);
finder.runCached(() {
for (int i = 0; i < 5; i++) {
finder.evaluate();
finder.tryEvaluate();
final FinderResult<String> _ = finder.found;
}
});
expect(actualCallCount, 1);
});
group('tryFind', () {
test('returns false if no results', () {
final _FakeFinder finder = _FakeFinder(
findInCandidatesCallback: (_) => <String>[],
);
expect(finder.tryEvaluate(), false);
});
test('returns true if results are available', () {
final _FakeFinder finder = _FakeFinder(
findInCandidatesCallback: (_) => <String>['Results'],
);
expect(finder.tryEvaluate(), true);
});
});
group('found', () {
test('throws before any calls to evaluate or tryEvaluate', () {
final _FakeFinder finder = _FakeFinder();
expect(finder.hasFound, false);
expect(() => finder.found, throwsAssertionError);
});
test('has same results as evaluate after call to evaluate', () {
final _FakeFinder finder = _FakeFinder();
final FinderResult<String> expected = finder.evaluate();
expect(finder.hasFound, true);
expect(finder.found, expected);
});
test('has expected results after call to tryFind', () {
final Iterable<String> expected = Iterable<String>.generate(10, (int i) => i.toString());
final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => expected);
finder.tryEvaluate();
expect(finder.hasFound, true);
expect(finder.found, orderedEquals(expected));
});
});
});
}
Widget _boilerplate(Widget child) {
......@@ -442,3 +1205,45 @@ class SimpleGenericWidget<T> extends StatelessWidget {
return _child;
}
}
/// Wraps [child] in [depth] layers of [SizedBox]
Widget _deepWidgetTree({required int depth, required Widget child}) {
Widget tree = child;
for (int i = 0; i < depth; i += 1) {
tree = SizedBox(child: tree);
}
return tree;
}
class _FakeFinder extends FinderBase<String> {
_FakeFinder({
this.allCandidatesCallback,
this.describeMatchCallback,
this.findInCandidatesCallback,
});
final Iterable<String> Function()? allCandidatesCallback;
final DescribeMatchCallback? describeMatchCallback;
final Iterable<String> Function(Iterable<String> candidates)? findInCandidatesCallback;
@override
Iterable<String> get allCandidates {
return allCandidatesCallback?.call() ?? <String>[
'String 1', 'String 2', 'String 3',
];
}
@override
String describeMatch(Plurality plurality) {
return describeMatchCallback?.call(plurality) ?? switch (plurality) {
Plurality.one => 'String',
Plurality.many || Plurality.zero => 'Strings',
};
}
@override
Iterable<String> findInCandidates(Iterable<String> candidates) {
return findInCandidatesCallback?.call(candidates) ?? candidates;
}
}
......@@ -1330,6 +1330,72 @@ void main() {
expect(find.byType(Text), isNot(findsAtLeastNWidgets(3)));
});
});
group('findsOneWidget', () {
testWidgets('finds exactly one widget', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
expect(find.text('foo'), findsOneWidget);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: exactly one matching candidate\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 0 widgets with text "foo"'));
expect(message, contains('Which: means none were found but one was expected\n'));
});
});
group('findsNothing', () {
testWidgets('finds no widgets', (WidgetTester tester) async {
expect(find.text('foo'), findsNothing);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching candidates\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching candidates\n'));
expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
});
}
enum _ComparatorBehavior {
......
......@@ -16,11 +16,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:matcher/expect.dart' as matcher;
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
const List<Widget> fooBarTexts = <Text>[
Text('foo', textDirection: TextDirection.ltr),
Text('bar', textDirection: TextDirection.ltr),
];
void main() {
group('expectLater', () {
testWidgets('completes when matcher completes', (WidgetTester tester) async {
......@@ -75,70 +70,6 @@ void main() {
});
}, skip: true); // [intended] API testing
group('findsOneWidget', () {
testWidgets('finds exactly one widget', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
expect(find.text('foo'), findsOneWidget);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: exactly one matching node in the widget tree\n'));
expect(message, contains('Actual: _TextFinder:<zero widgets with text "foo">\n'));
expect(message, contains('Which: means none were found but one was expected\n'));
});
});
group('findsNothing', () {
testWidgets('finds no widgets', (WidgetTester tester) async {
expect(find.text('foo'), findsNothing);
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo', skipOffstage: false), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo": Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
late TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
final String? message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr, dependencies: [MediaQuery])>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
});
group('pumping', () {
testWidgets('pumping', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
......@@ -196,215 +127,6 @@ void main() {
expect(logPaints, <int>[60000, 70000, 80000]);
});
});
group('find.byElementPredicate', () {
testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
const String customDescription = 'custom description';
late TestFailure failure;
try {
expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(failure.message, contains('Actual: _ElementPredicateFinder:<zero widgets with $customDescription'));
});
});
group('find.byWidgetPredicate', () {
testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
const String customDescription = 'custom description';
late TestFailure failure;
try {
expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(failure.message, contains('Actual: _WidgetPredicateFinder:<zero widgets with $customDescription'));
});
});
group('find.descendant', () {
testWidgets('finds one descendant', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.widgetWithText(Row, 'foo'),
matching: find.text('bar'),
), findsOneWidget);
});
testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.widgetWithText(Column, 'foo'),
matching: find.text('bar'),
), findsNWidgets(2));
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]),
Text('bar', textDirection: TextDirection.ltr),
],
));
late TestFailure failure;
try {
expect(find.descendant(
of: find.widgetWithText(Column, 'foo'),
matching: find.text('bar'),
), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(
failure.message,
contains(
'Actual: _DescendantFinder:<zero widgets with text "bar" that has ancestor(s) with type "Column" which is an ancestor of text "foo"',
),
);
});
});
group('find.ancestor', () {
testWidgets('finds one ancestor', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.ancestor(
of: find.text('bar'),
matching: find.widgetWithText(Row, 'foo'),
), findsOneWidget);
});
testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Row(children: fooBarTexts),
],
),
),
);
expect(find.ancestor(
of: find.text('bar'),
matching: find.byType(Row),
), findsNWidgets(2));
});
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]),
Text('bar', textDirection: TextDirection.ltr),
],
));
late TestFailure failure;
try {
expect(find.ancestor(
of: find.text('bar'),
matching: find.widgetWithText(Column, 'foo'),
), findsOneWidget);
} on TestFailure catch (e) {
failure = e;
}
expect(failure, isNotNull);
expect(
failure.message,
contains(
'Actual: _AncestorFinder:<zero widgets with type "Column" which is an ancestor of text "foo" which is an ancestor of text "bar"',
),
);
});
testWidgets('Root not matched by default', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.ancestor(
of: find.byType(Column),
matching: find.widgetWithText(Column, 'foo'),
), findsNothing);
});
testWidgets('Match the root', (WidgetTester tester) async {
await tester.pumpWidget(const Row(
textDirection: TextDirection.ltr,
children: <Widget>[
Column(children: fooBarTexts),
],
));
expect(find.descendant(
of: find.byType(Column),
matching: find.widgetWithText(Column, 'foo'),
matchRoot: true,
), findsOneWidget);
});
testWidgets('is fast in deep tree', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: _deepWidgetTree(
depth: 1000,
child: Row(
children: <Widget>[
_deepWidgetTree(
depth: 1000,
child: const Column(children: fooBarTexts),
),
],
),
),
),
);
expect(find.ancestor(
of: find.text('bar'),
matching: find.byType(Row),
), findsOneWidget);
});
});
group('pageBack', () {
testWidgets('fails when there are no back buttons', (WidgetTester tester) async {
await tester.pumpWidget(Container());
......@@ -985,12 +707,3 @@ class _AlwaysRepaint extends CustomPainter {
onPaint();
}
}
/// Wraps [child] in [depth] layers of [SizedBox]
Widget _deepWidgetTree({required int depth, required Widget child}) {
Widget tree = child;
for (int i = 0; i < depth; i += 1) {
tree = SizedBox(child: tree);
}
return tree;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment