mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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:
parent
73e0dbf5f4
commit
5df1c996ad
@ -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';
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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) {
|
||||
|
156
packages/flutter_test/lib/src/tree_traversal.dart
Normal file
156
packages/flutter_test/lib/src/tree_traversal.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user