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'; |
14 | library; |
15 | |
16 | import 'package:clock/clock.dart'; |
17 | import 'package:flutter/foundation.dart'; |
18 | import 'package:flutter/gestures.dart'; |
19 | import 'package:flutter/rendering.dart'; |
20 | import 'package:flutter/services.dart'; |
21 | import 'package:flutter/widgets.dart'; |
22 | |
23 | import 'event_simulation.dart'; |
24 | import 'finders.dart' as finders; |
25 | import 'test_async_utils.dart'; |
26 | import 'test_pointer.dart'; |
27 | import 'tree_traversal.dart'; |
28 | import '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]. |
34 | const 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. |
64 | class 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. |
707 | abstract 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]. |
2240 | class 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 |
Definitions
- kDragSlopDefault
- _findEndOfSpan
- SemanticsController
- _
- find
- simulatedAccessibilityTraversal
- _accessibilityTraversal
- _isImportantForAccessibility
- performAction
- tap
- longPress
- scrollLeft
- scrollRight
- scrollUp
- scrollDown
- increase
- decrease
- showOnScreen
- moveCursorForwardByCharacter
- moveCursorForwardByWord
- moveCursorBackwardByCharacter
- moveCursorBackwardByWord
- setText
- setSelection
- copy
- cut
- paste
- didGainAccessibilityFocus
- didLoseAccessibilityFocus
- customAction
- dismiss
- WidgetController
- WidgetController
- platformDispatcher
- view
- semantics
- viewOf
- _viewOf
- any
- allWidgets
- widget
- firstWidget
- widgetList
- layerListOf
- allElements
- element
- firstElement
- elementList
- allStates
- state
- firstState
- stateList
- _stateOf
- allRenderObjects
- renderObject
- firstRenderObject
- renderObjectList
- layers
- _walkLayers
- tap
- tapOnText
- tapAt
- press
- longPress
- longPressAt
- fling
- flingFrom
- trackpadFling
- trackpadFlingFrom
- handlePointerEventRecord
- pump
- pumpAndSettle
- drag
- dragFrom
- timedDrag
- timedDragFrom
- nextPointer
- _getNextPointer
- _createGesture
- createGesture
- startGesture
- hitTestOnBinding
- sendEventToBinding
- printToConsole
- getCenter
- getTopLeft
- getTopRight
- getBottomLeft
- getBottomRight
- _findHitTestableOffsetIn
- _getElementPoint
- getSize
- sendKeyEvent
- sendKeyDownEvent
- sendKeyUpEvent
- sendKeyRepeatEvent
- getRect
- getSemantics
- ensureSemantics
- ensureVisible
- scrollUntilVisible
- dragUntilVisible
- LiveWidgetController
- LiveWidgetController
- pump
- pumpAndSettle
Learn more about Flutter for embedded and desktop on industrialflutter.com