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';
6library;
7
8import 'dart:async';
9import 'dart:ui' as ui;
10
11import 'package:flutter/cupertino.dart';
12import 'package:flutter/foundation.dart';
13import 'package:flutter/material.dart';
14import 'package:flutter/rendering.dart';
15import 'package:flutter/scheduler.dart';
16import 'package:flutter_test/flutter_test.dart';
17
18import '../../driver_extension.dart';
19import '../extension/wait_conditions.dart';
20import 'diagnostics_tree.dart';
21import 'error.dart';
22import 'find.dart';
23import 'frame_sync.dart';
24import 'geometry.dart';
25import 'gesture.dart';
26import 'health.dart';
27import 'layer_tree.dart';
28import 'message.dart';
29import 'render_tree.dart';
30import 'request_data.dart';
31import 'screenshot.dart';
32import 'semantics.dart';
33import 'text.dart';
34import 'text_input_action.dart' show SendTextInputAction;
35import 'wait.dart';
36
37/// A factory which creates [Finder]s from [SerializableFinder]s.
38mixin 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.
127mixin 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

Provided by KDAB

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