1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:io';
6///
7/// @docImport 'package:flutter/scheduler.dart';
8/// @docImport 'package:flutter_driver/flutter_driver.dart';
9///
10/// @docImport 'binding.dart';
11/// @docImport 'finders.dart';
12/// @docImport 'matchers.dart';
13/// @docImport 'widget_tester.dart';
14library;
15
16import 'package:clock/clock.dart';
17import 'package:flutter/foundation.dart';
18import 'package:flutter/gestures.dart';
19import 'package:flutter/rendering.dart';
20import 'package:flutter/services.dart';
21import 'package:flutter/widgets.dart';
22
23import 'event_simulation.dart';
24import 'finders.dart' as finders;
25import 'test_async_utils.dart';
26import 'test_pointer.dart';
27import 'tree_traversal.dart';
28import 'window.dart';
29
30/// The default drag touch slop used to break up a large drag into multiple
31/// smaller moves.
32///
33/// This value must be greater than [kTouchSlop].
34const double kDragSlopDefault = 20.0;
35
36// Finds the end index (exclusive) of the span at `startIndex`, or `endIndex` if
37// there are no other spans between `startIndex` and `endIndex`.
38// The InlineSpan protocol doesn't expose the length of the span so we'll
39// have to iterate through the whole range.
40(InlineSpan, int)? _findEndOfSpan(InlineSpan rootSpan, int startIndex, int endIndex) {
41 assert(endIndex > startIndex);
42 final InlineSpan? subspan = rootSpan.getSpanForPosition(TextPosition(offset: startIndex));
43 if (subspan == null) {
44 return null;
45 }
46 int i = startIndex + 1;
47 while (i < endIndex && rootSpan.getSpanForPosition(TextPosition(offset: i)) == subspan) {
48 i += 1;
49 }
50 return (subspan, i);
51}
52
53// Examples can assume:
54// typedef MyWidget = Placeholder;
55
56/// Class that programmatically interacts with the [Semantics] tree.
57///
58/// Allows for testing of the [Semantics] tree, which is used by assistive
59/// technology, search engines, and other analysis software to determine the
60/// meaning of an application.
61///
62/// Should be accessed through [WidgetController.semantics]. If no custom
63/// implementation is provided, a default [SemanticsController] will be created.
64class SemanticsController {
65 /// Creates a [SemanticsController] that uses the given binding. Will be
66 /// automatically created as part of instantiating a [WidgetController], but
67 /// a custom implementation can be passed via the [WidgetController] constructor.
68 SemanticsController._(this._controller);
69
70 static final int _scrollingActions =
71 SemanticsAction.scrollUp.index |
72 SemanticsAction.scrollDown.index |
73 SemanticsAction.scrollLeft.index |
74 SemanticsAction.scrollRight.index |
75 SemanticsAction.scrollToOffset.index;
76
77 /// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java).
78 static final int _importantFlagsForAccessibility =
79 SemanticsFlag.hasCheckedState.index |
80 SemanticsFlag.hasToggledState.index |
81 SemanticsFlag.hasEnabledState.index |
82 SemanticsFlag.isButton.index |
83 SemanticsFlag.isTextField.index |
84 SemanticsFlag.isFocusable.index |
85 SemanticsFlag.isSlider.index |
86 SemanticsFlag.isInMutuallyExclusiveGroup.index;
87
88 final WidgetController _controller;
89
90 /// Attempts to find the [SemanticsNode] of first result from `finder`.
91 ///
92 /// If the object identified by the finder doesn't own its semantic node,
93 /// this will return the semantics data of the first ancestor with semantics.
94 /// The ancestor's semantic data will include the child's as well as
95 /// other nodes that have been merged together.
96 ///
97 /// If the [SemanticsNode] of the object identified by the finder is
98 /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget)
99 /// the node into which it is merged is returned. That node will include
100 /// all the semantics information of the nodes merged into it.
101 ///
102 /// Will throw a [StateError] if the finder returns more than one element or
103 /// if no semantics are found or are not enabled.
104 SemanticsNode find(finders.FinderBase<Element> finder) {
105 TestAsyncUtils.guardSync();
106 final Iterable<Element> candidates = finder.evaluate();
107 if (candidates.isEmpty) {
108 throw StateError('Finder returned no matching elements.');
109 }
110 if (candidates.length > 1) {
111 throw StateError('Finder returned more than one element.');
112 }
113 final Element element = candidates.single;
114 RenderObject? renderObject = element.findRenderObject();
115 SemanticsNode? result = renderObject?.debugSemantics;
116 while (renderObject != null && (result == null || result.isMergedIntoParent)) {
117 renderObject = renderObject.parent;
118 result = renderObject?.debugSemantics;
119 }
120 if (result == null) {
121 throw StateError('No Semantics data found.');
122 }
123 return result;
124 }
125
126 /// Simulates a traversal of the currently visible semantics tree as if by
127 /// assistive technologies.
128 ///
129 /// Starts at the node for `startNode`. If `startNode` is not provided, then
130 /// the traversal begins with the first accessible node in the tree. If
131 /// `startNode` finds zero elements or more than one element, a [StateError]
132 /// will be thrown.
133 ///
134 /// Ends at the node for `endNode`, inclusive. If `endNode` is not provided,
135 /// then the traversal ends with the last accessible node in the currently
136 /// available tree. If `endNode` finds zero elements or more than one element,
137 /// a [StateError] will be thrown.
138 ///
139 /// If provided, the nodes for `endNode` and `startNode` must be part of the
140 /// same semantics tree, i.e. they must be part of the same view.
141 ///
142 /// If neither `startNode` or `endNode` is provided, `view` can be provided to
143 /// specify the semantics tree to traverse. If `view` is left unspecified,
144 /// [WidgetTester.view] is traversed by default.
145 ///
146 /// Since the order is simulated, edge cases that differ between platforms
147 /// (such as how the last visible item in a scrollable list is handled) may be
148 /// inconsistent with platform behavior, but are expected to be sufficient for
149 /// testing order, availability to assistive technologies, and interactions.
150 ///
151 /// ## Sample Code
152 ///
153 /// ```dart
154 /// testWidgets('MyWidget', (WidgetTester tester) async {
155 /// await tester.pumpWidget(const MyWidget());
156 ///
157 /// expect(
158 /// tester.semantics.simulatedAccessibilityTraversal(),
159 /// containsAllInOrder(<Matcher>[
160 /// containsSemantics(label: 'My Widget'),
161 /// containsSemantics(label: 'is awesome!', isChecked: true),
162 /// ]),
163 /// );
164 /// });
165 /// ```
166 ///
167 /// See also:
168 ///
169 /// * [containsSemantics] and [matchesSemantics], which can be used to match
170 /// against a single node in the traversal.
171 /// * [containsAllInOrder], which can be given an [Iterable<Matcher>] to fuzzy
172 /// match the order allowing extra nodes before after and between matching
173 /// parts of the traversal.
174 /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
175 /// match the order of the traversal.
176 Iterable<SemanticsNode> simulatedAccessibilityTraversal({
177 @Deprecated(
178 'Use startNode instead. '
179 'This method was originally created before semantics finders were available. '
180 'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. '
181 'This feature was deprecated after v3.15.0-15.2.pre.',
182 )
183 finders.FinderBase<Element>? start,
184 @Deprecated(
185 'Use endNode instead. '
186 'This method was originally created before semantics finders were available. '
187 'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. '
188 'This feature was deprecated after v3.15.0-15.2.pre.',
189 )
190 finders.FinderBase<Element>? end,
191 finders.FinderBase<SemanticsNode>? startNode,
192 finders.FinderBase<SemanticsNode>? endNode,
193 FlutterView? view,
194 }) {
195 TestAsyncUtils.guardSync();
196 assert(
197 start == null || startNode == null,
198 'Cannot provide both start and startNode. Prefer startNode as start is deprecated.',
199 );
200 assert(
201 end == null || endNode == null,
202 'Cannot provide both end and endNode. Prefer endNode as end is deprecated.',
203 );
204
205 FlutterView? startView;
206 if (start != null) {
207 startView = _controller.viewOf(start);
208 if (view != null && startView != view) {
209 throw StateError(
210 'The start node is not part of the provided view.\n'
211 'Finder: ${start.toString(describeSelf: true)}\n'
212 'View of start node: $startView\n'
213 'Specified view: $view',
214 );
215 }
216 } else if (startNode != null) {
217 final SemanticsOwner owner = startNode.evaluate().single.owner!;
218 final RenderView renderView = _controller.binding.renderViews.firstWhere(
219 (RenderView render) => render.owner!.semanticsOwner == owner,
220 );
221 startView = renderView.flutterView;
222 if (view != null && startView != view) {
223 throw StateError(
224 'The start node is not part of the provided view.\n'
225 'Finder: ${startNode.toString(describeSelf: true)}\n'
226 'View of start node: $startView\n'
227 'Specified view: $view',
228 );
229 }
230 }
231
232 FlutterView? endView;
233 if (end != null) {
234 endView = _controller.viewOf(end);
235 if (view != null && endView != view) {
236 throw StateError(
237 'The end node is not part of the provided view.\n'
238 'Finder: ${end.toString(describeSelf: true)}\n'
239 'View of end node: $endView\n'
240 'Specified view: $view',
241 );
242 }
243 } else if (endNode != null) {
244 final SemanticsOwner owner = endNode.evaluate().single.owner!;
245 final RenderView renderView = _controller.binding.renderViews.firstWhere(
246 (RenderView render) => render.owner!.semanticsOwner == owner,
247 );
248 endView = renderView.flutterView;
249 if (view != null && endView != view) {
250 throw StateError(
251 'The end node is not part of the provided view.\n'
252 'Finder: ${endNode.toString(describeSelf: true)}\n'
253 'View of end node: $endView\n'
254 'Specified view: $view',
255 );
256 }
257 }
258
259 if (endView != null && startView != null && endView != startView) {
260 throw StateError(
261 'The start and end node are in different views.\n'
262 'Start finder: ${start!.toString(describeSelf: true)}\n'
263 'End finder: ${end!.toString(describeSelf: true)}\n'
264 'View of start node: $startView\n'
265 'View of end node: $endView',
266 );
267 }
268
269 final FlutterView actualView = view ?? startView ?? endView ?? _controller.view;
270 final RenderView renderView = _controller.binding.renderViews.firstWhere(
271 (RenderView r) => r.flutterView == actualView,
272 );
273
274 final List<SemanticsNode> traversal = <SemanticsNode>[];
275 _accessibilityTraversal(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
276
277 // Setting the range
278 SemanticsNode? node;
279 String? errorString;
280
281 int startIndex;
282 if (start != null) {
283 node = find(start);
284 startIndex = traversal.indexOf(node);
285 errorString = start.toString(describeSelf: true);
286 } else if (startNode != null) {
287 node = startNode.evaluate().single;
288 startIndex = traversal.indexOf(node);
289 errorString = startNode.toString(describeSelf: true);
290 } else {
291 startIndex = 0;
292 }
293 if (startIndex == -1) {
294 throw StateError(
295 'The expected starting node was not found.\n'
296 'Finder: $errorString\n\n'
297 'Expected Start Node: $node\n\n'
298 'Traversal: [\n ${traversal.join('\n ')}\n]',
299 );
300 }
301
302 int? endIndex;
303 if (end != null) {
304 node = find(end);
305 endIndex = traversal.indexOf(node);
306 errorString = end.toString(describeSelf: true);
307 } else if (endNode != null) {
308 node = endNode.evaluate().single;
309 endIndex = traversal.indexOf(node);
310 errorString = endNode.toString(describeSelf: true);
311 }
312 if (endIndex == -1) {
313 throw StateError(
314 'The expected ending node was not found.\n'
315 'Finder: $errorString\n\n'
316 'Expected End Node: $node\n\n'
317 'Traversal: [\n ${traversal.join('\n ')}\n]',
318 );
319 }
320 endIndex ??= traversal.length - 1;
321
322 return traversal.getRange(startIndex, endIndex + 1);
323 }
324
325 /// Recursive depth first traversal of the specified `node`, adding nodes
326 /// that are important for semantics to the `traversal` list.
327 void _accessibilityTraversal(SemanticsNode node, List<SemanticsNode> traversal) {
328 if (_isImportantForAccessibility(node)) {
329 traversal.add(node);
330 }
331
332 final List<SemanticsNode> children = node.debugListChildrenInOrder(
333 DebugSemanticsDumpOrder.traversalOrder,
334 );
335 for (final SemanticsNode child in children) {
336 _accessibilityTraversal(child, traversal);
337 }
338 }
339
340 /// Whether or not the node is important for semantics. Should match most cases
341 /// on the platforms, but certain edge cases will be inconsistent.
342 ///
343 /// Based on:
344 ///
345 /// * [flutter/engine/AccessibilityBridge.java#SemanticsNode.isFocusable()](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L2641)
346 /// * [flutter/engine/SemanticsObject.mm#SemanticsObject.isAccessibilityElement](https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm#L449)
347 bool _isImportantForAccessibility(SemanticsNode node) {
348 if (node.isMergedIntoParent) {
349 // If this node is merged, all its information are present on an ancestor
350 // node.
351 return false;
352 }
353 final SemanticsData data = node.getSemanticsData();
354 // If the node scopes a route, it doesn't matter what other flags/actions it
355 // has, it is _not_ important for accessibility, so we short circuit.
356 if (data.hasFlag(SemanticsFlag.scopesRoute)) {
357 return false;
358 }
359
360 final bool hasNonScrollingAction = data.actions & ~_scrollingActions != 0;
361 if (hasNonScrollingAction) {
362 return true;
363 }
364
365 final bool hasImportantFlag = data.flags & _importantFlagsForAccessibility != 0;
366 if (hasImportantFlag) {
367 return true;
368 }
369
370 final bool hasContent = data.label.isNotEmpty || data.value.isNotEmpty || data.hint.isNotEmpty;
371 if (hasContent) {
372 return true;
373 }
374
375 return false;
376 }
377
378 /// Performs the given [SemanticsAction] on the [SemanticsNode] found by `finder`.
379 ///
380 /// If `args` are provided, they will be passed unmodified with the `action`.
381 /// The `checkForAction` argument allows for attempting to perform `action` on
382 /// `node` even if it doesn't report supporting that action. This is useful
383 /// for implicitly supported actions such as [SemanticsAction.showOnScreen].
384 void performAction(
385 finders.FinderBase<SemanticsNode> finder,
386 SemanticsAction action, {
387 Object? args,
388 bool checkForAction = true,
389 }) {
390 final SemanticsNode node = finder.evaluate().single;
391 if (checkForAction && !node.getSemanticsData().hasAction(action)) {
392 throw StateError(
393 'The given node does not support $action. If the action is implicitly '
394 'supported or an unsupported action is being tested for this node, '
395 'set `checkForAction` to false.\n'
396 'Node: $node',
397 );
398 }
399
400 node.owner!.performAction(node.id, action, args);
401 }
402
403 /// Performs a [SemanticsAction.tap] action on the [SemanticsNode] found
404 /// by `finder`.
405 ///
406 /// Throws a [StateError] if:
407 /// * The given `finder` returns zero or more than one result.
408 /// * The [SemanticsNode] found with `finder` does not support
409 /// [SemanticsAction.tap].
410 void tap(finders.FinderBase<SemanticsNode> finder) {
411 performAction(finder, SemanticsAction.tap);
412 }
413
414 /// Performs a [SemanticsAction.longPress] action on the [SemanticsNode] found
415 /// by `finder`.
416 ///
417 /// Throws a [StateError] if:
418 /// * The given `finder` returns zero or more than one result.
419 /// * The [SemanticsNode] found with `finder` does not support
420 /// [SemanticsAction.longPress].
421 void longPress(finders.FinderBase<SemanticsNode> finder) {
422 performAction(finder, SemanticsAction.longPress);
423 }
424
425 /// Performs a [SemanticsAction.scrollLeft] action on the [SemanticsNode]
426 /// found by `scrollable` or the first scrollable node in the default
427 /// semantics tree if no `scrollable` is provided.
428 ///
429 /// Throws a [StateError] if:
430 /// * The given `scrollable` returns zero or more than one result.
431 /// * The [SemanticsNode] found with `scrollable` does not support
432 /// [SemanticsAction.scrollLeft].
433 void scrollLeft({finders.FinderBase<SemanticsNode>? scrollable}) {
434 performAction(scrollable ?? finders.find.semantics.scrollable(), SemanticsAction.scrollLeft);
435 }
436
437 /// Performs a [SemanticsAction.scrollRight] action on the [SemanticsNode]
438 /// found by `scrollable` or the first scrollable node in the default
439 /// semantics tree if no `scrollable` is provided.
440 ///
441 /// Throws a [StateError] if:
442 /// * The given `scrollable` returns zero or more than one result.
443 /// * The [SemanticsNode] found with `scrollable` does not support
444 /// [SemanticsAction.scrollRight].
445 void scrollRight({finders.FinderBase<SemanticsNode>? scrollable}) {
446 performAction(scrollable ?? finders.find.semantics.scrollable(), SemanticsAction.scrollRight);
447 }
448
449 /// Performs a [SemanticsAction.scrollUp] action on the [SemanticsNode] found
450 /// by `scrollable` or the first scrollable node in the default semantics
451 /// tree if no `scrollable` is provided.
452 ///
453 /// Throws a [StateError] if:
454 /// * The given `scrollable` returns zero or more than one result.
455 /// * The [SemanticsNode] found with `scrollable` does not support
456 /// [SemanticsAction.scrollUp].
457 void scrollUp({finders.FinderBase<SemanticsNode>? scrollable}) {
458 performAction(scrollable ?? finders.find.semantics.scrollable(), SemanticsAction.scrollUp);
459 }
460
461 /// Performs a [SemanticsAction.scrollDown] action on the [SemanticsNode]
462 /// found by `scrollable` or the first scrollable node in the default
463 /// semantics tree if no `scrollable` is provided.
464 ///
465 /// Throws a [StateError] if:
466 /// * The given `scrollable` returns zero or more than one result.
467 /// * The [SemanticsNode] found with `scrollable` does not support
468 /// [SemanticsAction.scrollDown].
469 void scrollDown({finders.FinderBase<SemanticsNode>? scrollable}) {
470 performAction(scrollable ?? finders.find.semantics.scrollable(), SemanticsAction.scrollDown);
471 }
472
473 /// Performs a [SemanticsAction.increase] action on the [SemanticsNode]
474 /// found by `finder`.
475 ///
476 /// Throws a [StateError] if:
477 /// * The given `finder` returns zero or more than one result.
478 /// * The [SemanticsNode] found with `finder` does not support
479 /// [SemanticsAction.increase].
480 void increase(finders.FinderBase<SemanticsNode> finder) {
481 performAction(finder, SemanticsAction.increase);
482 }
483
484 /// Performs a [SemanticsAction.decrease] action on the [SemanticsNode]
485 /// found by `finder`.
486 ///
487 /// Throws a [StateError] if:
488 /// * The given `finder` returns zero or more than one result.
489 /// * The [SemanticsNode] found with `finder` does not support
490 /// [SemanticsAction.decrease].
491 void decrease(finders.FinderBase<SemanticsNode> finder) {
492 performAction(finder, SemanticsAction.decrease);
493 }
494
495 /// Performs a [SemanticsAction.showOnScreen] action on the [SemanticsNode]
496 /// found by `finder`.
497 ///
498 /// Throws a [StateError] if:
499 /// * The given `finder` returns zero or more than one result.
500 /// * The [SemanticsNode] found with `finder` does not support
501 /// [SemanticsAction.showOnScreen].
502 void showOnScreen(finders.FinderBase<SemanticsNode> finder) {
503 performAction(finder, SemanticsAction.showOnScreen, checkForAction: false);
504 }
505
506 /// Performs a [SemanticsAction.moveCursorForwardByCharacter] action on the
507 /// [SemanticsNode] found by `finder`.
508 ///
509 /// If `shouldModifySelection` is true, then the cursor will begin or extend
510 /// a selection.
511 ///
512 /// Throws a [StateError] if:
513 /// * The given `finder` returns zero or more than one result.
514 /// * The [SemanticsNode] found with `finder` does not support
515 /// [SemanticsAction.moveCursorForwardByCharacter].
516 void moveCursorForwardByCharacter(
517 finders.FinderBase<SemanticsNode> finder, {
518 bool shouldModifySelection = false,
519 }) {
520 performAction(
521 finder,
522 SemanticsAction.moveCursorForwardByCharacter,
523 args: shouldModifySelection,
524 );
525 }
526
527 /// Performs a [SemanticsAction.moveCursorForwardByWord] action on the
528 /// [SemanticsNode] found by `finder`.
529 ///
530 /// Throws a [StateError] if:
531 /// * The given `finder` returns zero or more than one result.
532 /// * The [SemanticsNode] found with `finder` does not support
533 /// [SemanticsAction.moveCursorForwardByWord].
534 void moveCursorForwardByWord(
535 finders.FinderBase<SemanticsNode> finder, {
536 bool shouldModifySelection = false,
537 }) {
538 performAction(finder, SemanticsAction.moveCursorForwardByWord, args: shouldModifySelection);
539 }
540
541 /// Performs a [SemanticsAction.moveCursorBackwardByCharacter] action on the
542 /// [SemanticsNode] found by `finder`.
543 ///
544 /// If `shouldModifySelection` is true, then the cursor will begin or extend
545 /// a selection.
546 ///
547 /// Throws a [StateError] if:
548 /// * The given `finder` returns zero or more than one result.
549 /// * The [SemanticsNode] found with `finder` does not support
550 /// [SemanticsAction.moveCursorBackwardByCharacter].
551 void moveCursorBackwardByCharacter(
552 finders.FinderBase<SemanticsNode> finder, {
553 bool shouldModifySelection = false,
554 }) {
555 performAction(
556 finder,
557 SemanticsAction.moveCursorBackwardByCharacter,
558 args: shouldModifySelection,
559 );
560 }
561
562 /// Performs a [SemanticsAction.moveCursorBackwardByWord] action on the
563 /// [SemanticsNode] found by `finder`.
564 ///
565 /// Throws a [StateError] if:
566 /// * The given `finder` returns zero or more than one result.
567 /// * The [SemanticsNode] found with `finder` does not support
568 /// [SemanticsAction.moveCursorBackwardByWord].
569 void moveCursorBackwardByWord(
570 finders.FinderBase<SemanticsNode> finder, {
571 bool shouldModifySelection = false,
572 }) {
573 performAction(finder, SemanticsAction.moveCursorBackwardByWord, args: shouldModifySelection);
574 }
575
576 /// Performs a [SemanticsAction.setText] action on the [SemanticsNode]
577 /// found by `finder` using the given `text`.
578 ///
579 /// Throws a [StateError] if:
580 /// * The given `finder` returns zero or more than one result.
581 /// * The [SemanticsNode] found with `finder` does not support
582 /// [SemanticsAction.setText].
583 void setText(finders.FinderBase<SemanticsNode> finder, String text) {
584 performAction(finder, SemanticsAction.setText, args: text);
585 }
586
587 /// Performs a [SemanticsAction.setSelection] action on the [SemanticsNode]
588 /// found by `finder`.
589 ///
590 /// The `base` parameter is the start index of selection, and the `extent`
591 /// parameter is the length of the selection. Each value should be limited
592 /// between 0 and the length of the found [SemanticsNode]'s `value`.
593 ///
594 /// Throws a [StateError] if:
595 /// * The given `finder` returns zero or more than one result.
596 /// * The [SemanticsNode] found with `finder` does not support
597 /// [SemanticsAction.setSelection].
598 void setSelection(
599 finders.FinderBase<SemanticsNode> finder, {
600 required int base,
601 required int extent,
602 }) {
603 performAction(
604 finder,
605 SemanticsAction.setSelection,
606 args: <String, int>{'base': base, 'extent': extent},
607 );
608 }
609
610 /// Performs a [SemanticsAction.copy] action on the [SemanticsNode]
611 /// found by `finder`.
612 ///
613 /// Throws a [StateError] if:
614 /// * The given `finder` returns zero or more than one result.
615 /// * The [SemanticsNode] found with `finder` does not support
616 /// [SemanticsAction.copy].
617 void copy(finders.FinderBase<SemanticsNode> finder) {
618 performAction(finder, SemanticsAction.copy);
619 }
620
621 /// Performs a [SemanticsAction.cut] action on the [SemanticsNode]
622 /// found by `finder`.
623 ///
624 /// Throws a [StateError] if:
625 /// * The given `finder` returns zero or more than one result.
626 /// * The [SemanticsNode] found with `finder` does not support
627 /// [SemanticsAction.cut].
628 void cut(finders.FinderBase<SemanticsNode> finder) {
629 performAction(finder, SemanticsAction.cut);
630 }
631
632 /// Performs a [SemanticsAction.paste] action on the [SemanticsNode]
633 /// found by `finder`.
634 ///
635 /// Throws a [StateError] if:
636 /// * The given `finder` returns zero or more than one result.
637 /// * The [SemanticsNode] found with `finder` does not support
638 /// [SemanticsAction.paste].
639 void paste(finders.FinderBase<SemanticsNode> finder) {
640 performAction(finder, SemanticsAction.paste);
641 }
642
643 /// Performs a [SemanticsAction.didGainAccessibilityFocus] action on the
644 /// [SemanticsNode] found by `finder`.
645 ///
646 /// Throws a [StateError] if:
647 /// * The given `finder` returns zero or more than one result.
648 /// * The [SemanticsNode] found with `finder` does not support
649 /// [SemanticsAction.didGainAccessibilityFocus].
650 void didGainAccessibilityFocus(finders.FinderBase<SemanticsNode> finder) {
651 performAction(finder, SemanticsAction.didGainAccessibilityFocus);
652 }
653
654 /// Performs a [SemanticsAction.didLoseAccessibilityFocus] action on the
655 /// [SemanticsNode] found by `finder`.
656 ///
657 /// Throws a [StateError] if:
658 /// * The given `finder` returns zero or more than one result.
659 /// * The [SemanticsNode] found with `finder` does not support
660 /// [SemanticsAction.didLoseAccessibilityFocus].
661 void didLoseAccessibilityFocus(finders.FinderBase<SemanticsNode> finder) {
662 performAction(finder, SemanticsAction.didLoseAccessibilityFocus);
663 }
664
665 /// Performs a [SemanticsAction.customAction] action on the
666 /// [SemanticsNode] found by `finder`.
667 ///
668 /// Throws a [StateError] if:
669 /// * The given `finder` returns zero or more than one result.
670 /// * The [SemanticsNode] found with `finder` does not support
671 /// [SemanticsAction.customAction].
672 void customAction(finders.FinderBase<SemanticsNode> finder, CustomSemanticsAction action) {
673 performAction(
674 finder,
675 SemanticsAction.customAction,
676 args: CustomSemanticsAction.getIdentifier(action),
677 );
678 }
679
680 /// Performs a [SemanticsAction.dismiss] action on the [SemanticsNode]
681 /// found by `finder`.
682 ///
683 /// Throws a [StateError] if:
684 /// * The given `finder` returns zero or more than one result.
685 /// * The [SemanticsNode] found with `finder` does not support
686 /// [SemanticsAction.dismiss].
687 void dismiss(finders.FinderBase<SemanticsNode> finder) {
688 performAction(finder, SemanticsAction.dismiss);
689 }
690}
691
692/// Class that programmatically interacts with widgets.
693///
694/// For a variant of this class suited specifically for unit tests, see
695/// [WidgetTester]. For one suitable for live tests on a device, consider
696/// [LiveWidgetController].
697///
698/// Concrete subclasses must implement the [pump] method.
699abstract class WidgetController {
700 /// Creates a widget controller that uses the given binding.
701 WidgetController(this.binding);
702
703 /// A reference to the current instance of the binding.
704 final WidgetsBinding binding;
705
706 /// The [TestPlatformDispatcher] that is being used in this test.
707 ///
708 /// This will be injected into the framework such that calls to
709 /// [WidgetsBinding.platformDispatcher] will use this. This allows
710 /// users to change platform specific properties for testing.
711 ///
712 /// See also:
713 ///
714 /// * [TestFlutterView] which allows changing view specific properties
715 /// for testing
716 /// * [view] and [viewOf] which are used to find
717 /// [TestFlutterView]s from the widget tree
718 TestPlatformDispatcher get platformDispatcher =>
719 binding.platformDispatcher as TestPlatformDispatcher;
720
721 /// The [TestFlutterView] provided by default when testing with
722 /// [WidgetTester.pumpWidget].
723 ///
724 /// If the test uses multiple views, this will return the view that is painted
725 /// into by [WidgetTester.pumpWidget]. If a different view needs to be
726 /// accessed use [viewOf] to ensure that the view related to the widget being
727 /// evaluated is the one that gets updated.
728 ///
729 /// See also:
730 ///
731 /// * [viewOf], which can find a [TestFlutterView] related to a given finder.
732 /// This is how to modify view properties for testing when dealing with
733 /// multiple views.
734 TestFlutterView get view => platformDispatcher.implicitView!;
735
736 /// Provides access to a [SemanticsController] for testing anything related to
737 /// the [Semantics] tree.
738 ///
739 /// Assistive technologies, search engines, and other analysis tools all make
740 /// use of the [Semantics] tree to determine the meaning of an application.
741 /// If semantics has been disabled for the test, this will throw a [StateError].
742 SemanticsController get semantics {
743 if (!binding.semanticsEnabled) {
744 throw StateError(
745 'Semantics are not enabled. Enable them by passing '
746 '`semanticsEnabled: true` to `testWidgets`, or by manually creating a '
747 '`SemanticsHandle` with `WidgetController.ensureSemantics()`.',
748 );
749 }
750
751 return _semantics;
752 }
753
754 late final SemanticsController _semantics = SemanticsController._(this);
755
756 // FINDER API
757
758 // TODO(ianh): verify that the return values are of type T and throw
759 // a good message otherwise, in all the generic methods below
760
761 /// Finds the [TestFlutterView] that is the closest ancestor of the widget
762 /// found by [finder].
763 ///
764 /// [TestFlutterView] can be used to modify view specific properties for testing.
765 ///
766 /// See also:
767 ///
768 /// * [view] which returns the [TestFlutterView] used when only a single
769 /// view is being used.
770 TestFlutterView viewOf(finders.FinderBase<Element> finder) {
771 return _viewOf(finder) as TestFlutterView;
772 }
773
774 FlutterView _viewOf(finders.FinderBase<Element> finder) {
775 return firstWidget<View>(
776 finders.find.ancestor(of: finder, matching: finders.find.byType(View)),
777 ).view;
778 }
779
780 /// Checks if `finder` exists in the tree.
781 bool any(finders.FinderBase<Element> finder) {
782 TestAsyncUtils.guardSync();
783 return finder.evaluate().isNotEmpty;
784 }
785
786 /// All widgets currently in the widget tree (lazy pre-order traversal).
787 ///
788 /// Can contain duplicates, since widgets can be used in multiple
789 /// places in the widget tree.
790 Iterable<Widget> get allWidgets {
791 TestAsyncUtils.guardSync();
792 return allElements.map<Widget>((Element element) => element.widget);
793 }
794
795 /// The matching widget in the widget tree.
796 ///
797 /// Throws a [StateError] if `finder` is empty or matches more than
798 /// one widget.
799 ///
800 /// * Use [firstWidget] if you expect to match several widgets but only want the first.
801 /// * Use [widgetList] if you expect to match several widgets and want all of them.
802 T widget<T extends Widget>(finders.FinderBase<Element> finder) {
803 TestAsyncUtils.guardSync();
804 return finder.evaluate().single.widget as T;
805 }
806
807 /// The first matching widget according to a depth-first pre-order
808 /// traversal of the widget tree.
809 ///
810 /// Throws a [StateError] if `finder` is empty.
811 ///
812 /// * Use [widget] if you only expect to match one widget.
813 T firstWidget<T extends Widget>(finders.FinderBase<Element> finder) {
814 TestAsyncUtils.guardSync();
815 return finder.evaluate().first.widget as T;
816 }
817
818 /// The matching widgets in the widget tree.
819 ///
820 /// * Use [widget] if you only expect to match one widget.
821 /// * Use [firstWidget] if you expect to match several but only want the first.
822 Iterable<T> widgetList<T extends Widget>(finders.FinderBase<Element> finder) {
823 TestAsyncUtils.guardSync();
824 return finder.evaluate().map<T>((Element element) {
825 final T result = element.widget as T;
826 return result;
827 });
828 }
829
830 /// Find all layers that are children of the provided [finder].
831 ///
832 /// The [finder] must match exactly one element.
833 Iterable<Layer> layerListOf(finders.FinderBase<Element> finder) {
834 TestAsyncUtils.guardSync();
835 final Element element = finder.evaluate().single;
836 final RenderObject object = element.renderObject!;
837 RenderObject current = object;
838 while (current.debugLayer == null) {
839 current = current.parent!;
840 }
841 final ContainerLayer layer = current.debugLayer!;
842 return _walkLayers(layer);
843 }
844
845 /// All elements currently in the widget tree (lazy pre-order traversal).
846 ///
847 /// The returned iterable is lazy. It does not walk the entire widget tree
848 /// immediately, but rather a chunk at a time as the iteration progresses
849 /// using [Iterator.moveNext].
850 Iterable<Element> get allElements {
851 TestAsyncUtils.guardSync();
852 return collectAllElementsFrom(binding.rootElement!, skipOffstage: false);
853 }
854
855 /// The matching element in the widget tree.
856 ///
857 /// Throws a [StateError] if `finder` is empty or matches more than
858 /// one element.
859 ///
860 /// * Use [firstElement] if you expect to match several elements but only want the first.
861 /// * Use [elementList] if you expect to match several elements and want all of them.
862 T element<T extends Element>(finders.FinderBase<Element> finder) {
863 TestAsyncUtils.guardSync();
864 return finder.evaluate().single as T;
865 }
866
867 /// The first matching element according to a depth-first pre-order
868 /// traversal of the widget tree.
869 ///
870 /// Throws a [StateError] if `finder` is empty.
871 ///
872 /// * Use [element] if you only expect to match one element.
873 T firstElement<T extends Element>(finders.FinderBase<Element> finder) {
874 TestAsyncUtils.guardSync();
875 return finder.evaluate().first as T;
876 }
877
878 /// The matching elements in the widget tree.
879 ///
880 /// * Use [element] if you only expect to match one element.
881 /// * Use [firstElement] if you expect to match several but only want the first.
882 Iterable<T> elementList<T extends Element>(finders.FinderBase<Element> finder) {
883 TestAsyncUtils.guardSync();
884 return finder.evaluate().cast<T>();
885 }
886
887 /// All states currently in the widget tree (lazy pre-order traversal).
888 ///
889 /// The returned iterable is lazy. It does not walk the entire widget tree
890 /// immediately, but rather a chunk at a time as the iteration progresses
891 /// using [Iterator.moveNext].
892 Iterable<State> get allStates {
893 TestAsyncUtils.guardSync();
894 return allElements.whereType<StatefulElement>().map<State>(
895 (StatefulElement element) => element.state,
896 );
897 }
898
899 /// The matching state in the widget tree.
900 ///
901 /// Throws a [StateError] if `finder` is empty, matches more than
902 /// one state, or matches a widget that has no state.
903 ///
904 /// * Use [firstState] if you expect to match several states but only want the first.
905 /// * Use [stateList] if you expect to match several states and want all of them.
906 T state<T extends State>(finders.FinderBase<Element> finder) {
907 TestAsyncUtils.guardSync();
908 return _stateOf<T>(finder.evaluate().single, finder);
909 }
910
911 /// The first matching state according to a depth-first pre-order
912 /// traversal of the widget tree.
913 ///
914 /// Throws a [StateError] if `finder` is empty or if the first
915 /// matching widget has no state.
916 ///
917 /// * Use [state] if you only expect to match one state.
918 T firstState<T extends State>(finders.FinderBase<Element> finder) {
919 TestAsyncUtils.guardSync();
920 return _stateOf<T>(finder.evaluate().first, finder);
921 }
922
923 /// The matching states in the widget tree.
924 ///
925 /// Throws a [StateError] if any of the elements in `finder` match a widget
926 /// that has no state.
927 ///
928 /// * Use [state] if you only expect to match one state.
929 /// * Use [firstState] if you expect to match several but only want the first.
930 Iterable<T> stateList<T extends State>(finders.FinderBase<Element> finder) {
931 TestAsyncUtils.guardSync();
932 return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
933 }
934
935 T _stateOf<T extends State>(Element element, finders.FinderBase<Element> finder) {
936 TestAsyncUtils.guardSync();
937 if (element is StatefulElement) {
938 return element.state as T;
939 }
940 throw StateError(
941 'Widget of type ${element.widget.runtimeType}, with ${finder.describeMatch(finders.Plurality.many)}, is not a StatefulWidget.',
942 );
943 }
944
945 /// Render objects of all the widgets currently in the widget tree
946 /// (lazy pre-order traversal).
947 ///
948 /// This will almost certainly include many duplicates since the
949 /// render object of a [StatelessWidget] or [StatefulWidget] is the
950 /// render object of its child; only [RenderObjectWidget]s have
951 /// their own render object.
952 Iterable<RenderObject> get allRenderObjects {
953 TestAsyncUtils.guardSync();
954 return allElements.map<RenderObject>((Element element) => element.renderObject!);
955 }
956
957 /// The render object of the matching widget in the widget tree.
958 ///
959 /// Throws a [StateError] if `finder` is empty or matches more than
960 /// one widget (even if they all have the same render object).
961 ///
962 /// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
963 /// * Use [renderObjectList] if you expect to match several render objects and want all of them.
964 T renderObject<T extends RenderObject>(finders.FinderBase<Element> finder) {
965 TestAsyncUtils.guardSync();
966 return finder.evaluate().single.renderObject! as T;
967 }
968
969 /// The render object of the first matching widget according to a
970 /// depth-first pre-order traversal of the widget tree.
971 ///
972 /// Throws a [StateError] if `finder` is empty.
973 ///
974 /// * Use [renderObject] if you only expect to match one render object.
975 T firstRenderObject<T extends RenderObject>(finders.FinderBase<Element> finder) {
976 TestAsyncUtils.guardSync();
977 return finder.evaluate().first.renderObject! as T;
978 }
979
980 /// The render objects of the matching widgets in the widget tree.
981 ///
982 /// * Use [renderObject] if you only expect to match one render object.
983 /// * Use [firstRenderObject] if you expect to match several but only want the first.
984 Iterable<T> renderObjectList<T extends RenderObject>(finders.FinderBase<Element> finder) {
985 TestAsyncUtils.guardSync();
986 return finder.evaluate().map<T>((Element element) {
987 final T result = element.renderObject! as T;
988 return result;
989 });
990 }
991
992 /// Returns a list of all the [Layer] objects in the rendering.
993 List<Layer> get layers {
994 return <Layer>[
995 for (final RenderView renderView in binding.renderViews)
996 ..._walkLayers(renderView.debugLayer!),
997 ];
998 }
999
1000 Iterable<Layer> _walkLayers(Layer layer) sync* {
1001 TestAsyncUtils.guardSync();
1002 yield layer;
1003 if (layer is ContainerLayer) {
1004 final ContainerLayer root = layer;
1005 Layer? child = root.firstChild;
1006 while (child != null) {
1007 yield* _walkLayers(child);
1008 child = child.nextSibling;
1009 }
1010 }
1011 }
1012
1013 // INTERACTION
1014
1015 /// Dispatch a pointer down / pointer up sequence at the center of
1016 /// the given widget, assuming it is exposed.
1017 ///
1018 /// {@template flutter.flutter_test.WidgetController.tap.warnIfMissed}
1019 /// The `warnIfMissed` argument, if true (the default), causes a warning to be
1020 /// displayed on the console if the specified [Finder] indicates a widget and
1021 /// location that, were a pointer event to be sent to that location, would not
1022 /// actually send any events to the widget (e.g. because the widget is
1023 /// obscured, or the location is off-screen, or the widget is transparent to
1024 /// pointer events).
1025 ///
1026 /// Set the argument to false to silence that warning if you intend to not
1027 /// actually hit the specified element.
1028 /// {@endtemplate}
1029 ///
1030 /// For example, a test that verifies that tapping a disabled button does not
1031 /// trigger the button would set `warnIfMissed` to false, because the button
1032 /// would ignore the tap.
1033 Future<void> tap(
1034 finders.FinderBase<Element> finder, {
1035 int? pointer,
1036 int buttons = kPrimaryButton,
1037 bool warnIfMissed = true,
1038 PointerDeviceKind kind = PointerDeviceKind.touch,
1039 }) {
1040 return tapAt(
1041 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'tap'),
1042 pointer: pointer,
1043 buttons: buttons,
1044 kind: kind,
1045 );
1046 }
1047
1048 /// Dispatch a pointer down / pointer up sequence at a hit-testable
1049 /// [InlineSpan] (typically a [TextSpan]) within the given text range.
1050 ///
1051 /// This method performs a more spatially precise tap action on a piece of
1052 /// static text, than the widget-based [tap] method.
1053 ///
1054 /// The given [Finder] must find one and only one matching substring, and the
1055 /// substring must be hit-testable (meaning, it must not be off-screen, or be
1056 /// obscured by other widgets, or in a disabled widget). Otherwise this method
1057 /// throws a [FlutterError].
1058 ///
1059 /// If the target substring contains more than one hit-testable [InlineSpan]s,
1060 /// [tapOnText] taps on one of them, but does not guarantee which.
1061 ///
1062 /// The `pointer` and `button` arguments specify [PointerEvent.pointer] and
1063 /// [PointerEvent.buttons] of the tap event.
1064 Future<void> tapOnText(
1065 finders.FinderBase<finders.TextRangeContext> textRangeFinder, {
1066 int? pointer,
1067 int buttons = kPrimaryButton,
1068 }) {
1069 final Iterable<finders.TextRangeContext> ranges = textRangeFinder.evaluate();
1070 if (ranges.isEmpty) {
1071 throw FlutterError(textRangeFinder.toString());
1072 }
1073 if (ranges.length > 1) {
1074 throw FlutterError(
1075 '$textRangeFinder. The "tapOnText" method needs a single non-empty TextRange.',
1076 );
1077 }
1078 final Offset? tapLocation = _findHitTestableOffsetIn(ranges.single);
1079 if (tapLocation == null) {
1080 final finders.TextRangeContext found = textRangeFinder.evaluate().single;
1081 throw FlutterError.fromParts(<DiagnosticsNode>[
1082 ErrorSummary('Finder specifies a TextRange that can not receive pointer events.'),
1083 ErrorDescription('The finder used was: ${textRangeFinder.toString(describeSelf: true)}'),
1084 ErrorDescription(
1085 'Found a matching substring in a static text widget, within ${found.textRange}.',
1086 ),
1087 ErrorDescription(
1088 'But the "tapOnText" method could not find a hit-testable Offset with in that text range.',
1089 ),
1090 found.renderObject.toDiagnosticsNode(
1091 name: 'The RenderBox of that static text widget was',
1092 style: DiagnosticsTreeStyle.shallow,
1093 ),
1094 ]);
1095 }
1096 return tapAt(tapLocation, pointer: pointer, buttons: buttons);
1097 }
1098
1099 /// Dispatch a pointer down / pointer up sequence at the given location.
1100 Future<void> tapAt(
1101 Offset location, {
1102 int? pointer,
1103 int buttons = kPrimaryButton,
1104 PointerDeviceKind kind = PointerDeviceKind.touch,
1105 }) {
1106 return TestAsyncUtils.guard<void>(() async {
1107 final TestGesture gesture = await startGesture(
1108 location,
1109 pointer: pointer,
1110 buttons: buttons,
1111 kind: kind,
1112 );
1113 await gesture.up();
1114 });
1115 }
1116
1117 /// Dispatch a pointer down at the center of the given widget, assuming it is
1118 /// exposed.
1119 ///
1120 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1121 ///
1122 /// The return value is a [TestGesture] object that can be used to continue the
1123 /// gesture (e.g. moving the pointer or releasing it).
1124 ///
1125 /// See also:
1126 ///
1127 /// * [tap], which presses and releases a pointer at the given location.
1128 /// * [longPress], which presses and releases a pointer with a gap in
1129 /// between long enough to trigger the long-press gesture.
1130 Future<TestGesture> press(
1131 finders.FinderBase<Element> finder, {
1132 int? pointer,
1133 int buttons = kPrimaryButton,
1134 bool warnIfMissed = true,
1135 PointerDeviceKind kind = PointerDeviceKind.touch,
1136 }) {
1137 return TestAsyncUtils.guard<TestGesture>(() {
1138 return startGesture(
1139 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'),
1140 pointer: pointer,
1141 buttons: buttons,
1142 kind: kind,
1143 );
1144 });
1145 }
1146
1147 /// Dispatch a pointer down / pointer up sequence (with a delay of
1148 /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
1149 /// center of the given widget, assuming it is exposed.
1150 ///
1151 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1152 ///
1153 /// For example, consider a widget that, when long-pressed, shows an overlay
1154 /// that obscures the original widget. A test for that widget might first
1155 /// long-press that widget with `warnIfMissed` at its default value true, then
1156 /// later verify that long-pressing the same location (using the same finder)
1157 /// has no effect (since the widget is now obscured), setting `warnIfMissed`
1158 /// to false on that second call.
1159 Future<void> longPress(
1160 finders.FinderBase<Element> finder, {
1161 int? pointer,
1162 int buttons = kPrimaryButton,
1163 bool warnIfMissed = true,
1164 PointerDeviceKind kind = PointerDeviceKind.touch,
1165 }) {
1166 return longPressAt(
1167 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'longPress'),
1168 pointer: pointer,
1169 buttons: buttons,
1170 kind: kind,
1171 );
1172 }
1173
1174 /// Dispatch a pointer down / pointer up sequence at the given location with
1175 /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
1176 Future<void> longPressAt(
1177 Offset location, {
1178 int? pointer,
1179 int buttons = kPrimaryButton,
1180 PointerDeviceKind kind = PointerDeviceKind.touch,
1181 }) {
1182 return TestAsyncUtils.guard<void>(() async {
1183 final TestGesture gesture = await startGesture(
1184 location,
1185 pointer: pointer,
1186 buttons: buttons,
1187 kind: kind,
1188 );
1189 await pump(kLongPressTimeout + kPressTimeout);
1190 await gesture.up();
1191 });
1192 }
1193
1194 /// Attempts a fling gesture starting from the center of the given
1195 /// widget, moving the given distance, reaching the given speed.
1196 ///
1197 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1198 ///
1199 /// {@template flutter.flutter_test.WidgetController.fling.offset}
1200 /// The `offset` represents a distance the pointer moves in the global
1201 /// coordinate system of the screen.
1202 ///
1203 /// Positive [Offset.dy] values mean the pointer moves downward. Negative
1204 /// [Offset.dy] values mean the pointer moves upwards. Accordingly, positive
1205 /// [Offset.dx] values mean the pointer moves towards the right. Negative
1206 /// [Offset.dx] values mean the pointer moves towards left.
1207 /// {@endtemplate}
1208 ///
1209 /// {@template flutter.flutter_test.WidgetController.fling}
1210 /// This can pump frames.
1211 ///
1212 /// Exactly 50 pointer events are synthesized.
1213 ///
1214 /// The `speed` is in pixels per second in the direction given by `offset`.
1215 ///
1216 /// The `offset` and `speed` control the interval between each pointer event.
1217 /// For example, if the `offset` is 200 pixels down, and the `speed` is 800
1218 /// pixels per second, the pointer events will be sent for each increment
1219 /// of 4 pixels (200/50), over 250ms (200/800), meaning events will be sent
1220 /// every 1.25ms (250/200).
1221 ///
1222 /// To make tests more realistic, frames may be pumped during this time (using
1223 /// calls to [pump]). If the total duration is longer than `frameInterval`,
1224 /// then one frame is pumped each time that amount of time elapses while
1225 /// sending events, or each time an event is synthesized, whichever is rarer.
1226 ///
1227 /// See [LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive] if the method
1228 /// is used in a live environment and accurate time control is important.
1229 ///
1230 /// The `initialOffset` argument, if non-zero, causes the pointer to first
1231 /// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
1232 /// used to simulate a drag followed by a fling, including dragging in the
1233 /// opposite direction of the fling (e.g. dragging 200 pixels to the right,
1234 /// then fling to the left over 200 pixels, ending at the exact point that the
1235 /// drag started).
1236 /// {@endtemplate}
1237 ///
1238 /// A fling is essentially a drag that ends at a particular speed. If you
1239 /// just want to drag and end without a fling, use [drag].
1240 Future<void> fling(
1241 finders.FinderBase<Element> finder,
1242 Offset offset,
1243 double speed, {
1244 int? pointer,
1245 int buttons = kPrimaryButton,
1246 Duration frameInterval = const Duration(milliseconds: 16),
1247 Offset initialOffset = Offset.zero,
1248 Duration initialOffsetDelay = const Duration(seconds: 1),
1249 bool warnIfMissed = true,
1250 PointerDeviceKind deviceKind = PointerDeviceKind.touch,
1251 }) {
1252 return flingFrom(
1253 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
1254 offset,
1255 speed,
1256 pointer: pointer,
1257 buttons: buttons,
1258 frameInterval: frameInterval,
1259 initialOffset: initialOffset,
1260 initialOffsetDelay: initialOffsetDelay,
1261 deviceKind: deviceKind,
1262 );
1263 }
1264
1265 /// Attempts a fling gesture starting from the given location, moving the
1266 /// given distance, reaching the given speed.
1267 ///
1268 /// {@macro flutter.flutter_test.WidgetController.fling}
1269 ///
1270 /// A fling is essentially a drag that ends at a particular speed. If you
1271 /// just want to drag and end without a fling, use [dragFrom].
1272 Future<void> flingFrom(
1273 Offset startLocation,
1274 Offset offset,
1275 double speed, {
1276 int? pointer,
1277 int buttons = kPrimaryButton,
1278 Duration frameInterval = const Duration(milliseconds: 16),
1279 Offset initialOffset = Offset.zero,
1280 Duration initialOffsetDelay = const Duration(seconds: 1),
1281 PointerDeviceKind deviceKind = PointerDeviceKind.touch,
1282 }) {
1283 assert(offset.distance > 0.0);
1284 assert(speed > 0.0); // speed is pixels/second
1285 return TestAsyncUtils.guard<void>(() async {
1286 final TestPointer testPointer = TestPointer(
1287 pointer ?? _getNextPointer(),
1288 deviceKind,
1289 null,
1290 buttons,
1291 );
1292 const int kMoveCount =
1293 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
1294 final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
1295 double timeStamp = 0.0;
1296 double lastTimeStamp = timeStamp;
1297 await sendEventToBinding(
1298 testPointer.down(startLocation, timeStamp: Duration(microseconds: timeStamp.round())),
1299 );
1300 if (initialOffset.distance > 0.0) {
1301 await sendEventToBinding(
1302 testPointer.move(
1303 startLocation + initialOffset,
1304 timeStamp: Duration(microseconds: timeStamp.round()),
1305 ),
1306 );
1307 timeStamp += initialOffsetDelay.inMicroseconds;
1308 await pump(initialOffsetDelay);
1309 }
1310 for (int i = 0; i <= kMoveCount; i += 1) {
1311 final Offset location =
1312 startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
1313 await sendEventToBinding(
1314 testPointer.move(location, timeStamp: Duration(microseconds: timeStamp.round())),
1315 );
1316 timeStamp += timeStampDelta;
1317 if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
1318 await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
1319 lastTimeStamp = timeStamp;
1320 }
1321 }
1322 await sendEventToBinding(
1323 testPointer.up(timeStamp: Duration(microseconds: timeStamp.round())),
1324 );
1325 });
1326 }
1327
1328 /// Attempts a trackpad fling gesture starting from the center of the given
1329 /// widget, moving the given distance, reaching the given speed. A trackpad
1330 /// fling sends PointerPanZoom events instead of a sequence of touch events.
1331 ///
1332 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1333 ///
1334 /// {@macro flutter.flutter_test.WidgetController.fling}
1335 ///
1336 /// A fling is essentially a drag that ends at a particular speed. If you
1337 /// just want to drag and end without a fling, use [drag].
1338 Future<void> trackpadFling(
1339 finders.FinderBase<Element> finder,
1340 Offset offset,
1341 double speed, {
1342 int? pointer,
1343 int buttons = kPrimaryButton,
1344 Duration frameInterval = const Duration(milliseconds: 16),
1345 Offset initialOffset = Offset.zero,
1346 Duration initialOffsetDelay = const Duration(seconds: 1),
1347 bool warnIfMissed = true,
1348 }) {
1349 return trackpadFlingFrom(
1350 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
1351 offset,
1352 speed,
1353 pointer: pointer,
1354 buttons: buttons,
1355 frameInterval: frameInterval,
1356 initialOffset: initialOffset,
1357 initialOffsetDelay: initialOffsetDelay,
1358 );
1359 }
1360
1361 /// Attempts a fling gesture starting from the given location, moving the
1362 /// given distance, reaching the given speed. A trackpad fling sends
1363 /// PointerPanZoom events instead of a sequence of touch events.
1364 ///
1365 /// {@macro flutter.flutter_test.WidgetController.fling}
1366 ///
1367 /// A fling is essentially a drag that ends at a particular speed. If you
1368 /// just want to drag and end without a fling, use [dragFrom].
1369 Future<void> trackpadFlingFrom(
1370 Offset startLocation,
1371 Offset offset,
1372 double speed, {
1373 int? pointer,
1374 int buttons = kPrimaryButton,
1375 Duration frameInterval = const Duration(milliseconds: 16),
1376 Offset initialOffset = Offset.zero,
1377 Duration initialOffsetDelay = const Duration(seconds: 1),
1378 }) {
1379 assert(offset.distance > 0.0);
1380 assert(speed > 0.0); // speed is pixels/second
1381 return TestAsyncUtils.guard<void>(() async {
1382 final TestPointer testPointer = TestPointer(
1383 pointer ?? _getNextPointer(),
1384 PointerDeviceKind.trackpad,
1385 null,
1386 buttons,
1387 );
1388 const int kMoveCount =
1389 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
1390 final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
1391 double timeStamp = 0.0;
1392 double lastTimeStamp = timeStamp;
1393 await sendEventToBinding(
1394 testPointer.panZoomStart(
1395 startLocation,
1396 timeStamp: Duration(microseconds: timeStamp.round()),
1397 ),
1398 );
1399 if (initialOffset.distance > 0.0) {
1400 await sendEventToBinding(
1401 testPointer.panZoomUpdate(
1402 startLocation,
1403 pan: initialOffset,
1404 timeStamp: Duration(microseconds: timeStamp.round()),
1405 ),
1406 );
1407 timeStamp += initialOffsetDelay.inMicroseconds;
1408 await pump(initialOffsetDelay);
1409 }
1410 for (int i = 0; i <= kMoveCount; i += 1) {
1411 final Offset pan = initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
1412 await sendEventToBinding(
1413 testPointer.panZoomUpdate(
1414 startLocation,
1415 pan: pan,
1416 timeStamp: Duration(microseconds: timeStamp.round()),
1417 ),
1418 );
1419 timeStamp += timeStampDelta;
1420 if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
1421 await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
1422 lastTimeStamp = timeStamp;
1423 }
1424 }
1425 await sendEventToBinding(
1426 testPointer.panZoomEnd(timeStamp: Duration(microseconds: timeStamp.round())),
1427 );
1428 });
1429 }
1430
1431 /// A simulator of how the framework handles a series of [PointerEvent]s
1432 /// received from the Flutter engine.
1433 ///
1434 /// The [PointerEventRecord.timeDelay] is used as the time delay of the events
1435 /// injection relative to the starting point of the method call.
1436 ///
1437 /// Returns a list of the difference between the real delay time when the
1438 /// [PointerEventRecord.events] are processed and
1439 /// [PointerEventRecord.timeDelay].
1440 /// - For [AutomatedTestWidgetsFlutterBinding] where the clock is fake, the
1441 /// return value should be exact zeros.
1442 /// - For [LiveTestWidgetsFlutterBinding], the values are typically small
1443 /// positives, meaning the event happens a little later than the set time,
1444 /// but a very small portion may have a tiny negative value for about tens of
1445 /// microseconds. This is due to the nature of [Future.delayed].
1446 ///
1447 /// The closer the return values are to zero the more faithful it is to the
1448 /// `records`.
1449 ///
1450 /// See [PointerEventRecord].
1451 Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records);
1452
1453 /// Called to indicate that there should be a new frame after an optional
1454 /// delay.
1455 ///
1456 /// The frame is pumped after a delay of [duration] if [duration] is not null,
1457 /// or immediately otherwise.
1458 ///
1459 /// This is invoked by [flingFrom], for instance, so that the sequence of
1460 /// pointer events occurs over time.
1461 ///
1462 /// The [WidgetTester] subclass implements this by deferring to the [binding].
1463 ///
1464 /// See also:
1465 ///
1466 /// * [SchedulerBinding.endOfFrame], which returns a future that could be
1467 /// appropriate to return in the implementation of this method.
1468 Future<void> pump([Duration duration]);
1469
1470 /// Repeatedly calls [pump] with the given `duration` until there are no
1471 /// longer any frames scheduled. This will call [pump] at least once, even if
1472 /// no frames are scheduled when the function is called, to flush any pending
1473 /// microtasks which may themselves schedule a frame.
1474 ///
1475 /// This essentially waits for all animations to have completed.
1476 ///
1477 /// If it takes longer that the given `timeout` to settle, then the test will
1478 /// fail (this method will throw an exception). In particular, this means that
1479 /// if there is an infinite animation in progress (for example, if there is an
1480 /// indeterminate progress indicator spinning), this method will throw.
1481 ///
1482 /// The default timeout is ten minutes, which is longer than most reasonable
1483 /// finite animations would last.
1484 ///
1485 /// If the function returns, it returns the number of pumps that it performed.
1486 ///
1487 /// In general, it is better practice to figure out exactly why each frame is
1488 /// needed, and then to [pump] exactly as many frames as necessary. This will
1489 /// help catch regressions where, for instance, an animation is being started
1490 /// one frame later than it should.
1491 ///
1492 /// Alternatively, one can check that the return value from this function
1493 /// matches the expected number of pumps.
1494 Future<int> pumpAndSettle([Duration duration = const Duration(milliseconds: 100)]);
1495
1496 /// Attempts to drag the given widget by the given offset, by
1497 /// starting a drag in the middle of the widget.
1498 ///
1499 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1500 ///
1501 /// If you want the drag to end with a speed so that the gesture recognition
1502 /// system identifies the gesture as a fling, consider using [fling] instead.
1503 ///
1504 /// The operation happens at once. If you want the drag to last for a period
1505 /// of time, consider using [timedDrag].
1506 ///
1507 /// {@macro flutter.flutter_test.WidgetController.fling.offset}
1508 ///
1509 /// {@template flutter.flutter_test.WidgetController.drag}
1510 /// By default, if the x or y component of offset is greater than
1511 /// [kDragSlopDefault], the gesture is broken up into two separate moves
1512 /// calls. Changing `touchSlopX` or `touchSlopY` will change the minimum
1513 /// amount of movement in the respective axis before the drag will be broken
1514 /// into multiple calls. To always send the drag with just a single call to
1515 /// [TestGesture.moveBy], `touchSlopX` and `touchSlopY` should be set to 0.
1516 ///
1517 /// Breaking the drag into multiple moves is necessary for accurate execution
1518 /// of drag update calls with a [DragStartBehavior] variable set to
1519 /// [DragStartBehavior.start]. Without such a change, the dragUpdate callback
1520 /// from a drag recognizer will never be invoked.
1521 ///
1522 /// To force this function to a send a single move event, the `touchSlopX` and
1523 /// `touchSlopY` variables should be set to 0. However, generally, these values
1524 /// should be left to their default values.
1525 /// {@endtemplate}
1526 Future<void> drag(
1527 finders.FinderBase<Element> finder,
1528 Offset offset, {
1529 int? pointer,
1530 int buttons = kPrimaryButton,
1531 double touchSlopX = kDragSlopDefault,
1532 double touchSlopY = kDragSlopDefault,
1533 bool warnIfMissed = true,
1534 PointerDeviceKind kind = PointerDeviceKind.touch,
1535 }) {
1536 return dragFrom(
1537 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
1538 offset,
1539 pointer: pointer,
1540 buttons: buttons,
1541 touchSlopX: touchSlopX,
1542 touchSlopY: touchSlopY,
1543 kind: kind,
1544 );
1545 }
1546
1547 /// Attempts a drag gesture consisting of a pointer down, a move by
1548 /// the given offset, and a pointer up.
1549 ///
1550 /// If you want the drag to end with a speed so that the gesture recognition
1551 /// system identifies the gesture as a fling, consider using [flingFrom]
1552 /// instead.
1553 ///
1554 /// The operation happens at once. If you want the drag to last for a period
1555 /// of time, consider using [timedDragFrom].
1556 ///
1557 /// {@macro flutter.flutter_test.WidgetController.drag}
1558 Future<void> dragFrom(
1559 Offset startLocation,
1560 Offset offset, {
1561 int? pointer,
1562 int buttons = kPrimaryButton,
1563 double touchSlopX = kDragSlopDefault,
1564 double touchSlopY = kDragSlopDefault,
1565 PointerDeviceKind kind = PointerDeviceKind.touch,
1566 }) {
1567 assert(kDragSlopDefault > kTouchSlop);
1568 return TestAsyncUtils.guard<void>(() async {
1569 final TestGesture gesture = await startGesture(
1570 startLocation,
1571 pointer: pointer,
1572 buttons: buttons,
1573 kind: kind,
1574 );
1575
1576 final double xSign = offset.dx.sign;
1577 final double ySign = offset.dy.sign;
1578
1579 final double offsetX = offset.dx;
1580 final double offsetY = offset.dy;
1581
1582 final bool separateX = offset.dx.abs() > touchSlopX && touchSlopX > 0;
1583 final bool separateY = offset.dy.abs() > touchSlopY && touchSlopY > 0;
1584
1585 if (separateY || separateX) {
1586 final double offsetSlope = offsetY / offsetX;
1587 final double inverseOffsetSlope = offsetX / offsetY;
1588 final double slopSlope = touchSlopY / touchSlopX;
1589 final double absoluteOffsetSlope = offsetSlope.abs();
1590 final double signedSlopX = touchSlopX * xSign;
1591 final double signedSlopY = touchSlopY * ySign;
1592 if (absoluteOffsetSlope != slopSlope) {
1593 // The drag goes through one or both of the extents of the edges of the box.
1594 if (absoluteOffsetSlope < slopSlope) {
1595 assert(offsetX.abs() > touchSlopX);
1596 // The drag goes through the vertical edge of the box.
1597 // It is guaranteed that the |offsetX| > touchSlopX.
1598 final double diffY = offsetSlope.abs() * touchSlopX * ySign;
1599
1600 // The vector from the origin to the vertical edge.
1601 await gesture.moveBy(Offset(signedSlopX, diffY));
1602 if (offsetY.abs() <= touchSlopY) {
1603 // The drag ends on or before getting to the horizontal extension of the horizontal edge.
1604 await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY));
1605 } else {
1606 final double diffY2 = signedSlopY - diffY;
1607 final double diffX2 = inverseOffsetSlope * diffY2;
1608
1609 // The vector from the edge of the box to the horizontal extension of the horizontal edge.
1610 await gesture.moveBy(Offset(diffX2, diffY2));
1611 await gesture.moveBy(Offset(offsetX - diffX2 - signedSlopX, offsetY - signedSlopY));
1612 }
1613 } else {
1614 assert(offsetY.abs() > touchSlopY);
1615 // The drag goes through the horizontal edge of the box.
1616 // It is guaranteed that the |offsetY| > touchSlopY.
1617 final double diffX = inverseOffsetSlope.abs() * touchSlopY * xSign;
1618
1619 // The vector from the origin to the vertical edge.
1620 await gesture.moveBy(Offset(diffX, signedSlopY));
1621 if (offsetX.abs() <= touchSlopX) {
1622 // The drag ends on or before getting to the vertical extension of the vertical edge.
1623 await gesture.moveBy(Offset(offsetX - diffX, offsetY - signedSlopY));
1624 } else {
1625 final double diffX2 = signedSlopX - diffX;
1626 final double diffY2 = offsetSlope * diffX2;
1627
1628 // The vector from the edge of the box to the vertical extension of the vertical edge.
1629 await gesture.moveBy(Offset(diffX2, diffY2));
1630 await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY2 - signedSlopY));
1631 }
1632 }
1633 } else {
1634 // The drag goes through the corner of the box.
1635 await gesture.moveBy(Offset(signedSlopX, signedSlopY));
1636 await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - signedSlopY));
1637 }
1638 } else {
1639 // The drag ends inside the box.
1640 await gesture.moveBy(offset);
1641 }
1642 await gesture.up();
1643 });
1644 }
1645
1646 /// Attempts to drag the given widget by the given offset in the `duration`
1647 /// time, starting in the middle of the widget.
1648 ///
1649 /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1650 ///
1651 /// {@macro flutter.flutter_test.WidgetController.fling.offset}
1652 ///
1653 /// This is the timed version of [drag]. This may or may not result in a
1654 /// [fling] or ballistic animation, depending on the speed from
1655 /// `offset/duration`.
1656 ///
1657 /// {@template flutter.flutter_test.WidgetController.timedDrag}
1658 /// The move events are sent at a given `frequency` in Hz (or events per
1659 /// second). It defaults to 60Hz.
1660 ///
1661 /// The movement is linear in time.
1662 ///
1663 /// See also [LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive] for
1664 /// more accurate time control.
1665 /// {@endtemplate}
1666 Future<void> timedDrag(
1667 finders.FinderBase<Element> finder,
1668 Offset offset,
1669 Duration duration, {
1670 int? pointer,
1671 int buttons = kPrimaryButton,
1672 double frequency = 60.0,
1673 bool warnIfMissed = true,
1674 }) {
1675 return timedDragFrom(
1676 getCenter(finder, warnIfMissed: warnIfMissed, callee: 'timedDrag'),
1677 offset,
1678 duration,
1679 pointer: pointer,
1680 buttons: buttons,
1681 frequency: frequency,
1682 );
1683 }
1684
1685 /// Attempts a series of [PointerEvent]s to simulate a drag operation in the
1686 /// `duration` time.
1687 ///
1688 /// This is the timed version of [dragFrom]. This may or may not result in a
1689 /// [flingFrom] or ballistic animation, depending on the speed from
1690 /// `offset/duration`.
1691 ///
1692 /// {@macro flutter.flutter_test.WidgetController.timedDrag}
1693 Future<void> timedDragFrom(
1694 Offset startLocation,
1695 Offset offset,
1696 Duration duration, {
1697 int? pointer,
1698 int buttons = kPrimaryButton,
1699 double frequency = 60.0,
1700 }) {
1701 assert(frequency > 0);
1702 final int intervals = duration.inMicroseconds * frequency ~/ 1E6;
1703 assert(intervals > 1);
1704 pointer ??= _getNextPointer();
1705 final List<Duration> timeStamps = <Duration>[
1706 for (int t = 0; t <= intervals; t += 1) duration * t ~/ intervals,
1707 ];
1708 final List<Offset> offsets = <Offset>[
1709 startLocation,
1710 for (int t = 0; t <= intervals; t += 1) startLocation + offset * (t / intervals),
1711 ];
1712 final List<PointerEventRecord> records = <PointerEventRecord>[
1713 PointerEventRecord(Duration.zero, <PointerEvent>[
1714 PointerAddedEvent(position: startLocation),
1715 PointerDownEvent(position: startLocation, pointer: pointer, buttons: buttons),
1716 ]),
1717 ...<PointerEventRecord>[
1718 for (int t = 0; t <= intervals; t += 1)
1719 PointerEventRecord(timeStamps[t], <PointerEvent>[
1720 PointerMoveEvent(
1721 timeStamp: timeStamps[t],
1722 position: offsets[t + 1],
1723 delta: offsets[t + 1] - offsets[t],
1724 pointer: pointer,
1725 buttons: buttons,
1726 ),
1727 ]),
1728 ],
1729 PointerEventRecord(duration, <PointerEvent>[
1730 PointerUpEvent(
1731 timeStamp: duration,
1732 position: offsets.last,
1733 pointer: pointer,
1734 // The PointerData received from the engine with
1735 // change = PointerChange.up, which translates to PointerUpEvent,
1736 // doesn't provide the button field.
1737 // buttons: buttons,
1738 ),
1739 ]),
1740 ];
1741 return TestAsyncUtils.guard<void>(() async {
1742 await handlePointerEventRecord(records);
1743 });
1744 }
1745
1746 /// The next available pointer identifier.
1747 ///
1748 /// This is the default pointer identifier that will be used the next time the
1749 /// [startGesture] method is called without an explicit pointer identifier.
1750 int get nextPointer => _nextPointer;
1751
1752 static int _nextPointer = 1;
1753
1754 static int _getNextPointer() {
1755 final int result = _nextPointer;
1756 _nextPointer += 1;
1757 return result;
1758 }
1759
1760 TestGesture _createGesture({
1761 int? pointer,
1762 required PointerDeviceKind kind,
1763 required int buttons,
1764 }) {
1765 return TestGesture(
1766 dispatcher: sendEventToBinding,
1767 kind: kind,
1768 pointer: pointer ?? _getNextPointer(),
1769 buttons: buttons,
1770 );
1771 }
1772
1773 /// Creates gesture and returns the [TestGesture] object which you can use
1774 /// to continue the gesture using calls on the [TestGesture] object.
1775 ///
1776 /// You can use [startGesture] instead if your gesture begins with a down
1777 /// event.
1778 Future<TestGesture> createGesture({
1779 int? pointer,
1780 PointerDeviceKind kind = PointerDeviceKind.touch,
1781 int buttons = kPrimaryButton,
1782 }) async {
1783 return _createGesture(pointer: pointer, kind: kind, buttons: buttons);
1784 }
1785
1786 /// Creates a gesture with an initial appropriate starting gesture at a
1787 /// particular point, and returns the [TestGesture] object which you can use
1788 /// to continue the gesture. Usually, the starting gesture will be a down event,
1789 /// but if [kind] is set to [PointerDeviceKind.trackpad], the gesture will start
1790 /// with a panZoomStart gesture.
1791 ///
1792 /// You can use [createGesture] if your gesture doesn't begin with an initial
1793 /// down or panZoomStart gesture.
1794 ///
1795 /// See also:
1796 /// * [WidgetController.drag], a method to simulate a drag.
1797 /// * [WidgetController.timedDrag], a method to simulate the drag of a given
1798 /// widget in a given duration. It sends move events at a given frequency and
1799 /// it is useful when there are listeners involved.
1800 /// * [WidgetController.fling], a method to simulate a fling.
1801 Future<TestGesture> startGesture(
1802 Offset downLocation, {
1803 int? pointer,
1804 PointerDeviceKind kind = PointerDeviceKind.touch,
1805 int buttons = kPrimaryButton,
1806 }) async {
1807 final TestGesture result = _createGesture(pointer: pointer, kind: kind, buttons: buttons);
1808 if (kind == PointerDeviceKind.trackpad) {
1809 await result.panZoomStart(downLocation);
1810 } else {
1811 await result.down(downLocation);
1812 }
1813 return result;
1814 }
1815
1816 /// Forwards the given location to the binding's hitTest logic.
1817 HitTestResult hitTestOnBinding(Offset location, {int? viewId}) {
1818 viewId ??= view.viewId;
1819 final HitTestResult result = HitTestResult();
1820 binding.hitTestInView(result, location, viewId);
1821 return result;
1822 }
1823
1824 /// Forwards the given pointer event to the binding.
1825 Future<void> sendEventToBinding(PointerEvent event) {
1826 return TestAsyncUtils.guard<void>(() async {
1827 binding.handlePointerEvent(event);
1828 });
1829 }
1830
1831 /// Calls [debugPrint] with the given message.
1832 ///
1833 /// This is overridden by the WidgetTester subclass to use the test binding's
1834 /// [TestWidgetsFlutterBinding.debugPrintOverride], so that it appears on the
1835 /// console even if the test is logging output from the application.
1836 @protected
1837 void printToConsole(String message) {
1838 debugPrint(message);
1839 }
1840
1841 // GEOMETRY
1842
1843 /// Returns the point at the center of the given widget.
1844 ///
1845 /// {@template flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1846 /// If `warnIfMissed` is true (the default is false), then the returned
1847 /// coordinate is checked to see if a hit test at the returned location would
1848 /// actually include the specified element in the [HitTestResult], and if not,
1849 /// a warning is printed to the console.
1850 ///
1851 /// The `callee` argument is used to identify the method that should be
1852 /// referenced in messages regarding `warnIfMissed`. It can be ignored unless
1853 /// this method is being called from another that is forwarding its own
1854 /// `warnIfMissed` parameter (see e.g. the implementation of [tap]).
1855 /// {@endtemplate}
1856 Offset getCenter(
1857 finders.FinderBase<Element> finder, {
1858 bool warnIfMissed = false,
1859 String callee = 'getCenter',
1860 }) {
1861 return _getElementPoint(
1862 finder,
1863 (Size size) => size.center(Offset.zero),
1864 warnIfMissed: warnIfMissed,
1865 callee: callee,
1866 );
1867 }
1868
1869 /// Returns the point at the top left of the given widget.
1870 ///
1871 /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1872 Offset getTopLeft(
1873 finders.FinderBase<Element> finder, {
1874 bool warnIfMissed = false,
1875 String callee = 'getTopLeft',
1876 }) {
1877 return _getElementPoint(
1878 finder,
1879 (Size size) => Offset.zero,
1880 warnIfMissed: warnIfMissed,
1881 callee: callee,
1882 );
1883 }
1884
1885 /// Returns the point at the top right of the given widget. This
1886 /// point is not inside the object's hit test area.
1887 ///
1888 /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1889 Offset getTopRight(
1890 finders.FinderBase<Element> finder, {
1891 bool warnIfMissed = false,
1892 String callee = 'getTopRight',
1893 }) {
1894 return _getElementPoint(
1895 finder,
1896 (Size size) => size.topRight(Offset.zero),
1897 warnIfMissed: warnIfMissed,
1898 callee: callee,
1899 );
1900 }
1901
1902 /// Returns the point at the bottom left of the given widget. This
1903 /// point is not inside the object's hit test area.
1904 ///
1905 /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1906 Offset getBottomLeft(
1907 finders.FinderBase<Element> finder, {
1908 bool warnIfMissed = false,
1909 String callee = 'getBottomLeft',
1910 }) {
1911 return _getElementPoint(
1912 finder,
1913 (Size size) => size.bottomLeft(Offset.zero),
1914 warnIfMissed: warnIfMissed,
1915 callee: callee,
1916 );
1917 }
1918
1919 /// Returns the point at the bottom right of the given widget. This
1920 /// point is not inside the object's hit test area.
1921 ///
1922 /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1923 Offset getBottomRight(
1924 finders.FinderBase<Element> finder, {
1925 bool warnIfMissed = false,
1926 String callee = 'getBottomRight',
1927 }) {
1928 return _getElementPoint(
1929 finder,
1930 (Size size) => size.bottomRight(Offset.zero),
1931 warnIfMissed: warnIfMissed,
1932 callee: callee,
1933 );
1934 }
1935
1936 /// Whether warnings relating to hit tests not hitting their mark should be
1937 /// fatal (cause the test to fail).
1938 ///
1939 /// Some methods, e.g. [tap], have an argument `warnIfMissed` which causes a
1940 /// warning to be displayed if the specified [Finder] indicates a widget and
1941 /// location that, were a pointer event to be sent to that location, would not
1942 /// actually send any events to the widget (e.g. because the widget is
1943 /// obscured, or the location is off-screen, or the widget is transparent to
1944 /// pointer events).
1945 ///
1946 /// This warning was added in 2021. In ordinary operation this warning is
1947 /// non-fatal since making it fatal would be a significantly breaking change
1948 /// for anyone who already has tests relying on the ability to target events
1949 /// using finders where the events wouldn't reach the widgets specified by the
1950 /// finders in question.
1951 ///
1952 /// However, doing this is usually unintentional. To make the warning fatal,
1953 /// thus failing any tests where it occurs, this property can be set to true.
1954 ///
1955 /// Typically this is done using a `flutter_test_config.dart` file, as described
1956 /// in the documentation for the [flutter_test] library.
1957 static bool hitTestWarningShouldBeFatal = false;
1958
1959 /// Finds one hit-testable Offset in the given `textRangeContext`'s render
1960 /// object.
1961 Offset? _findHitTestableOffsetIn(finders.TextRangeContext textRangeContext) {
1962 TestAsyncUtils.guardSync();
1963 final TextRange range = textRangeContext.textRange;
1964 assert(range.isNormalized);
1965 assert(range.isValid);
1966 final Offset renderParagraphPaintOffset = textRangeContext.renderObject.localToGlobal(
1967 Offset.zero,
1968 );
1969 assert(renderParagraphPaintOffset.isFinite);
1970
1971 int spanStart = range.start;
1972 while (spanStart < range.end) {
1973 switch (_findEndOfSpan(textRangeContext.renderObject.text, spanStart, range.end)) {
1974 case (final HitTestTarget target, final int endIndex):
1975 // Uses BoxHeightStyle.tight in getBoxesForSelection to make sure the
1976 // returned boxes don't extend outside of the hit-testable region.
1977 final Iterable<Offset> testOffsets = textRangeContext.renderObject
1978 .getBoxesForSelection(TextSelection(baseOffset: spanStart, extentOffset: endIndex))
1979 // Try hit-testing the center of each TextBox.
1980 .map((TextBox textBox) => textBox.toRect().center);
1981
1982 for (final Offset localOffset in testOffsets) {
1983 final HitTestResult result = HitTestResult();
1984 final Offset globalOffset = localOffset + renderParagraphPaintOffset;
1985 binding.hitTestInView(result, globalOffset, textRangeContext.view.view.viewId);
1986 if (result.path.any((HitTestEntry entry) => entry.target == target)) {
1987 return globalOffset;
1988 }
1989 }
1990 spanStart = endIndex;
1991 case (_, final int endIndex):
1992 spanStart = endIndex;
1993 case null:
1994 break;
1995 }
1996 }
1997 return null;
1998 }
1999
2000 Offset _getElementPoint(
2001 finders.FinderBase<Element> finder,
2002 Offset Function(Size size) sizeToPoint, {
2003 required bool warnIfMissed,
2004 required String callee,
2005 }) {
2006 TestAsyncUtils.guardSync();
2007 final Iterable<Element> elements = finder.evaluate();
2008 if (elements.isEmpty) {
2009 throw FlutterError(
2010 'The finder "$finder" (used in a call to "$callee()") could not find any matching widgets.',
2011 );
2012 }
2013 if (elements.length > 1) {
2014 throw FlutterError(
2015 'The finder "$finder" (used in a call to "$callee()") ambiguously found multiple matching widgets. The "$callee()" method needs a single target.',
2016 );
2017 }
2018 final Element element = elements.single;
2019 final RenderObject? renderObject = element.renderObject;
2020 if (renderObject == null) {
2021 throw FlutterError(
2022 'The finder "$finder" (used in a call to "$callee()") found an element, but it does not have a corresponding render object. '
2023 'Maybe the element has not yet been rendered?',
2024 );
2025 }
2026 if (renderObject is! RenderBox) {
2027 throw FlutterError(
2028 'The finder "$finder" (used in a call to "$callee()") found an element whose corresponding render object is not a RenderBox (it is a ${renderObject.runtimeType}: "$renderObject"). '
2029 'Unfortunately "$callee()" only supports targeting widgets that correspond to RenderBox objects in the rendering.',
2030 );
2031 }
2032 final RenderBox box = element.renderObject! as RenderBox;
2033 final Offset location = box.localToGlobal(sizeToPoint(box.size));
2034 if (warnIfMissed) {
2035 final FlutterView view = _viewOf(finder);
2036 final HitTestResult result = HitTestResult();
2037 binding.hitTestInView(result, location, view.viewId);
2038 final bool found = result.path.any((HitTestEntry entry) => entry.target == box);
2039 if (!found) {
2040 final RenderView renderView = binding.renderViews.firstWhere(
2041 (RenderView r) => r.flutterView == view,
2042 );
2043 final bool outOfBounds = !(Offset.zero & renderView.size).contains(location);
2044 if (hitTestWarningShouldBeFatal) {
2045 throw FlutterError.fromParts(<DiagnosticsNode>[
2046 ErrorSummary('Finder specifies a widget that would not receive pointer events.'),
2047 ErrorDescription(
2048 'A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.',
2049 ),
2050 ErrorHint(
2051 'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.',
2052 ),
2053 if (outOfBounds)
2054 ErrorHint(
2055 'Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.',
2056 ),
2057 box.toDiagnosticsNode(
2058 name: 'The finder corresponds to this RenderBox',
2059 style: DiagnosticsTreeStyle.singleLine,
2060 ),
2061 ErrorDescription('The hit test result at that offset is: $result'),
2062 ErrorDescription(
2063 'If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".',
2064 ),
2065 ErrorDescription(
2066 'To make this error into a non-fatal warning, set WidgetController.hitTestWarningShouldBeFatal to false.',
2067 ),
2068 ]);
2069 }
2070 printToConsole(
2071 '\n'
2072 'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n'
2073 'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n'
2074 '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.\n" : ""}'
2075 'The finder corresponds to this RenderBox: $box\n'
2076 'The hit test result at that offset is: $result\n'
2077 '${StackTrace.current}'
2078 'To silence this warning, pass "warnIfMissed: false" to "$callee()".\n'
2079 'To make this warning fatal, set WidgetController.hitTestWarningShouldBeFatal to true.\n',
2080 );
2081 }
2082 }
2083 return location;
2084 }
2085
2086 /// Returns the size of the given widget. This is only valid once
2087 /// the widget's render object has been laid out at least once.
2088 Size getSize(finders.FinderBase<Element> finder) {
2089 TestAsyncUtils.guardSync();
2090 final Element element = finder.evaluate().single;
2091 final RenderBox box = element.renderObject! as RenderBox;
2092 return box.size;
2093 }
2094
2095 /// Simulates sending physical key down and up events.
2096 ///
2097 /// This only simulates key events coming from a physical keyboard, not from a
2098 /// soft keyboard.
2099 ///
2100 /// Specify `platform` as one of the platforms allowed in
2101 /// [Platform.operatingSystem] to make the event appear to be from
2102 /// that type of system. If not specified, defaults to "web" on web, and the
2103 /// operating system name based on [defaultTargetPlatform] everywhere else.
2104 ///
2105 /// Specify the `physicalKey` for the event to override what is included in
2106 /// the simulated event. If not specified, it uses a default from the US
2107 /// keyboard layout for the corresponding logical `key`.
2108 ///
2109 /// Specify the `character` for the event to override what is included in the
2110 /// simulated event. If not specified, it uses a default derived from the
2111 /// logical `key`.
2112 ///
2113 /// Keys that are down when the test completes are cleared after each test.
2114 ///
2115 /// This method sends both the key down and the key up events, to simulate a
2116 /// key press. To simulate individual down and/or up events, see
2117 /// [sendKeyDownEvent] and [sendKeyUpEvent].
2118 ///
2119 /// Returns true if the key down event was handled by the framework.
2120 ///
2121 /// See also:
2122 ///
2123 /// - [sendKeyDownEvent] to simulate only a key down event.
2124 /// - [sendKeyUpEvent] to simulate only a key up event.
2125 Future<bool> sendKeyEvent(
2126 LogicalKeyboardKey key, {
2127 String? platform,
2128 String? character,
2129 PhysicalKeyboardKey? physicalKey,
2130 }) async {
2131 final bool handled = await simulateKeyDownEvent(
2132 key,
2133 platform: platform,
2134 character: character,
2135 physicalKey: physicalKey,
2136 );
2137 // Internally wrapped in async guard.
2138 await simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
2139 return handled;
2140 }
2141
2142 /// Simulates sending a physical key down event.
2143 ///
2144 /// This only simulates key down events coming from a physical keyboard, not
2145 /// from a soft keyboard.
2146 ///
2147 /// Specify `platform` as one of the platforms allowed in
2148 /// [Platform.operatingSystem] to make the event appear to be from
2149 /// that type of system. If not specified, defaults to "web" on web, and the
2150 /// operating system name based on [defaultTargetPlatform] everywhere else.
2151 ///
2152 /// Specify the `physicalKey` for the event to override what is included in
2153 /// the simulated event. If not specified, it uses a default from the US
2154 /// keyboard layout for the corresponding logical `key`.
2155 ///
2156 /// Specify the `character` for the event to override what is included in the
2157 /// simulated event. If not specified, it uses a default derived from the
2158 /// logical `key`.
2159 ///
2160 /// Keys that are down when the test completes are cleared after each test.
2161 ///
2162 /// Returns true if the key event was handled by the framework.
2163 ///
2164 /// See also:
2165 ///
2166 /// - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
2167 /// key up and repeat event.
2168 /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
2169 Future<bool> sendKeyDownEvent(
2170 LogicalKeyboardKey key, {
2171 String? platform,
2172 String? character,
2173 PhysicalKeyboardKey? physicalKey,
2174 }) async {
2175 // Internally wrapped in async guard.
2176 return simulateKeyDownEvent(
2177 key,
2178 platform: platform,
2179 character: character,
2180 physicalKey: physicalKey,
2181 );
2182 }
2183
2184 /// Simulates sending a physical key up event through the system channel.
2185 ///
2186 /// This only simulates key up events coming from a physical keyboard,
2187 /// not from a soft keyboard.
2188 ///
2189 /// Specify `platform` as one of the platforms allowed in
2190 /// [Platform.operatingSystem] to make the event appear to be from
2191 /// that type of system. If not specified, defaults to "web" on web, and the
2192 /// operating system name based on [defaultTargetPlatform] everywhere else.
2193 ///
2194 /// Specify the `physicalKey` for the event to override what is included in
2195 /// the simulated event. If not specified, it uses a default from the US
2196 /// keyboard layout for the corresponding logical `key`.
2197 ///
2198 /// Returns true if the key event was handled by the framework.
2199 ///
2200 /// See also:
2201 ///
2202 /// - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
2203 /// corresponding key down and repeat event.
2204 /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
2205 Future<bool> sendKeyUpEvent(
2206 LogicalKeyboardKey key, {
2207 String? platform,
2208 PhysicalKeyboardKey? physicalKey,
2209 }) async {
2210 // Internally wrapped in async guard.
2211 return simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
2212 }
2213
2214 /// Simulates sending a key repeat event from a physical keyboard.
2215 ///
2216 /// This only simulates key repeat events coming from a physical keyboard, not
2217 /// from a soft keyboard.
2218 ///
2219 /// Specify `platform` as one of the platforms allowed in
2220 /// [Platform.operatingSystem] to make the event appear to be from
2221 /// that type of system. If not specified, defaults to "web" on web, and the
2222 /// operating system name based on [defaultTargetPlatform] everywhere else.
2223 ///
2224 /// Specify the `physicalKey` for the event to override what is included in
2225 /// the simulated event. If not specified, it uses a default from the US
2226 /// keyboard layout for the corresponding logical `key`.
2227 ///
2228 /// Specify the `character` for the event to override what is included in the
2229 /// simulated event. If not specified, it uses a default derived from the
2230 /// logical `key`.
2231 ///
2232 /// Keys that are down when the test completes are cleared after each test.
2233 ///
2234 /// Returns true if the key event was handled by the framework.
2235 ///
2236 /// See also:
2237 ///
2238 /// - [sendKeyDownEvent] and [sendKeyUpEvent] to simulate the corresponding
2239 /// key down and up event.
2240 /// - [sendKeyEvent] to simulate both the key up and key down in the same call.
2241 Future<bool> sendKeyRepeatEvent(
2242 LogicalKeyboardKey key, {
2243 String? platform,
2244 String? character,
2245 PhysicalKeyboardKey? physicalKey,
2246 }) async {
2247 // Internally wrapped in async guard.
2248 return simulateKeyRepeatEvent(
2249 key,
2250 platform: platform,
2251 character: character,
2252 physicalKey: physicalKey,
2253 );
2254 }
2255
2256 /// Returns the rect of the given widget. This is only valid once
2257 /// the widget's render object has been laid out at least once.
2258 Rect getRect(finders.FinderBase<Element> finder) =>
2259 Rect.fromPoints(getTopLeft(finder), getBottomRight(finder));
2260
2261 /// Attempts to find the [SemanticsNode] of first result from `finder`.
2262 ///
2263 /// If the object identified by the finder doesn't own it's semantic node,
2264 /// this will return the semantics data of the first ancestor with semantics.
2265 /// The ancestor's semantic data will include the child's as well as
2266 /// other nodes that have been merged together.
2267 ///
2268 /// If the [SemanticsNode] of the object identified by the finder is
2269 /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget)
2270 /// the node into which it is merged is returned. That node will include
2271 /// all the semantics information of the nodes merged into it.
2272 ///
2273 /// Will throw a [StateError] if the finder returns more than one element or
2274 /// if no semantics are found or are not enabled.
2275 // TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670.
2276 SemanticsNode getSemantics(finders.FinderBase<Element> finder) => semantics.find(finder);
2277
2278 /// Enable semantics in a test by creating a [SemanticsHandle].
2279 ///
2280 /// The handle must be disposed at the end of the test.
2281 SemanticsHandle ensureSemantics() {
2282 return binding.ensureSemantics();
2283 }
2284
2285 /// Given a widget `W` specified by [finder] and a [Scrollable] widget `S` in
2286 /// its ancestry tree, this scrolls `S` so as to make `W` visible.
2287 ///
2288 /// Usually the `finder` for this method should be labeled `skipOffstage:
2289 /// false`, so that the [Finder] deals with widgets that are off the screen
2290 /// correctly.
2291 ///
2292 /// This does not work when `S` is long enough, and `W` far away enough from
2293 /// the displayed part of `S`, that `S` has not yet cached `W`'s element.
2294 /// Consider using [scrollUntilVisible] in such a situation.
2295 ///
2296 /// See also:
2297 ///
2298 /// * [Scrollable.ensureVisible], which is the production API used to
2299 /// implement this method.
2300 Future<void> ensureVisible(finders.FinderBase<Element> finder) =>
2301 Scrollable.ensureVisible(element(finder));
2302
2303 /// Repeatedly scrolls a [Scrollable] by `delta` in the
2304 /// [Scrollable.axisDirection] direction until a widget matching `finder` is
2305 /// visible.
2306 ///
2307 /// Between each scroll, advances the clock by `duration` time.
2308 ///
2309 /// Scrolling is performed until the start of the `finder` is visible. This is
2310 /// due to the default parameter values of the [Scrollable.ensureVisible] method.
2311 ///
2312 /// If `scrollable` is `null`, a [Finder] that looks for a [Scrollable] is
2313 /// used instead.
2314 ///
2315 /// If `continuous` is `true`, the gesture will be reused to simulate the effect
2316 /// of actual finger scrolling, which is useful when used alongside listeners
2317 /// like [GestureDetector.onTap]. The default is `false`.
2318 ///
2319 /// Throws a [StateError] if `finder` is not found after `maxScrolls` scrolls.
2320 ///
2321 /// This is different from [ensureVisible] in that this allows looking for
2322 /// `finder` that is not yet built. The caller must specify the scrollable
2323 /// that will build child specified by `finder` when there are multiple
2324 /// [Scrollable]s.
2325 ///
2326 /// See also:
2327 ///
2328 /// * [dragUntilVisible], which implements the body of this method.
2329 Future<void> scrollUntilVisible(
2330 finders.FinderBase<Element> finder,
2331 double delta, {
2332 finders.FinderBase<Element>? scrollable,
2333 int maxScrolls = 50,
2334 Duration duration = const Duration(milliseconds: 50),
2335 bool continuous = false,
2336 }) {
2337 assert(maxScrolls > 0);
2338 scrollable ??= finders.find.byType(Scrollable);
2339 return TestAsyncUtils.guard<void>(() async {
2340 Offset moveStep;
2341 switch (widget<Scrollable>(scrollable!).axisDirection) {
2342 case AxisDirection.up:
2343 moveStep = Offset(0, delta);
2344 case AxisDirection.down:
2345 moveStep = Offset(0, -delta);
2346 case AxisDirection.left:
2347 moveStep = Offset(delta, 0);
2348 case AxisDirection.right:
2349 moveStep = Offset(-delta, 0);
2350 }
2351 await dragUntilVisible(
2352 finder,
2353 scrollable,
2354 moveStep,
2355 maxIteration: maxScrolls,
2356 duration: duration,
2357 continuous: continuous,
2358 );
2359 });
2360 }
2361
2362 /// Repeatedly drags `view` by `moveStep` until `finder` is visible.
2363 ///
2364 /// Between each drag, advances the clock by `duration`.
2365 ///
2366 /// Throws a [StateError] if `finder` is not found after `maxIteration`
2367 /// drags.
2368 ///
2369 /// See also:
2370 ///
2371 /// * [scrollUntilVisible], which wraps this method with an API that is more
2372 /// convenient when dealing with a [Scrollable].
2373 Future<void> dragUntilVisible(
2374 finders.FinderBase<Element> finder,
2375 finders.FinderBase<Element> view,
2376 Offset moveStep, {
2377 int maxIteration = 50,
2378 Duration duration = const Duration(milliseconds: 50),
2379 bool continuous = false,
2380 }) {
2381 return TestAsyncUtils.guard<void>(() async {
2382 TestGesture? gesture;
2383 while (maxIteration > 0 && finder.evaluate().isEmpty) {
2384 if (continuous) {
2385 gesture ??= await startGesture(getCenter(view, warnIfMissed: true));
2386 await gesture.moveBy(moveStep);
2387 } else {
2388 await drag(view, moveStep);
2389 }
2390 await pump(duration);
2391 maxIteration -= 1;
2392 }
2393 await gesture?.up();
2394 await Scrollable.ensureVisible(element(finder));
2395 });
2396 }
2397}
2398
2399/// Variant of [WidgetController] that can be used in tests running
2400/// on a device.
2401///
2402/// This is used, for instance, by [FlutterDriver].
2403class LiveWidgetController extends WidgetController {
2404 /// Creates a widget controller that uses the given binding.
2405 LiveWidgetController(super.binding);
2406
2407 @override
2408 Future<void> pump([Duration? duration]) async {
2409 if (duration != null) {
2410 await Future<void>.delayed(duration);
2411 }
2412 binding.scheduleFrame();
2413 await binding.endOfFrame;
2414 }
2415
2416 @override
2417 Future<int> pumpAndSettle([Duration duration = const Duration(milliseconds: 100)]) {
2418 assert(duration > Duration.zero);
2419 return TestAsyncUtils.guard<int>(() async {
2420 int count = 0;
2421 do {
2422 await pump(duration);
2423 count += 1;
2424 } while (binding.hasScheduledFrame);
2425 return count;
2426 });
2427 }
2428
2429 @override
2430 Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
2431 assert(records.isNotEmpty);
2432 return TestAsyncUtils.guard<List<Duration>>(() async {
2433 final List<Duration> handleTimeStampDiff = <Duration>[];
2434 DateTime? startTime;
2435 for (final PointerEventRecord record in records) {
2436 final DateTime now = clock.now();
2437 startTime ??= now;
2438 // So that the first event is promised to receive a zero timeDiff.
2439 final Duration timeDiff = record.timeDelay - now.difference(startTime);
2440 if (timeDiff.isNegative) {
2441 // This happens when something (e.g. GC) takes a long time during the
2442 // processing of the events.
2443 // Flush all past events.
2444 handleTimeStampDiff.add(-timeDiff);
2445 record.events.forEach(binding.handlePointerEvent);
2446 } else {
2447 await Future<void>.delayed(timeDiff);
2448 handleTimeStampDiff.add(
2449 // Recalculating the time diff for getting exact time when the event
2450 // packet is sent. For a perfect Future.delayed like the one in a
2451 // fake async this new diff should be zero.
2452 clock.now().difference(startTime) - record.timeDelay,
2453 );
2454 record.events.forEach(binding.handlePointerEvent);
2455 }
2456 }
2457
2458 return handleTimeStampDiff;
2459 });
2460 }
2461}
2462

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com