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 'package:flutter_driver/flutter_driver.dart'; |
6 | library; |
7 | |
8 | import 'dart:async'; |
9 | import 'dart:ui' as ui; |
10 | |
11 | import 'package:flutter/cupertino.dart'; |
12 | import 'package:flutter/foundation.dart'; |
13 | import 'package:flutter/material.dart'; |
14 | import 'package:flutter/rendering.dart'; |
15 | import 'package:flutter/scheduler.dart'; |
16 | import 'package:flutter_test/flutter_test.dart'; |
17 | |
18 | import '../../driver_extension.dart'; |
19 | import '../extension/wait_conditions.dart'; |
20 | import 'diagnostics_tree.dart'; |
21 | import 'error.dart'; |
22 | import 'find.dart'; |
23 | import 'frame_sync.dart'; |
24 | import 'geometry.dart'; |
25 | import 'gesture.dart'; |
26 | import 'health.dart'; |
27 | import 'layer_tree.dart'; |
28 | import 'message.dart'; |
29 | import 'render_tree.dart'; |
30 | import 'request_data.dart'; |
31 | import 'screenshot.dart'; |
32 | import 'semantics.dart'; |
33 | import 'text.dart'; |
34 | import 'text_input_action.dart' show SendTextInputAction; |
35 | import 'wait.dart'; |
36 | |
37 | /// A factory which creates [Finder]s from [SerializableFinder]s. |
38 | mixin CreateFinderFactory { |
39 | /// Creates the flutter widget finder from [SerializableFinder]. |
40 | Finder createFinder(SerializableFinder finder) { |
41 | return switch (finder.finderType) { |
42 | 'ByText'=> _createByTextFinder(finder as ByText), |
43 | 'ByTooltipMessage'=> _createByTooltipMessageFinder(finder as ByTooltipMessage), |
44 | 'BySemanticsLabel'=> _createBySemanticsLabelFinder(finder as BySemanticsLabel), |
45 | 'ByValueKey'=> _createByValueKeyFinder(finder as ByValueKey), |
46 | 'ByType'=> _createByTypeFinder(finder as ByType), |
47 | 'PageBack'=> _createPageBackFinder(), |
48 | 'Ancestor'=> _createAncestorFinder(finder as Ancestor), |
49 | 'Descendant'=> _createDescendantFinder(finder as Descendant), |
50 | final String type => throw DriverError('Unsupported search specification type$type '), |
51 | }; |
52 | } |
53 | |
54 | Finder _createByTextFinder(ByText arguments) { |
55 | return find.text(arguments.text); |
56 | } |
57 | |
58 | Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) { |
59 | return find.byElementPredicate((Element element) { |
60 | final Widget widget = element.widget; |
61 | if (widget is Tooltip) { |
62 | return widget.message == arguments.text; |
63 | } |
64 | return false; |
65 | }, description: 'widget with text tooltip "${arguments.text} "'); |
66 | } |
67 | |
68 | Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) { |
69 | return find.byElementPredicate((Element element) { |
70 | if (element is! RenderObjectElement) { |
71 | return false; |
72 | } |
73 | final String? semanticsLabel = element.renderObject.debugSemantics?.label; |
74 | if (semanticsLabel == null) { |
75 | return false; |
76 | } |
77 | final Pattern label = arguments.label; |
78 | return label is RegExp ? label.hasMatch(semanticsLabel) : label == semanticsLabel; |
79 | }, description: 'widget with semantic label "${arguments.label} "'); |
80 | } |
81 | |
82 | Finder _createByValueKeyFinder(ByValueKey arguments) { |
83 | return switch (arguments.keyValueType) { |
84 | 'int'=> find.byKey(ValueKey<int>(arguments.keyValue as int)), |
85 | 'String'=> find.byKey(ValueKey<String>(arguments.keyValue as String)), |
86 | _ => throw UnimplementedError('Unsupported ByValueKey type:${arguments.keyValueType} '), |
87 | }; |
88 | } |
89 | |
90 | Finder _createByTypeFinder(ByType arguments) { |
91 | return find.byElementPredicate((Element element) { |
92 | return element.widget.runtimeType.toString() == arguments.type; |
93 | }, description: 'widget with runtimeType "${arguments.type} "'); |
94 | } |
95 | |
96 | Finder _createPageBackFinder() { |
97 | return find.byElementPredicate( |
98 | (Element element) => switch (element.widget) { |
99 | Tooltip(message: 'Back') => true, |
100 | CupertinoNavigationBarBackButton() => true, |
101 | _ => false, |
102 | }, |
103 | description: 'Material or Cupertino back button', |
104 | ); |
105 | } |
106 | |
107 | Finder _createAncestorFinder(Ancestor arguments) { |
108 | final Finder finder = find.ancestor( |
109 | of: createFinder(arguments.of), |
110 | matching: createFinder(arguments.matching), |
111 | matchRoot: arguments.matchRoot, |
112 | ); |
113 | return arguments.firstMatchOnly ? finder.first : finder; |
114 | } |
115 | |
116 | Finder _createDescendantFinder(Descendant arguments) { |
117 | final Finder finder = find.descendant( |
118 | of: createFinder(arguments.of), |
119 | matching: createFinder(arguments.matching), |
120 | matchRoot: arguments.matchRoot, |
121 | ); |
122 | return arguments.firstMatchOnly ? finder.first : finder; |
123 | } |
124 | } |
125 | |
126 | /// A factory for [Command] handlers. |
127 | mixin CommandHandlerFactory { |
128 | /// With [_frameSync] enabled, Flutter Driver will wait to perform an action |
129 | /// until there are no pending frames in the app under test. |
130 | bool _frameSync = true; |
131 | |
132 | /// Gets [DataHandler] for result delivery. |
133 | @protected |
134 | DataHandler? getDataHandler() => null; |
135 | |
136 | /// Registers text input emulation. |
137 | @protected |
138 | void registerTextInput() { |
139 | _testTextInput.register(); |
140 | } |
141 | |
142 | final TestTextInput _testTextInput = TestTextInput(); |
143 | |
144 | /// Deserializes the finder from JSON generated by [Command.serialize] or [CommandWithTarget.serialize]. |
145 | Future<Result> handleCommand( |
146 | Command command, |
147 | WidgetController prober, |
148 | CreateFinderFactory finderFactory, |
149 | ) { |
150 | return switch (command.kind) { |
151 | 'get_health'=> _getHealth(command), |
152 | 'get_layer_tree'=> _getLayerTree(command), |
153 | 'get_render_tree'=> _getRenderTree(command), |
154 | 'enter_text'=> _enterText(command), |
155 | 'send_text_input_action'=> _sendTextInputAction(command), |
156 | 'get_text'=> _getText(command, finderFactory), |
157 | 'request_data'=> _requestData(command), |
158 | 'scroll'=> _scroll(command, prober, finderFactory), |
159 | 'scrollIntoView'=> _scrollIntoView(command, finderFactory), |
160 | 'set_frame_sync'=> _setFrameSync(command), |
161 | 'set_semantics'=> _setSemantics(command), |
162 | 'set_text_entry_emulation'=> _setTextEntryEmulation(command), |
163 | 'tap'=> _tap(command, prober, finderFactory), |
164 | 'waitFor'=> _waitFor(command, finderFactory), |
165 | 'waitForAbsent'=> _waitForAbsent(command, finderFactory), |
166 | 'waitForTappable'=> _waitForTappable(command, finderFactory), |
167 | 'waitForCondition'=> _waitForCondition(command), |
168 | 'waitUntilNoTransientCallbacks'=> _waitUntilNoTransientCallbacks(command), |
169 | 'waitUntilNoPendingFrame'=> _waitUntilNoPendingFrame(command), |
170 | 'waitUntilFirstFrameRasterized'=> _waitUntilFirstFrameRasterized(command), |
171 | 'get_semantics_id'=> _getSemanticsId(command, finderFactory), |
172 | 'get_offset'=> _getOffset(command, finderFactory), |
173 | 'get_diagnostics_tree'=> _getDiagnosticsTree(command, finderFactory), |
174 | 'screenshot'=> _takeScreenshot(command), |
175 | final String kind => throw DriverError('Unsupported command kind$kind '), |
176 | }; |
177 | } |
178 | |
179 | Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok); |
180 | |
181 | Future<LayerTree> _getLayerTree(Command command) async { |
182 | final String trees = <String>[ |
183 | for (final RenderView renderView in RendererBinding.instance.renderViews) |
184 | if (renderView.debugLayer != null) renderView.debugLayer!.toStringDeep(), |
185 | ].join('\n\n'); |
186 | return LayerTree(trees.isNotEmpty ? trees : null); |
187 | } |
188 | |
189 | Future<RenderTree> _getRenderTree(Command command) async { |
190 | final String trees = <String>[ |
191 | for (final RenderView renderView in RendererBinding.instance.renderViews) |
192 | renderView.toStringDeep(), |
193 | ].join('\n\n'); |
194 | return RenderTree(trees.isNotEmpty ? trees : null); |
195 | } |
196 | |
197 | Future<Result> _enterText(Command command) async { |
198 | if (!_testTextInput.isRegistered) { |
199 | throw StateError( |
200 | 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is ' |
201 | 'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.', |
202 | ); |
203 | } |
204 | final EnterText enterTextCommand = command as EnterText; |
205 | _testTextInput.enterText(enterTextCommand.text); |
206 | return Result.empty; |
207 | } |
208 | |
209 | Future<Result> _sendTextInputAction(Command command) async { |
210 | if (!_testTextInput.isRegistered) { |
211 | throw StateError( |
212 | 'Unable to fulfill `FlutterDriver.sendTextInputAction`. Text emulation is ' |
213 | 'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.', |
214 | ); |
215 | } |
216 | final SendTextInputAction sendTextInputAction = command as SendTextInputAction; |
217 | _testTextInput.receiveAction(TextInputAction.values[sendTextInputAction.textInputAction.index]); |
218 | return Result.empty; |
219 | } |
220 | |
221 | Future<RequestDataResult> _requestData(Command command) async { |
222 | final RequestData requestDataCommand = command as RequestData; |
223 | final DataHandler? dataHandler = getDataHandler(); |
224 | return RequestDataResult( |
225 | dataHandler == null |
226 | ? 'No requestData Extension registered' |
227 | : await dataHandler(requestDataCommand.message), |
228 | ); |
229 | } |
230 | |
231 | Future<Result> _setFrameSync(Command command) async { |
232 | final SetFrameSync setFrameSyncCommand = command as SetFrameSync; |
233 | _frameSync = setFrameSyncCommand.enabled; |
234 | return Result.empty; |
235 | } |
236 | |
237 | Future<Result> _tap( |
238 | Command command, |
239 | WidgetController prober, |
240 | CreateFinderFactory finderFactory, |
241 | ) async { |
242 | final Tap tapCommand = command as Tap; |
243 | final Finder computedFinder = await waitForElement( |
244 | finderFactory.createFinder(tapCommand.finder).hitTestable(), |
245 | ); |
246 | await prober.tap(computedFinder); |
247 | return Result.empty; |
248 | } |
249 | |
250 | Future<Result> _waitFor(Command command, CreateFinderFactory finderFactory) async { |
251 | final WaitFor waitForCommand = command as WaitFor; |
252 | await waitForElement(finderFactory.createFinder(waitForCommand.finder)); |
253 | return Result.empty; |
254 | } |
255 | |
256 | Future<Result> _waitForAbsent(Command command, CreateFinderFactory finderFactory) async { |
257 | final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent; |
258 | await waitForAbsentElement(finderFactory.createFinder(waitForAbsentCommand.finder)); |
259 | return Result.empty; |
260 | } |
261 | |
262 | Future<Result> _waitForTappable(Command command, CreateFinderFactory finderFactory) async { |
263 | final WaitForTappable waitForTappableCommand = command as WaitForTappable; |
264 | await waitForElement(finderFactory.createFinder(waitForTappableCommand.finder).hitTestable()); |
265 | return Result.empty; |
266 | } |
267 | |
268 | Future<Result> _waitForCondition(Command command) async { |
269 | final WaitForCondition waitForConditionCommand = command as WaitForCondition; |
270 | final WaitCondition condition = deserializeCondition(waitForConditionCommand.condition); |
271 | await condition.wait(); |
272 | return Result.empty; |
273 | } |
274 | |
275 | @Deprecated( |
276 | 'This method has been deprecated in favor of _waitForCondition. ' |
277 | 'This feature was deprecated after v1.9.3.', |
278 | ) |
279 | Future<Result> _waitUntilNoTransientCallbacks(Command command) async { |
280 | if (SchedulerBinding.instance.transientCallbackCount != 0) { |
281 | await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); |
282 | } |
283 | return Result.empty; |
284 | } |
285 | |
286 | /// Returns a future that waits until no pending frame is scheduled (frame is synced). |
287 | /// |
288 | /// Specifically, it checks: |
289 | /// * Whether the count of transient callbacks is zero. |
290 | /// * Whether there's no pending request for scheduling a new frame. |
291 | /// |
292 | /// We consider the frame is synced when both conditions are met. |
293 | /// |
294 | /// This method relies on a Flutter Driver mechanism called "frame sync", |
295 | /// which waits for transient animations to finish. Persistent animations will |
296 | /// cause this to wait forever. |
297 | /// |
298 | /// If a test needs to interact with the app while animations are running, it |
299 | /// should avoid this method and instead disable the frame sync using |
300 | /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more |
301 | /// details on how to do this. Note, disabling frame sync will require the |
302 | /// test author to use some other method to avoid flakiness. |
303 | /// |
304 | /// This method has been deprecated in favor of [_waitForCondition]. |
305 | @Deprecated( |
306 | 'This method has been deprecated in favor of _waitForCondition. ' |
307 | 'This feature was deprecated after v1.9.3.', |
308 | ) |
309 | Future<Result> _waitUntilNoPendingFrame(Command command) async { |
310 | await _waitUntilFrame(() { |
311 | return SchedulerBinding.instance.transientCallbackCount == 0 && |
312 | !SchedulerBinding.instance.hasScheduledFrame; |
313 | }); |
314 | return Result.empty; |
315 | } |
316 | |
317 | Future<GetSemanticsIdResult> _getSemanticsId( |
318 | Command command, |
319 | CreateFinderFactory finderFactory, |
320 | ) async { |
321 | final GetSemanticsId semanticsCommand = command as GetSemanticsId; |
322 | final Finder target = await waitForElement(finderFactory.createFinder(semanticsCommand.finder)); |
323 | final Iterable<Element> elements = target.evaluate(); |
324 | if (elements.length > 1) { |
325 | throw StateError('Found more than one element with the same ID:$elements '); |
326 | } |
327 | final Element element = elements.single; |
328 | RenderObject? renderObject = element.renderObject; |
329 | SemanticsNode? node; |
330 | while (renderObject != null && node == null) { |
331 | node = renderObject.debugSemantics; |
332 | renderObject = renderObject.parent; |
333 | } |
334 | if (node == null) { |
335 | throw StateError('No semantics data found'); |
336 | } |
337 | return GetSemanticsIdResult(node.id); |
338 | } |
339 | |
340 | Future<GetOffsetResult> _getOffset(Command command, CreateFinderFactory finderFactory) async { |
341 | final GetOffset getOffsetCommand = command as GetOffset; |
342 | final Finder finder = await waitForElement(finderFactory.createFinder(getOffsetCommand.finder)); |
343 | final Element element = finder.evaluate().single; |
344 | final RenderBox box = (element.renderObject as RenderBox?)!; |
345 | final Offset localPoint = switch (getOffsetCommand.offsetType) { |
346 | OffsetType.topLeft => Offset.zero, |
347 | OffsetType.topRight => box.size.topRight(Offset.zero), |
348 | OffsetType.bottomLeft => box.size.bottomLeft(Offset.zero), |
349 | OffsetType.bottomRight => box.size.bottomRight(Offset.zero), |
350 | OffsetType.center => box.size.center(Offset.zero), |
351 | }; |
352 | final Offset globalPoint = box.localToGlobal(localPoint); |
353 | return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy); |
354 | } |
355 | |
356 | Future<DiagnosticsTreeResult> _getDiagnosticsTree( |
357 | Command command, |
358 | CreateFinderFactory finderFactory, |
359 | ) async { |
360 | final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree; |
361 | final Finder finder = await waitForElement( |
362 | finderFactory.createFinder(diagnosticsCommand.finder), |
363 | ); |
364 | final Element element = finder.evaluate().single; |
365 | final DiagnosticsNode diagnosticsNode = switch (diagnosticsCommand.diagnosticsType) { |
366 | DiagnosticsType.renderObject => element.renderObject!.toDiagnosticsNode(), |
367 | DiagnosticsType.widget => element.toDiagnosticsNode(), |
368 | }; |
369 | return DiagnosticsTreeResult( |
370 | diagnosticsNode.toJsonMap( |
371 | DiagnosticsSerializationDelegate( |
372 | subtreeDepth: diagnosticsCommand.subtreeDepth, |
373 | includeProperties: diagnosticsCommand.includeProperties, |
374 | ), |
375 | ), |
376 | ); |
377 | } |
378 | |
379 | Future<ScreenshotResult> _takeScreenshot(Command command) async { |
380 | final ScreenshotCommand screenshotCommand = command as ScreenshotCommand; |
381 | final RenderView renderView = RendererBinding.instance.renderViews.first; |
382 | // ignore: invalid_use_of_protected_member |
383 | final ContainerLayer? layer = renderView.layer; |
384 | final OffsetLayer offsetLayer = layer! as OffsetLayer; |
385 | final ui.Image image = await offsetLayer.toImage(renderView.paintBounds); |
386 | final ui.ImageByteFormat format = ui.ImageByteFormat.values[screenshotCommand.format.index]; |
387 | final ByteData buffer = (await image.toByteData(format: format))!; |
388 | return ScreenshotResult(buffer.buffer.asUint8List()); |
389 | } |
390 | |
391 | Future<Result> _scroll( |
392 | Command command, |
393 | WidgetController prober, |
394 | CreateFinderFactory finderFactory, |
395 | ) async { |
396 | final Scroll scrollCommand = command as Scroll; |
397 | final Finder target = await waitForElement(finderFactory.createFinder(scrollCommand.finder)); |
398 | final int totalMoves = |
399 | scrollCommand.duration.inMicroseconds * |
400 | scrollCommand.frequency ~/ |
401 | Duration.microsecondsPerSecond; |
402 | final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble(); |
403 | final Duration pause = scrollCommand.duration ~/ totalMoves; |
404 | final Offset startLocation = prober.getCenter(target); |
405 | Offset currentLocation = startLocation; |
406 | final TestPointer pointer = TestPointer(); |
407 | prober.binding.handlePointerEvent(pointer.down(startLocation)); |
408 | await Future<void>.value(); // so that down and move don't happen in the same microtask |
409 | for (int moves = 0; moves < totalMoves; moves += 1) { |
410 | currentLocation = currentLocation + delta; |
411 | prober.binding.handlePointerEvent(pointer.move(currentLocation)); |
412 | await Future<void>.delayed(pause); |
413 | } |
414 | prober.binding.handlePointerEvent(pointer.up()); |
415 | |
416 | return Result.empty; |
417 | } |
418 | |
419 | Future<Result> _scrollIntoView(Command command, CreateFinderFactory finderFactory) async { |
420 | final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView; |
421 | final Finder target = await waitForElement( |
422 | finderFactory.createFinder(scrollIntoViewCommand.finder), |
423 | ); |
424 | await Scrollable.ensureVisible( |
425 | target.evaluate().single, |
426 | duration: const Duration(milliseconds: 100), |
427 | alignment: scrollIntoViewCommand.alignment, |
428 | ); |
429 | return Result.empty; |
430 | } |
431 | |
432 | Future<GetTextResult> _getText(Command command, CreateFinderFactory finderFactory) async { |
433 | final GetText getTextCommand = command as GetText; |
434 | final Finder target = await waitForElement(finderFactory.createFinder(getTextCommand.finder)); |
435 | |
436 | final Widget widget = target.evaluate().single.widget; |
437 | String? text; |
438 | |
439 | if (widget.runtimeType == Text) { |
440 | text = (widget as Text).data; |
441 | } else if (widget.runtimeType == RichText) { |
442 | final RichText richText = widget as RichText; |
443 | text = richText.text.toPlainText(includeSemanticsLabels: false, includePlaceholders: false); |
444 | } else if (widget.runtimeType == TextField) { |
445 | text = (widget as TextField).controller?.text; |
446 | } else if (widget.runtimeType == TextFormField) { |
447 | text = (widget as TextFormField).controller?.text; |
448 | } else if (widget.runtimeType == EditableText) { |
449 | text = (widget as EditableText).controller.text; |
450 | } |
451 | |
452 | if (text == null) { |
453 | throw UnsupportedError('Type${widget.runtimeType} is currently not supported by getText'); |
454 | } |
455 | |
456 | return GetTextResult(text); |
457 | } |
458 | |
459 | Future<Result> _setTextEntryEmulation(Command command) async { |
460 | final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation; |
461 | if (setTextEntryEmulationCommand.enabled) { |
462 | _testTextInput.register(); |
463 | } else { |
464 | _testTextInput.unregister(); |
465 | } |
466 | return Result.empty; |
467 | } |
468 | |
469 | SemanticsHandle? _semantics; |
470 | bool get _semanticsIsEnabled => SemanticsBinding.instance.semanticsEnabled; |
471 | |
472 | Future<SetSemanticsResult> _setSemantics(Command command) async { |
473 | final SetSemantics setSemanticsCommand = command as SetSemantics; |
474 | final bool semanticsWasEnabled = _semanticsIsEnabled; |
475 | if (setSemanticsCommand.enabled && _semantics == null) { |
476 | _semantics = SemanticsBinding.instance.ensureSemantics(); |
477 | if (!semanticsWasEnabled) { |
478 | // wait for the first frame where semantics is enabled. |
479 | final Completer<void> completer = Completer<void>(); |
480 | SchedulerBinding.instance.addPostFrameCallback((Duration d) { |
481 | completer.complete(); |
482 | }); |
483 | await completer.future; |
484 | } |
485 | } else if (!setSemanticsCommand.enabled && _semantics != null) { |
486 | _semantics!.dispose(); |
487 | _semantics = null; |
488 | } |
489 | return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled); |
490 | } |
491 | |
492 | // This can be used to wait for the first frame being rasterized during app launch. |
493 | @Deprecated( |
494 | 'This method has been deprecated in favor of _waitForCondition. ' |
495 | 'This feature was deprecated after v1.9.3.', |
496 | ) |
497 | Future<Result> _waitUntilFirstFrameRasterized(Command command) async { |
498 | await WidgetsBinding.instance.waitUntilFirstFrameRasterized; |
499 | return Result.empty; |
500 | } |
501 | |
502 | /// Runs `finder` repeatedly until it finds one or more [Element]s. |
503 | Future<Finder> waitForElement(Finder finder) async { |
504 | if (_frameSync) { |
505 | await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); |
506 | } |
507 | |
508 | await _waitUntilFrame(() => finder.evaluate().isNotEmpty); |
509 | |
510 | if (_frameSync) { |
511 | await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); |
512 | } |
513 | |
514 | return finder; |
515 | } |
516 | |
517 | /// Runs `finder` repeatedly until it finds zero [Element]s. |
518 | Future<Finder> waitForAbsentElement(Finder finder) async { |
519 | if (_frameSync) { |
520 | await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); |
521 | } |
522 | |
523 | await _waitUntilFrame(() => finder.evaluate().isEmpty); |
524 | |
525 | if (_frameSync) { |
526 | await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); |
527 | } |
528 | |
529 | return finder; |
530 | } |
531 | |
532 | // Waits until at the end of a frame the provided [condition] is [true]. |
533 | Future<void> _waitUntilFrame(bool Function() condition, [Completer<void>? completer]) { |
534 | completer ??= Completer<void>(); |
535 | if (!condition()) { |
536 | SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { |
537 | _waitUntilFrame(condition, completer); |
538 | }); |
539 | } else { |
540 | completer.complete(); |
541 | } |
542 | return completer.future; |
543 | } |
544 | } |
545 |
Definitions
- CreateFinderFactory
- createFinder
- _createByTextFinder
- _createByTooltipMessageFinder
- _createBySemanticsLabelFinder
- _createByValueKeyFinder
- _createByTypeFinder
- _createPageBackFinder
- _createAncestorFinder
- _createDescendantFinder
- CommandHandlerFactory
- getDataHandler
- registerTextInput
- handleCommand
- _getHealth
- _getLayerTree
- _getRenderTree
- _enterText
- _sendTextInputAction
- _requestData
- _setFrameSync
- _tap
- _waitFor
- _waitForAbsent
- _waitForTappable
- _waitForCondition
- _waitUntilNoTransientCallbacks
- _waitUntilNoPendingFrame
- _getSemanticsId
- _getOffset
- _getDiagnosticsTree
- _takeScreenshot
- _scroll
- _scrollIntoView
- _getText
- _setTextEntryEmulation
- _semanticsIsEnabled
- _setSemantics
- _waitUntilFirstFrameRasterized
- waitForElement
- waitForAbsentElement
Learn more about Flutter for embedded and desktop on industrialflutter.com