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

Provided by KDAB

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