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