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
This commit is contained in:
pdblasi-google 2023-08-10 14:31:06 -07:00 committed by GitHub
parent 73e0dbf5f4
commit 5df1c996ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2046 additions and 718 deletions

View File

@ -126,11 +126,11 @@ class _LiveWidgetController extends LiveWidgetController {
} }
/// Runs `finder` repeatedly until it finds one or more [Element]s. /// 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) { if (frameSync) {
await _waitUntilFrame(() => binding.transientCallbackCount == 0); await _waitUntilFrame(() => binding.transientCallbackCount == 0);
} }
await _waitUntilFrame(() => finder.precache()); await _waitUntilFrame(() => finder.tryEvaluate());
if (frameSync) { if (frameSync) {
await _waitUntilFrame(() => binding.transientCallbackCount == 0); await _waitUntilFrame(() => binding.transientCallbackCount == 0);
} }
@ -138,12 +138,12 @@ class _LiveWidgetController extends LiveWidgetController {
} }
@override @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); await super.tap(await _waitForElement(finder), pointer: pointer, buttons: buttons, warnIfMissed: warnIfMissed);
} }
Future<void> scrollIntoView(Finder finder, {required double alignment}) async { Future<void> scrollIntoView(FinderBase<Element> finder, {required double alignment}) async {
final Finder target = await _waitForElement(finder); final FinderBase<Element> target = await _waitForElement(finder);
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment); await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment);
} }
} }

View File

@ -56,7 +56,7 @@ Future<void> main() async {
do { do {
await controller.drag(list, const Offset(0.0, -30.0)); await controller.drag(list, const Offset(0.0, -30.0));
await Future<void>.delayed(const Duration(milliseconds: 20)); await Future<void>.delayed(const Duration(milliseconds: 20));
} while (!lastItem.precache()); } while (!lastItem.tryEvaluate());
debugPrint('==== MEMORY BENCHMARK ==== DONE ===='); debugPrint('==== MEMORY BENCHMARK ==== DONE ====');
} }

View File

@ -60,7 +60,7 @@ Future<void> main() async {
do { do {
await controller.drag(demoList, const Offset(0.0, -300.0)); await controller.drag(demoList, const Offset(0.0, -300.0));
await Future<void>.delayed(const Duration(milliseconds: 20)); 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 // Ensure that the center of the "Text fields" item is visible
// because that's where we're going to tap // because that's where we're going to tap

View File

@ -15,13 +15,14 @@ const List<Widget> children = <Widget>[
void expectRects(WidgetTester tester, List<Rect> expected) { void expectRects(WidgetTester tester, List<Rect> expected) {
final Finder finder = find.byType(SizedBox); final Finder finder = find.byType(SizedBox);
finder.precache();
final List<Rect> actual = <Rect>[]; final List<Rect> actual = <Rect>[];
for (int i = 0; i < expected.length; ++i) { finder.runCached(() {
final Finder current = finder.at(i); for (int i = 0; i < expected.length; ++i) {
expect(current, findsOneWidget); final Finder current = finder.at(i);
actual.add(tester.getRect(finder.at(i))); expect(current, findsOneWidget);
} actual.add(tester.getRect(finder.at(i)));
}
});
expect(() => finder.at(expected.length), throwsRangeError); expect(() => finder.at(expected.length), throwsRangeError);
expect(actual, equals(expected)); expect(actual, equals(expected));
} }

View File

@ -58,7 +58,6 @@ export 'dart:async' show Future;
export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; 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/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart';
export 'src/accessibility.dart'; export 'src/accessibility.dart';
export 'src/all_elements.dart';
export 'src/animation_sheet.dart'; export 'src/animation_sheet.dart';
export 'src/binding.dart'; export 'src/binding.dart';
export 'src/controller.dart'; export 'src/controller.dart';
@ -83,5 +82,6 @@ export 'src/test_exception_reporter.dart';
export 'src/test_pointer.dart'; export 'src/test_pointer.dart';
export 'src/test_text_input.dart'; export 'src/test_text_input.dart';
export 'src/test_vsync.dart'; export 'src/test_vsync.dart';
export 'src/tree_traversal.dart';
export 'src/widget_tester.dart'; export 'src/widget_tester.dart';
export 'src/window.dart'; export 'src/window.dart';

View File

@ -1,91 +0,0 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
/// Provides an iterable that efficiently returns all the elements
/// rooted at the given element. See [CachingIterable] for details.
///
/// This method 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.
///
/// 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<Element> collectAllElementsFrom(
Element rootElement, {
required bool skipOffstage,
}) {
return CachingIterable<Element>(_DepthFirstChildIterator(rootElement, skipOffstage));
}
/// Provides a recursive, efficient, depth first search of an element tree.
///
/// [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:
///
/// 1
/// / \
/// 2 3
/// / \ / \
/// 4 5 6 7
///
/// Will iterate in order 2, 4, 5, 3, 6, 7.
///
/// Performance is important here because this method 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
/// on that benchmark to avoid regressions in general performance test overhead.
///
/// 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;
late Element _current;
final List<Element> _stack = <Element>[];
@override
Element get current => _current;
@override
bool moveNext() {
if (_stack.isEmpty) {
return false;
}
_current = _stack.removeLast();
_fillChildren(_current);
return true;
}
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());
}
}
}

