// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../driver_extension.dart'; import '../extension/wait_conditions.dart'; import 'diagnostics_tree.dart'; import 'error.dart'; import 'find.dart'; import 'frame_sync.dart'; import 'geometry.dart'; import 'gesture.dart'; import 'health.dart'; import 'layer_tree.dart'; import 'message.dart'; import 'render_tree.dart'; import 'request_data.dart'; import 'semantics.dart'; import 'text.dart'; import 'text_input_action.dart' show SendTextInputAction; import 'wait.dart'; /// A factory which creates [Finder]s from [SerializableFinder]s. mixin CreateFinderFactory { /// Creates the flutter widget finder from [SerializableFinder]. Finder createFinder(SerializableFinder finder) { final String finderType = finder.finderType; switch (finderType) { case 'ByText': return _createByTextFinder(finder as ByText); case 'ByTooltipMessage': return _createByTooltipMessageFinder(finder as ByTooltipMessage); case 'BySemanticsLabel': return _createBySemanticsLabelFinder(finder as BySemanticsLabel); case 'ByValueKey': return _createByValueKeyFinder(finder as ByValueKey); case 'ByType': return _createByTypeFinder(finder as ByType); case 'PageBack': return _createPageBackFinder(); case 'Ancestor': return _createAncestorFinder(finder as Ancestor); case 'Descendant': return _createDescendantFinder(finder as Descendant); default: throw DriverError('Unsupported search specification type $finderType'); } } Finder _createByTextFinder(ByText arguments) { return find.text(arguments.text); } Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) { return find.byElementPredicate((Element element) { final Widget widget = element.widget; if (widget is Tooltip) { return widget.message == arguments.text; } return false; }, description: 'widget with text tooltip "${arguments.text}"'); } Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) { return find.byElementPredicate((Element element) { if (element is! RenderObjectElement) { return false; } final String? semanticsLabel = element.renderObject.debugSemantics?.label; if (semanticsLabel == null) { return false; } final Pattern label = arguments.label; return label is RegExp ? label.hasMatch(semanticsLabel) : label == semanticsLabel; }, description: 'widget with semantic label "${arguments.label}"'); } Finder _createByValueKeyFinder(ByValueKey arguments) { switch (arguments.keyValueType) { case 'int': return find.byKey(ValueKey<int>(arguments.keyValue as int)); case 'String': return find.byKey(ValueKey<String>(arguments.keyValue as String)); default: throw UnimplementedError('Unsupported ByValueKey type: ${arguments.keyValueType}'); } } Finder _createByTypeFinder(ByType arguments) { return find.byElementPredicate((Element element) { return element.widget.runtimeType.toString() == arguments.type; }, description: 'widget with runtimeType "${arguments.type}"'); } Finder _createPageBackFinder() { return find.byElementPredicate((Element element) { final Widget widget = element.widget; if (widget is Tooltip) { return widget.message == 'Back'; } if (widget is CupertinoNavigationBarBackButton) { return true; } return false; }, description: 'Material or Cupertino back button'); } Finder _createAncestorFinder(Ancestor arguments) { final Finder finder = find.ancestor( of: createFinder(arguments.of), matching: createFinder(arguments.matching), matchRoot: arguments.matchRoot, ); return arguments.firstMatchOnly ? finder.first : finder; } Finder _createDescendantFinder(Descendant arguments) { final Finder finder = find.descendant( of: createFinder(arguments.of), matching: createFinder(arguments.matching), matchRoot: arguments.matchRoot, ); return arguments.firstMatchOnly ? finder.first : finder; } } /// A factory for [Command] handlers. mixin CommandHandlerFactory { /// With [_frameSync] enabled, Flutter Driver will wait to perform an action /// until there are no pending frames in the app under test. bool _frameSync = true; /// Gets [DataHandler] for result delivery. @protected DataHandler? getDataHandler() => null; /// Registers text input emulation. @protected void registerTextInput() { _testTextInput.register(); } final TestTextInput _testTextInput = TestTextInput(); /// Deserializes the finder from JSON generated by [Command.serialize] or [CommandWithTarget.serialize]. Future<Result> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) { switch(command.kind) { case 'get_health': return _getHealth(command); case 'get_layer_tree': return _getLayerTree(command); case 'get_render_tree': return _getRenderTree(command); case 'enter_text': return _enterText(command); case 'send_text_input_action': return _sendTextInputAction(command); case 'get_text': return _getText(command, finderFactory); case 'request_data': return _requestData(command); case 'scroll': return _scroll(command, prober, finderFactory); case 'scrollIntoView': return _scrollIntoView(command, finderFactory); case 'set_frame_sync': return _setFrameSync(command); case 'set_semantics': return _setSemantics(command); case 'set_text_entry_emulation': return _setTextEntryEmulation(command); case 'tap': return _tap(command, prober, finderFactory); case 'waitFor': return _waitFor(command, finderFactory); case 'waitForAbsent': return _waitForAbsent(command, finderFactory); case 'waitForTappable': return _waitForTappable(command, finderFactory); case 'waitForCondition': return _waitForCondition(command); case 'waitUntilNoTransientCallbacks': return _waitUntilNoTransientCallbacks(command); case 'waitUntilNoPendingFrame': return _waitUntilNoPendingFrame(command); case 'waitUntilFirstFrameRasterized': return _waitUntilFirstFrameRasterized(command); case 'get_semantics_id': return _getSemanticsId(command, finderFactory); case 'get_offset': return _getOffset(command, finderFactory); case 'get_diagnostics_tree': return _getDiagnosticsTree(command, finderFactory); } throw DriverError('Unsupported command kind ${command.kind}'); } Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok); Future<LayerTree> _getLayerTree(Command command) async { return LayerTree(RendererBinding.instance.renderView.debugLayer?.toStringDeep()); } Future<RenderTree> _getRenderTree(Command command) async { return RenderTree(RendererBinding.instance.renderView.toStringDeep()); } Future<Result> _enterText(Command command) async { if (!_testTextInput.isRegistered) { throw StateError( 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is ' 'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.', ); } final EnterText enterTextCommand = command as EnterText; _testTextInput.enterText(enterTextCommand.text); return Result.empty; } Future<Result> _sendTextInputAction(Command command) async { if (!_testTextInput.isRegistered) { throw StateError('Unable to fulfill `FlutterDriver.sendTextInputAction`. Text emulation is ' 'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.'); } final SendTextInputAction sendTextInputAction = command as SendTextInputAction; _testTextInput.receiveAction(TextInputAction.values[sendTextInputAction.textInputAction.index]); return Result.empty; } Future<RequestDataResult> _requestData(Command command) async { final RequestData requestDataCommand = command as RequestData; final DataHandler? dataHandler = getDataHandler(); return RequestDataResult(dataHandler == null ? 'No requestData Extension registered' : await dataHandler(requestDataCommand.message)); } Future<Result> _setFrameSync(Command command) async { final SetFrameSync setFrameSyncCommand = command as SetFrameSync; _frameSync = setFrameSyncCommand.enabled; return Result.empty; } Future<Result> _tap(Command command, WidgetController prober, CreateFinderFactory finderFactory) async { final Tap tapCommand = command as Tap; final Finder computedFinder = await waitForElement( finderFactory.createFinder(tapCommand.finder).hitTestable(), ); await prober.tap(computedFinder); return Result.empty; } Future<Result> _waitFor(Command command, CreateFinderFactory finderFactory) async { final WaitFor waitForCommand = command as WaitFor; await waitForElement(finderFactory.createFinder(waitForCommand.finder)); return Result.empty; } Future<Result> _waitForAbsent(Command command, CreateFinderFactory finderFactory) async { final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent; await waitForAbsentElement(finderFactory.createFinder(waitForAbsentCommand.finder)); return Result.empty; } Future<Result> _waitForTappable(Command command, CreateFinderFactory finderFactory) async { final WaitForTappable waitForTappableCommand = command as WaitForTappable; await waitForElement( finderFactory.createFinder(waitForTappableCommand.finder).hitTestable(), ); return Result.empty; } Future<Result> _waitForCondition(Command command) async { assert(command != null); final WaitForCondition waitForConditionCommand = command as WaitForCondition; final WaitCondition condition = deserializeCondition(waitForConditionCommand.condition); await condition.wait(); return Result.empty; } @Deprecated( 'This method has been deprecated in favor of _waitForCondition. ' 'This feature was deprecated after v1.9.3.' ) Future<Result> _waitUntilNoTransientCallbacks(Command command) async { if (SchedulerBinding.instance.transientCallbackCount != 0) { await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); } return Result.empty; } /// Returns a future that waits until no pending frame is scheduled (frame is synced). /// /// Specifically, it checks: /// * Whether the count of transient callbacks is zero. /// * Whether there's no pending request for scheduling a new frame. /// /// We consider the frame is synced when both conditions are met. /// /// This method relies on a Flutter Driver mechanism called "frame sync", /// which waits for transient animations to finish. Persistent animations will /// cause this to wait forever. /// /// If a test needs to interact with the app while animations are running, it /// should avoid this method and instead disable the frame sync using /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more /// details on how to do this. Note, disabling frame sync will require the /// test author to use some other method to avoid flakiness. /// /// This method has been deprecated in favor of [_waitForCondition]. @Deprecated( 'This method has been deprecated in favor of _waitForCondition. ' 'This feature was deprecated after v1.9.3.' ) Future<Result> _waitUntilNoPendingFrame(Command command) async { await _waitUntilFrame(() { return SchedulerBinding.instance.transientCallbackCount == 0 && !SchedulerBinding.instance.hasScheduledFrame; }); return Result.empty; } Future<GetSemanticsIdResult> _getSemanticsId(Command command, CreateFinderFactory finderFactory) async { final GetSemanticsId semanticsCommand = command as GetSemanticsId; final Finder target = await waitForElement(finderFactory.createFinder(semanticsCommand.finder)); final Iterable<Element> elements = target.evaluate(); if (elements.length > 1) { throw StateError('Found more than one element with the same ID: $elements'); } final Element element = elements.single; RenderObject? renderObject = element.renderObject; SemanticsNode? node; while (renderObject != null && node == null) { node = renderObject.debugSemantics; renderObject = renderObject.parent as RenderObject?; } if (node == null) { throw StateError('No semantics data found'); } return GetSemanticsIdResult(node.id); } Future<GetOffsetResult> _getOffset(Command command, CreateFinderFactory finderFactory) async { final GetOffset getOffsetCommand = command as GetOffset; final Finder finder = await waitForElement(finderFactory.createFinder(getOffsetCommand.finder)); final Element element = finder.evaluate().single; final RenderBox box = (element.renderObject as RenderBox?)!; Offset localPoint; switch (getOffsetCommand.offsetType) { case OffsetType.topLeft: localPoint = Offset.zero; break; case OffsetType.topRight: localPoint = box.size.topRight(Offset.zero); break; case OffsetType.bottomLeft: localPoint = box.size.bottomLeft(Offset.zero); break; case OffsetType.bottomRight: localPoint = box.size.bottomRight(Offset.zero); break; case OffsetType.center: localPoint = box.size.center(Offset.zero); break; } final Offset globalPoint = box.localToGlobal(localPoint); return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy); } Future<DiagnosticsTreeResult> _getDiagnosticsTree(Command command, CreateFinderFactory finderFactory) async { final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree; final Finder finder = await waitForElement(finderFactory.createFinder(diagnosticsCommand.finder)); final Element element = finder.evaluate().single; DiagnosticsNode diagnosticsNode; switch (diagnosticsCommand.diagnosticsType) { case DiagnosticsType.renderObject: diagnosticsNode = element.renderObject!.toDiagnosticsNode(); break; case DiagnosticsType.widget: diagnosticsNode = element.toDiagnosticsNode(); break; } return DiagnosticsTreeResult(diagnosticsNode.toJsonMap(DiagnosticsSerializationDelegate( subtreeDepth: diagnosticsCommand.subtreeDepth, includeProperties: diagnosticsCommand.includeProperties, ))); } Future<Result> _scroll(Command command, WidgetController prober, CreateFinderFactory finderFactory) async { final Scroll scrollCommand = command as Scroll; final Finder target = await waitForElement(finderFactory.createFinder(scrollCommand.finder)); final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond; final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble(); final Duration pause = scrollCommand.duration ~/ totalMoves; final Offset startLocation = prober.getCenter(target); Offset currentLocation = startLocation; final TestPointer pointer = TestPointer(); prober.binding.handlePointerEvent(pointer.down(startLocation)); await Future<void>.value(); // so that down and move don't happen in the same microtask for (int moves = 0; moves < totalMoves; moves += 1) { currentLocation = currentLocation + delta; prober.binding.handlePointerEvent(pointer.move(currentLocation)); await Future<void>.delayed(pause); } prober.binding.handlePointerEvent(pointer.up()); return Result.empty; } Future<Result> _scrollIntoView(Command command, CreateFinderFactory finderFactory) async { final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView; final Finder target = await waitForElement(finderFactory.createFinder(scrollIntoViewCommand.finder)); await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment); return Result.empty; } Future<GetTextResult> _getText(Command command, CreateFinderFactory finderFactory) async { final GetText getTextCommand = command as GetText; final Finder target = await waitForElement(finderFactory.createFinder(getTextCommand.finder)); final Widget widget = target.evaluate().single.widget; String? text; if (widget.runtimeType == Text) { text = (widget as Text).data; } else if (widget.runtimeType == RichText) { final RichText richText = widget as RichText; text = richText.text.toPlainText( includeSemanticsLabels: false, includePlaceholders: false, ); } else if (widget.runtimeType == TextField) { text = (widget as TextField).controller?.text; } else if (widget.runtimeType == TextFormField) { text = (widget as TextFormField).controller?.text; } else if (widget.runtimeType == EditableText) { text = (widget as EditableText).controller.text; } if (text == null) { throw UnsupportedError('Type ${widget.runtimeType} is currently not supported by getText'); } return GetTextResult(text); } Future<Result> _setTextEntryEmulation(Command command) async { final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation; if (setTextEntryEmulationCommand.enabled) { _testTextInput.register(); } else { _testTextInput.unregister(); } return Result.empty; } SemanticsHandle? _semantics; bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null; Future<SetSemanticsResult> _setSemantics(Command command) async { final SetSemantics setSemanticsCommand = command as SetSemantics; final bool semanticsWasEnabled = _semanticsIsEnabled; if (setSemanticsCommand.enabled && _semantics == null) { _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); if (!semanticsWasEnabled) { // wait for the first frame where semantics is enabled. final Completer<void> completer = Completer<void>(); SchedulerBinding.instance.addPostFrameCallback((Duration d) { completer.complete(); }); await completer.future; } } else if (!setSemanticsCommand.enabled && _semantics != null) { _semantics!.dispose(); _semantics = null; } return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled); } // This can be used to wait for the first frame being rasterized during app launch. @Deprecated( 'This method has been deprecated in favor of _waitForCondition. ' 'This feature was deprecated after v1.9.3.' ) Future<Result> _waitUntilFirstFrameRasterized(Command command) async { await WidgetsBinding.instance.waitUntilFirstFrameRasterized; return Result.empty; } /// Runs `finder` repeatedly until it finds one or more [Element]s. Future<Finder> waitForElement(Finder finder) async { if (_frameSync) { await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); } await _waitUntilFrame(() => finder.evaluate().isNotEmpty); if (_frameSync) { await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); } return finder; } /// Runs `finder` repeatedly until it finds zero [Element]s. Future<Finder> waitForAbsentElement(Finder finder) async { if (_frameSync) { await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); } await _waitUntilFrame(() => finder.evaluate().isEmpty); if (_frameSync) { await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); } return finder; } // Waits until at the end of a frame the provided [condition] is [true]. Future<void> _waitUntilFrame(bool Function() condition, [ Completer<void>? completer ]) { completer ??= Completer<void>(); if (!condition()) { SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { _waitUntilFrame(condition, completer); }); } else { completer.complete(); } return completer.future; } }