View File

@ -9,11 +9,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'all_elements.dart';
import 'event_simulation.dart'; import 'event_simulation.dart';
import 'finders.dart'; import 'finders.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_pointer.dart'; import 'test_pointer.dart';
import 'tree_traversal.dart';
import 'window.dart'; import 'window.dart';
/// The default drag touch slop used to break up a large drag into multiple /// 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 /// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled. /// if no semantics are found or are not enabled.
SemanticsNode find(Finder finder) { SemanticsNode find(FinderBase<Element> finder) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
if (!_controller.binding.semanticsEnabled) { if (!_controller.binding.semanticsEnabled) {
throw StateError('Semantics are not enabled.'); throw StateError('Semantics are not enabled.');
@ -149,7 +149,7 @@ class SemanticsController {
/// parts of the traversal. /// parts of the traversal.
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
/// match the order of the traversal. /// 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(); TestAsyncUtils.guardSync();
FlutterView? startView; FlutterView? startView;
FlutterView? endView; FlutterView? endView;
@ -158,7 +158,7 @@ class SemanticsController {
if (view != null && startView != view) { if (view != null && startView != view) {
throw StateError( throw StateError(
'The start node is not part of the provided view.\n' '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' 'View of start node: $startView\n'
'Specified view: $view' 'Specified view: $view'
); );
@ -169,7 +169,7 @@ class SemanticsController {
if (view != null && endView != view) { if (view != null && endView != view) {
throw StateError( throw StateError(
'The end node is not part of the provided view.\n' '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' 'View of end node: $endView\n'
'Specified view: $view' 'Specified view: $view'
); );
@ -178,8 +178,8 @@ class SemanticsController {
if (endView != null && startView != null && endView != startView) { if (endView != null && startView != null && endView != startView) {
throw StateError( throw StateError(
'The start and end node are in different views.\n' 'The start and end node are in different views.\n'
'Start finder: ${start!.description}\n' 'Start finder: ${start!.toString(describeSelf: true)}\n'
'End finder: ${end!.description}\n' 'End finder: ${end!.toString(describeSelf: true)}\n'
'View of start node: $startView\n' 'View of start node: $startView\n'
'View of end node: $endView' 'View of end node: $endView'
); );
@ -200,7 +200,7 @@ class SemanticsController {
if (startIndex == -1) { if (startIndex == -1) {
throw StateError( throw StateError(
'The expected starting node was not found.\n' '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' 'Expected Start Node: $startNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]'); 'Traversal: [\n ${traversal.join('\n ')}\n]');
} }
@ -212,7 +212,7 @@ class SemanticsController {
if (endIndex == -1) { if (endIndex == -1) {
throw StateError( throw StateError(
'The expected ending node was not found.\n' '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' 'Expected End Node: $endNode\n\n'
'Traversal: [\n ${traversal.join('\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] which returns the [TestFlutterView] used when only a single
/// view is being used. /// view is being used.
TestFlutterView viewOf(Finder finder) { TestFlutterView viewOf(FinderBase<Element> finder) {
return _viewOf(finder) as TestFlutterView; return _viewOf(finder) as TestFlutterView;
} }
FlutterView _viewOf(Finder finder) { FlutterView _viewOf(FinderBase<Element> finder) {
return firstWidget<View>( return firstWidget<View>(
find.ancestor( find.ancestor(
of: finder, of: finder,
@ -356,7 +356,7 @@ abstract class WidgetController {
} }
/// Checks if `finder` exists in the tree. /// Checks if `finder` exists in the tree.
bool any(Finder finder) { bool any(FinderBase<Element> finder) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
return finder.evaluate().isNotEmpty; 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 [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. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().single.widget as T; return finder.evaluate().single.widget as T;
} }
@ -388,7 +388,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty. /// Throws a [StateError] if `finder` is empty.
/// ///
/// * Use [widget] if you only expect to match one widget. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().first.widget as T; 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 [widget] if you only expect to match one widget.
/// * Use [firstWidget] if you expect to match several but only want the first. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) { return finder.evaluate().map<T>((Element element) {
final T result = element.widget as T; final T result = element.widget as T;
@ -408,7 +408,7 @@ abstract class WidgetController {
/// Find all layers that are children of the provided [finder]. /// Find all layers that are children of the provided [finder].
/// ///
/// The [finder] must match exactly one element. /// The [finder] must match exactly one element.
Iterable<Layer> layerListOf(Finder finder) { Iterable<Layer> layerListOf(FinderBase<Element> finder) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
final Element element = finder.evaluate().single; final Element element = finder.evaluate().single;
final RenderObject object = element.renderObject!; 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 [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. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().single as T; return finder.evaluate().single as T;
} }
@ -448,7 +448,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty. /// Throws a [StateError] if `finder` is empty.
/// ///
/// * Use [element] if you only expect to match one element. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().first as T; return finder.evaluate().first as T;
} }
@ -457,7 +457,7 @@ abstract class WidgetController {
/// ///
/// * Use [element] if you only expect to match one element. /// * Use [element] if you only expect to match one element.
/// * Use [firstElement] if you expect to match several but only want the first. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().cast<T>(); 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 [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. /// * 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(); TestAsyncUtils.guardSync();
return _stateOf<T>(finder.evaluate().single, finder); return _stateOf<T>(finder.evaluate().single, finder);
} }
@ -491,7 +491,7 @@ abstract class WidgetController {
/// matching widget has no state. /// matching widget has no state.
/// ///
/// * Use [state] if you only expect to match one 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(); TestAsyncUtils.guardSync();
return _stateOf<T>(finder.evaluate().first, finder); 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 [state] if you only expect to match one state.
/// * Use [firstState] if you expect to match several but only want the first. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder)); 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(); TestAsyncUtils.guardSync();
if (element is StatefulElement) { if (element is StatefulElement) {
return element.state as T; 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 /// 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 [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. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().single.renderObject! as T; return finder.evaluate().single.renderObject! as T;
} }
@ -546,7 +546,7 @@ abstract class WidgetController {
/// Throws a [StateError] if `finder` is empty. /// Throws a [StateError] if `finder` is empty.
/// ///
/// * Use [renderObject] if you only expect to match one render object. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().first.renderObject! as T; 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 [renderObject] if you only expect to match one render object.
/// * Use [firstRenderObject] if you expect to match several but only want the first. /// * 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(); TestAsyncUtils.guardSync();
return finder.evaluate().map<T>((Element element) { return finder.evaluate().map<T>((Element element) {
final T result = element.renderObject! as T; 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 /// For example, a test that verifies that tapping a disabled button does not
/// trigger the button would set `warnIfMissed` to false, because the button /// trigger the button would set `warnIfMissed` to false, because the button
/// would ignore the tap. /// 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); 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. /// * [tap], which presses and releases a pointer at the given location.
/// * [longPress], which presses and releases a pointer with a gap in /// * [longPress], which presses and releases a pointer with a gap in
/// between long enough to trigger the long-press gesture. /// 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 TestAsyncUtils.guard<TestGesture>(() {
return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons); 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) /// later verify that long-pressing the same location (using the same finder)
/// has no effect (since the widget is now obscured), setting `warnIfMissed` /// has no effect (since the widget is now obscured), setting `warnIfMissed`
/// to false on that second call. /// 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); 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 /// 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]. /// just want to drag and end without a fling, use [drag].
Future<void> fling( Future<void> fling(
Finder finder, FinderBase<Element> finder,
Offset offset, Offset offset,
double speed, { double speed, {
int? pointer, int? pointer,
@ -787,7 +787,7 @@ abstract class WidgetController {
/// A fling is essentially a drag that ends at a particular speed. If you /// 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]. /// just want to drag and end without a fling, use [drag].
Future<void> trackpadFling( Future<void> trackpadFling(
Finder finder, FinderBase<Element> finder,
Offset offset, Offset offset,
double speed, { double speed, {
int? pointer, int? pointer,
@ -952,7 +952,7 @@ abstract class WidgetController {
/// should be left to their default values. /// should be left to their default values.
/// {@endtemplate} /// {@endtemplate}
Future<void> drag( Future<void> drag(
Finder finder, FinderBase<Element> finder,
Offset offset, { Offset offset, {
int? pointer, int? pointer,
int buttons = kPrimaryButton, int buttons = kPrimaryButton,
@ -1085,7 +1085,7 @@ abstract class WidgetController {
/// more accurate time control. /// more accurate time control.
/// {@endtemplate} /// {@endtemplate}
Future<void> timedDrag( Future<void> timedDrag(
Finder finder, FinderBase<Element> finder,
Offset offset, Offset offset,
Duration duration, { Duration duration, {
int? pointer, int? pointer,
@ -1282,14 +1282,14 @@ abstract class WidgetController {
/// this method is being called from another that is forwarding its own /// this method is being called from another that is forwarding its own
/// `warnIfMissed` parameter (see e.g. the implementation of [tap]). /// `warnIfMissed` parameter (see e.g. the implementation of [tap]).
/// {@endtemplate} /// {@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); return _getElementPoint(finder, (Size size) => size.center(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
} }
/// Returns the point at the top left of the given widget. /// Returns the point at the top left of the given widget.
/// ///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} /// {@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); 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. /// point is not inside the object's hit test area.
/// ///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} /// {@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); 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. /// point is not inside the object's hit test area.
/// ///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} /// {@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); 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. /// point is not inside the object's hit test area.
/// ///
/// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed} /// {@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); 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. /// in the documentation for the [flutter_test] library.
static bool hitTestWarningShouldBeFatal = false; 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(); TestAsyncUtils.guardSync();
final Iterable<Element> elements = finder.evaluate(); final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) { if (elements.isEmpty) {
@ -1411,7 +1411,7 @@ abstract class WidgetController {
/// Returns the size of the given widget. This is only valid once /// Returns the size of the given widget. This is only valid once
/// the widget's render object has been laid out at least once. /// the widget's render object has been laid out at least once.
Size getSize(Finder finder) { Size getSize(FinderBase<Element> finder) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
final Element element = finder.evaluate().single; final Element element = finder.evaluate().single;
final RenderBox box = element.renderObject! as RenderBox; 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 /// Returns the rect of the given widget. This is only valid once
/// the widget's render object has been laid out at least 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`. /// 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 /// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled. /// 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. // 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]. /// 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 /// * [Scrollable.ensureVisible], which is the production API used to
/// implement this method. /// 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 /// Repeatedly scrolls a [Scrollable] by `delta` in the
/// [Scrollable.axisDirection] direction until a widget matching `finder` is /// [Scrollable.axisDirection] direction until a widget matching `finder` is
@ -1645,9 +1645,9 @@ abstract class WidgetController {
/// ///
/// * [dragUntilVisible], which implements the body of this method. /// * [dragUntilVisible], which implements the body of this method.
Future<void> scrollUntilVisible( Future<void> scrollUntilVisible(
Finder finder, FinderBase<Element> finder,
double delta, { double delta, {
Finder? scrollable, FinderBase<Element>? scrollable,
int maxScrolls = 50, int maxScrolls = 50,
Duration duration = const Duration(milliseconds: 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 /// * [scrollUntilVisible], which wraps this method with an API that is more
/// convenient when dealing with a [Scrollable]. /// convenient when dealing with a [Scrollable].
Future<void> dragUntilVisible( Future<void> dragUntilVisible(
Finder finder, FinderBase<Element> finder,
Finder view, FinderBase<Element> view,
Offset moveStep, { Offset moveStep, {
int maxIteration = 50, int maxIteration = 50,
Duration duration = const Duration(milliseconds: 50), Duration duration = const Duration(milliseconds: 50),

File diff suppressed because it is too large Load Diff

View File

@ -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 '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
import 'accessibility.dart'; import 'accessibility.dart';
import 'binding.dart'; import 'binding.dart';
import 'controller.dart';
import 'finders.dart'; import 'finders.dart';
import 'goldens.dart'; import 'goldens.dart';
import 'widget_tester.dart' show WidgetTester; 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 /// ## Sample code
/// ///
@ -30,14 +31,16 @@ import 'widget_tester.dart' show WidgetTester;
/// ///
/// See also: /// See also:
/// ///
/// * [findsWidgets], when you want the finder to find one or more widgets. /// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOneWidget], when you want the finder to find exactly one widget. /// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets. /// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsNothing = _FindsWidgetMatcher(null, 0); const Matcher findsNothing = _FindsCountMatcher(null, 0);
/// Asserts that the [Finder] locates at least one widget in the widget tree. /// Asserts that the [Finder] locates at least one widget in the widget tree.
/// ///
/// This is equivalent to the preferred [findsAny] method.
///
/// ## Sample code /// ## Sample code
/// ///
/// ```dart /// ```dart
@ -47,13 +50,31 @@ const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
/// See also: /// See also:
/// ///
/// * [findsNothing], when you want the finder to not find anything. /// * [findsNothing], when you want the finder to not find anything.
/// * [findsOneWidget], when you want the finder to find exactly one widget. /// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets. /// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsWidgets = _FindsWidgetMatcher(1, null); 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. /// Asserts that the [Finder] locates at exactly one widget in the widget tree.
/// ///
/// This is equivalent to the preferred [findsOne] method.
///
/// ## Sample code /// ## Sample code
/// ///
/// ```dart /// ```dart
@ -63,13 +84,31 @@ const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
/// See also: /// See also:
/// ///
/// * [findsNothing], when you want the finder to not find anything. /// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets. /// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets. /// * [findsExactly], when you want the finder to find a specific number of candidates.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1); 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. /// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
/// ///
/// This is equivalent to the preferred [findsExactly] method.
///
/// ## Sample code /// ## Sample code
/// ///
/// ```dart /// ```dart
@ -79,13 +118,31 @@ const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
/// See also: /// See also:
/// ///
/// * [findsNothing], when you want the finder to not find anything. /// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets. /// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOneWidget], when you want the finder to find exactly one widget. /// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets. /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates.
Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n); 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. /// 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 /// ## Sample code
/// ///
/// ```dart /// ```dart
@ -95,10 +152,26 @@ Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
/// See also: /// See also:
/// ///
/// * [findsNothing], when you want the finder to not find anything. /// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets. /// * [findsAny], when you want the finder to find one or more candidates.
/// * [findsOneWidget], when you want the finder to find exactly one widget. /// * [findsOne], when you want the finder to find exactly one candidate.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets. /// * [findsExactly], when you want the finder to find a specific number of candidates.
Matcher findsAtLeastNWidgets(int n) => _FindsWidgetMatcher(n, null); 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 /// Asserts that the [Finder] locates a single widget that has at
/// least one [Offstage] widget ancestor. /// least one [Offstage] widget ancestor.
@ -527,7 +600,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
/// ///
/// See also: /// 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. /// * [containsSemantics], a similar matcher without default values for flags or actions.
Matcher matchesSemantics({ Matcher matchesSemantics({
String? label, String? label,
@ -707,7 +780,7 @@ Matcher matchesSemantics({
/// ///
/// See also: /// 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. /// * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({ Matcher containsSemantics({
String? label, String? label,
@ -900,19 +973,19 @@ AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
return _DoesNotMatchAccessibilityGuideline(guideline); return _DoesNotMatchAccessibilityGuideline(guideline);
} }
class _FindsWidgetMatcher extends Matcher { class _FindsCountMatcher extends Matcher {
const _FindsWidgetMatcher(this.min, this.max); const _FindsCountMatcher(this.min, this.max);
final int? min; final int? min;
final int? max; final int? max;
@override @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);
assert(min == null || max == null || min! <= max!); assert(min == null || max == null || min! <= max!);
matchState[Finder] = finder; matchState[FinderBase] = finder;
int count = 0; int count = 0;
final Iterator<Element> iterator = finder.evaluate().iterator; final Iterator<dynamic> iterator = finder.evaluate().iterator;
if (min != null) { if (min != null) {
while (count < min! && iterator.moveNext()) { while (count < min! && iterator.moveNext()) {
count += 1; count += 1;
@ -937,26 +1010,26 @@ class _FindsWidgetMatcher extends Matcher {
assert(min != null || max != null); assert(min != null || max != null);
if (min == max) { if (min == max) {
if (min == 1) { 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 (min == null) {
if (max == 0) { if (max == 0) {
return description.add('no matching nodes in the widget tree'); return description.add('no matching candidates');
} }
if (max == 1) { 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 (max == null) {
if (min == 1) { 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 @override
@ -966,8 +1039,8 @@ class _FindsWidgetMatcher extends Matcher {
Map<dynamic, dynamic> matchState, Map<dynamic, dynamic> matchState,
bool verbose, bool verbose,
) { ) {
final Finder finder = matchState[Finder] as Finder; final FinderBase<dynamic> finder = matchState[FinderBase] as FinderBase<dynamic>;
final int count = finder.evaluate().length; final int count = finder.found.length;
if (count == 0) { if (count == 0) {
assert(min != null && min! > 0); assert(min != null && min! > 0);
if (min == 1 && max == 1) { if (min == 1 && max == 1) {

View File

@ -0,0 +1,156 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/widgets.dart';
/// Provides an iterable that efficiently returns all the [Element]s
/// rooted at the given [Element]. See [CachingIterable] for details.
///
/// 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.
///
/// 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<Element> collectAllElementsFrom(
Element rootElement, {
required bool skipOffstage,
}) {
return CachingIterable<Element>(_DepthFirstElementTreeIterator(rootElement, !skipOffstage));
}
/// 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:
///
/// 1
/// / \
/// 2 3
/// / \ / \
/// 4 5 6 7
///
/// Will iterate in order 2, 4, 5, 3, 6, 7.
///
/// 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
/// on that benchmark to avoid regressions in general performance test overhead.
///
/// 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 _DepthFirstElementTreeIterator extends _DepthFirstTreeIterator<Element> {
_DepthFirstElementTreeIterator(super.root, this.includeOffstage);
final bool includeOffstage;
@override
List<Element> _collectChildren(Element root) {
final List<Element> children = <Element>[];
if (includeOffstage) {
root.visitChildren(children.add);
} else {
root.debugVisitOnstageChildren(children.add);
}
return children;
}
}
/// 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);
}
}

View File

@ -13,7 +13,6 @@ import 'package:matcher/expect.dart' as matcher_expect;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:test_api/scaffolding.dart' as test_package; import 'package:test_api/scaffolding.dart' as test_package;
import 'all_elements.dart';
import 'binding.dart'; import 'binding.dart';
import 'controller.dart'; import 'controller.dart';
import 'finders.dart'; import 'finders.dart';
@ -23,6 +22,7 @@ import 'test_async_utils.dart';
import 'test_compat.dart'; import 'test_compat.dart';
import 'test_pointer.dart'; import 'test_pointer.dart';
import 'test_text_input.dart'; import 'test_text_input.dart';
import 'tree_traversal.dart';
// Keep users from needing multiple imports to test semantics. // Keep users from needing multiple imports to test semantics.
export 'package:flutter/rendering.dart' show SemanticsHandle; 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] /// Tests that just need to add text to widgets like [TextField]
/// or [TextFormField] only need to call [enterText]. /// 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 { return TestAsyncUtils.guard<void>(() async {
final EditableTextState editable = state<EditableTextState>( final EditableTextState editable = state<EditableTextState>(
find.descendant( find.descendant(
of: finder, of: finder,
matching: find.byType(EditableText, skipOffstage: finder.skipOffstage), matching: find.byType(EditableText, skipOffstage: skipOffstage),
matchRoot: true, 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), /// that widget has an open connection (e.g. by using [tap] to focus it),
/// then call `testTextInput.enterText` directly (see /// then call `testTextInput.enterText` directly (see
/// [TestTextInput.enterText]). /// [TestTextInput.enterText]).
Future<void> enterText(Finder finder, String text) async { Future<void> enterText(FinderBase<Element> finder, String text) async {
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
await showKeyboard(finder); await showKeyboard(finder);
testTextInput.enterText(text); testTextInput.enterText(text);

View File

@ -8,6 +8,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.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() { void main() {
group('image', () { group('image', () {
testWidgets('finds Image widgets', (WidgetTester tester) async { testWidgets('finds Image widgets', (WidgetTester tester) async {
@ -390,6 +395,764 @@ void main() {
find.byWidgetPredicate((_) => true).evaluate().length; find.byWidgetPredicate((_) => true).evaluate().length;
expect(find.bySubtype<Widget>(), findsNWidgets(totalWidgetCount)); 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) { Widget _boilerplate(Widget child) {
@ -442,3 +1205,45 @@ class SimpleGenericWidget<T> extends StatelessWidget {
return _child; 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;
}
}

View File

@ -1330,6 +1330,72 @@ void main() {
expect(find.byType(Text), isNot(findsAtLeastNWidgets(3))); 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 { enum _ComparatorBehavior {

View File

@ -16,11 +16,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:matcher/expect.dart' as matcher; import 'package:matcher/expect.dart' as matcher;
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports 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() { void main() {
group('expectLater', () { group('expectLater', () {
testWidgets('completes when matcher completes', (WidgetTester tester) async { testWidgets('completes when matcher completes', (WidgetTester tester) async {
@ -75,70 +70,6 @@ void main() {
}); });
}, skip: true); // [intended] API testing }, 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', () { group('pumping', () {
testWidgets('pumping', (WidgetTester tester) async { testWidgets('pumping', (WidgetTester tester) async {
await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
@ -196,215 +127,6 @@ void main() {
expect(logPaints, <int>[60000, 70000, 80000]); 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', () { group('pageBack', () {
testWidgets('fails when there are no back buttons', (WidgetTester tester) async { testWidgets('fails when there are no back buttons', (WidgetTester tester) async {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
@ -985,12 +707,3 @@ class _AlwaysRepaint extends CustomPainter {
onPaint(); 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;
}