Unverified Commit d2d07219 authored by Marcin Jeleński's avatar Marcin Jeleński Committed by GitHub

Flutter Driver: command extensions and extension feature cleanup (#67916)

parent 4aa1154b
...@@ -24,5 +24,6 @@ ...@@ -24,5 +24,6 @@
/// } /// }
library flutter_driver_extension; library flutter_driver_extension;
export 'src/common/create_finder_factory.dart'; export 'src/common/deserialization_factory.dart';
export 'src/common/handler_factory.dart';
export 'src/extension/extension.dart'; export 'src/extension/extension.dart';
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
/// Protractor (Angular), Espresso (Android) or Earl Gray (iOS). /// Protractor (Angular), Espresso (Android) or Earl Gray (iOS).
library flutter_driver; library flutter_driver;
export 'src/common/deserialization_factory.dart';
export 'src/common/diagnostics_tree.dart'; export 'src/common/diagnostics_tree.dart';
export 'src/common/enum_util.dart'; export 'src/common/enum_util.dart';
export 'src/common/error.dart'; export 'src/common/error.dart';
......
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'error.dart';
import 'find.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 '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;
}
}
// 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 '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 'wait.dart';
/// A factory for deserializing [Finder]s.
mixin DeserializeFinderFactory {
/// Deserializes the finder from JSON generated by [SerializableFinder.serialize].
SerializableFinder deserializeFinder(Map<String, String> json) {
final String? finderType = json['finderType'];
switch (finderType) {
case 'ByType': return ByType.deserialize(json);
case 'ByValueKey': return ByValueKey.deserialize(json);
case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
case 'ByText': return ByText.deserialize(json);
case 'PageBack': return const PageBack();
case 'Descendant': return Descendant.deserialize(json, this);
case 'Ancestor': return Ancestor.deserialize(json, this);
}
throw DriverError('Unsupported search specification type $finderType');
}
}
/// A factory for deserializing [Command]s.
mixin DeserializeCommandFactory {
/// Deserializes the finder from JSON generated by [Command.serialize] or [CommandWithTarget.serialize].
Command deserializeCommand(Map<String, String> params, DeserializeFinderFactory finderFactory) {
final String? kind = params['command'];
switch(kind) {
case 'get_health': return GetHealth.deserialize(params);
case 'get_layer_tree': return GetLayerTree.deserialize(params);
case 'get_render_tree': return GetRenderTree.deserialize(params);
case 'enter_text': return EnterText.deserialize(params);
case 'get_text': return GetText.deserialize(params, finderFactory);
case 'request_data': return RequestData.deserialize(params);
case 'scroll': return Scroll.deserialize(params, finderFactory);
case 'scrollIntoView': return ScrollIntoView.deserialize(params, finderFactory);
case 'set_frame_sync': return SetFrameSync.deserialize(params);
case 'set_semantics': return SetSemantics.deserialize(params);
case 'set_text_entry_emulation': return SetTextEntryEmulation.deserialize(params);
case 'tap': return Tap.deserialize(params, finderFactory);
case 'waitFor': return WaitFor.deserialize(params, finderFactory);
case 'waitForAbsent': return WaitForAbsent.deserialize(params, finderFactory);
case 'waitForCondition': return WaitForCondition.deserialize(params);
case 'waitUntilNoTransientCallbacks': return WaitUntilNoTransientCallbacks.deserialize(params);
case 'waitUntilNoPendingFrame': return WaitUntilNoPendingFrame.deserialize(params);
case 'waitUntilFirstFrameRasterized': return WaitUntilFirstFrameRasterized.deserialize(params);
case 'get_semantics_id': return GetSemanticsId.deserialize(params, finderFactory);
case 'get_offset': return GetOffset.deserialize(params, finderFactory);
case 'get_diagnostics_tree': return GetDiagnosticsTree.deserialize(params, finderFactory);
}
throw DriverError('Unsupported command kind $kind');
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'deserialization_factory.dart';
import 'enum_util.dart'; import 'enum_util.dart';
import 'find.dart'; import 'find.dart';
import 'message.dart'; import 'message.dart';
......
...@@ -6,28 +6,10 @@ import 'dart:convert'; ...@@ -6,28 +6,10 @@ import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'deserialization_factory.dart';
import 'error.dart'; import 'error.dart';
import 'message.dart'; import 'message.dart';
/// A factory for deserializing [Finder]s.
mixin DeserializeFinderFactory {
/// Deserializes the finder from JSON generated by [SerializableFinder.serialize].
SerializableFinder deserializeFinder(Map<String, String> json) {
final String? finderType = json['finderType'];
switch (finderType) {
case 'ByType': return ByType.deserialize(json);
case 'ByValueKey': return ByValueKey.deserialize(json);
case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
case 'ByText': return ByText.deserialize(json);
case 'PageBack': return const PageBack();
case 'Descendant': return Descendant.deserialize(json, this);
case 'Ancestor': return Ancestor.deserialize(json, this);
}
throw DriverError('Unsupported search specification type $finderType');
}
}
const List<Type> _supportedKeyValueTypes = <Type>[String, int]; const List<Type> _supportedKeyValueTypes = <Type>[String, int];
DriverError _createInvalidKeyValueTypeError(String invalidType) { DriverError _createInvalidKeyValueTypeError(String invalidType) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'deserialization_factory.dart';
import 'enum_util.dart'; import 'enum_util.dart';
import 'find.dart'; import 'find.dart';
import 'message.dart'; import 'message.dart';
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'deserialization_factory.dart';
import 'find.dart'; import 'find.dart';
import 'message.dart'; import 'message.dart';
......
// 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/widgets.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/src/extension/wait_conditions.dart';
import 'package:flutter_test/flutter_test.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 '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 '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 '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 '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<EnterTextResult> _enterText(Command command) async {
if (!_testTextInput.isRegistered) {
throw '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 const EnterTextResult();
}
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<SetFrameSyncResult> _setFrameSync(Command command) async {
final SetFrameSync setFrameSyncCommand = command as SetFrameSync;
_frameSync = setFrameSyncCommand.enabled;
return const SetFrameSyncResult();
}
Future<TapResult> _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 const TapResult();
}
Future<WaitForResult> _waitFor(Command command, CreateFinderFactory finderFactory) async {
final WaitFor waitForCommand = command as WaitFor;
await waitForElement(finderFactory.createFinder(waitForCommand.finder));
return const WaitForResult();
}
Future<WaitForAbsentResult> _waitForAbsent(Command command, CreateFinderFactory finderFactory) async {
final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent;
await waitForAbsentElement(finderFactory.createFinder(waitForAbsentCommand.finder));
return const WaitForAbsentResult();
}
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 null;
}
@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 null;
}
/// 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 null;
}
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<ScrollResult> _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(1);
_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 const ScrollResult();
}
Future<ScrollResult> _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 const ScrollResult();
}
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;
if (richText.text.runtimeType == TextSpan) {
text = (richText.text as TextSpan).text;
}
} 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.toString()} is currently not supported by getText');
}
return GetTextResult(text);
}
Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation;
if (setTextEntryEmulationCommand.enabled) {
_testTextInput.register();
} else {
_testTextInput.unregister();
}
return const SetTextEntryEmulationResult();
}
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 null;
}
/// 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 condition(), [ Completer<void>? completer ]) {
completer ??= Completer<void>();
if (!condition()) {
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
_waitUntilFrame(condition, completer);
});
} else {
completer.complete();
}
return completer.future;
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'deserialization_factory.dart';
import 'find.dart'; import 'find.dart';
import 'message.dart'; import 'message.dart';
......
...@@ -9,33 +9,21 @@ import 'package:flutter/cupertino.dart'; ...@@ -9,33 +9,21 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle; import 'package:flutter/rendering.dart' show RendererBinding;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../common/create_finder_factory.dart'; import '../common/deserialization_factory.dart';
import '../common/diagnostics_tree.dart';
import '../common/error.dart'; import '../common/error.dart';
import '../common/find.dart'; import '../common/find.dart';
import '../common/frame_sync.dart'; import '../common/handler_factory.dart';
import '../common/geometry.dart';
import '../common/gesture.dart';
import '../common/health.dart';
import '../common/layer_tree.dart';
import '../common/message.dart'; import '../common/message.dart';
import '../common/render_tree.dart';
import '../common/request_data.dart';
import '../common/semantics.dart';
import '../common/text.dart';
import '../common/wait.dart';
import '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; import '_extension_io.dart' if (dart.library.html) '_extension_web.dart';
import 'wait_conditions.dart';
const String _extensionMethodName = 'driver'; const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
/// Signature for the handler passed to [enableFlutterDriverExtension]. /// Signature for the handler passed to [enableFlutterDriverExtension].
/// ///
...@@ -44,16 +32,17 @@ const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; ...@@ -44,16 +32,17 @@ const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
typedef DataHandler = Future<String> Function(String? message); typedef DataHandler = Future<String> Function(String? message);
class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
_DriverBinding(this._handler, this._silenceErrors, this.finders); _DriverBinding(this._handler, this._silenceErrors, this.finders, this.commands);
final DataHandler? _handler; final DataHandler? _handler;
final bool _silenceErrors; final bool _silenceErrors;
final List<FinderExtension>? finders; final List<FinderExtension>? finders;
final List<CommandExtension>? commands;
@override @override
void initServiceExtensions() { void initServiceExtensions() {
super.initServiceExtensions(); super.initServiceExtensions();
final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors, finders: finders ?? const <FinderExtension>[]); final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors, finders: finders ?? const <FinderExtension>[], commands: commands ?? const <CommandExtension>[]);
registerServiceExtension( registerServiceExtension(
name: _extensionMethodName, name: _extensionMethodName,
callback: extension.call, callback: extension.call,
...@@ -89,24 +78,36 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, ...@@ -89,24 +78,36 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding,
/// will still be returned in the `response` field of the result JSON along /// will still be returned in the `response` field of the result JSON along
/// with an `isError` boolean. /// with an `isError` boolean.
/// ///
/// The `finders` parameter are used to add custom finders, as in the following example. /// The `finders` and `commands` parameters are optional and used to add custom
/// finders or commands, as in the following example.
/// ///
/// ```dart main /// ```dart main
/// void main() { /// void main() {
/// enableFlutterDriverExtension(finders: <FinderExtension>[ SomeFinderExtension() ]); /// enableFlutterDriverExtension(
/// finders: <FinderExtension>[ SomeFinderExtension() ],
/// commands: <CommandExtension>[ SomeCommandExtension() ],
/// );
/// ///
/// app.main(); /// app.main();
/// } /// }
/// ``` /// ```
/// ///
/// ```dart /// ```dart
/// class Some extends SerializableFinder { /// driver.sendCommand(SomeCommand(ByValueKey('Button'), 7));
/// const Some(this.title); /// ```
///
/// Note: SomeFinder and SomeFinderExtension must be placed in different files
/// to avoid `dart:ui` import issue. Imports relative to `dart:ui` can't be
/// accessed from host runner, where flutter runtime is not accessible.
///
/// ```dart
/// class SomeFinder extends SerializableFinder {
/// const SomeFinder(this.title);
/// ///
/// final String title; /// final String title;
/// ///
/// @override /// @override
/// String get finderType => 'Some'; /// String get finderType => 'SomeFinder';
/// ///
/// @override /// @override
/// Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ /// Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
...@@ -118,14 +119,14 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, ...@@ -118,14 +119,14 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding,
/// ```dart /// ```dart
/// class SomeFinderExtension extends FinderExtension { /// class SomeFinderExtension extends FinderExtension {
/// ///
/// String get finderType => 'Some'; /// String get finderType => 'SomeFinder';
/// ///
/// SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory) { /// SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory) {
/// return Some(json['title']); /// return SomeFinder(json['title']);
/// } /// }
/// ///
/// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) { /// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) {
/// Some someFinder = finder as Some; /// Some someFinder = finder as SomeFinder;
/// ///
/// return find.byElementPredicate((Element element) { /// return find.byElementPredicate((Element element) {
/// final Widget widget = element.widget; /// final Widget widget = element.widget;
...@@ -138,9 +139,87 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, ...@@ -138,9 +139,87 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding,
/// } /// }
/// ``` /// ```
/// ///
void enableFlutterDriverExtension({ DataHandler? handler, bool silenceErrors = false, List<FinderExtension>? finders}) { /// Note: SomeCommand, SomeResult and SomeCommandExtension must be placed in
/// different files to avoid `dart:ui` import issue. Imports relative to `dart:ui`
/// can't be accessed from host runner, where flutter runtime is not accessible.
///
/// ```dart
/// class SomeCommand extends CommandWithTarget {
/// SomeCommand(SerializableFinder finder, this.times, {Duration? timeout})
/// : super(finder, timeout: timeout);
///
/// SomeCommand.deserialize(Map<String, String> json, DeserializeFinderFactory finderFactory)
/// : times = int.parse(json['times']!),
/// super.deserialize(json, finderFactory);
///
/// @override
/// Map<String, String> serialize() {
/// return super.serialize()..addAll(<String, String>{'times': '$times'});
/// }
///
/// @override
/// String get kind => 'SomeCommand';
///
/// final int times;
/// }
///```
///
/// ```dart
/// class SomeCommandResult extends Result {
/// const SomeCommandResult(this.resultParam);
///
/// final String resultParam;
///
/// @override
/// Map<String, dynamic> toJson() {
/// return <String, dynamic>{
/// 'resultParam': resultParam,
/// };
/// }
/// }
/// ```
///
/// ```dart
/// class SomeCommandExtension extends CommandExtension {
/// @override
/// String get commandKind => 'SomeCommand';
///
/// @override
/// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async {
/// final SomeCommand someCommand = command as SomeCommand;
///
/// // Deserialize [Finder]:
/// final Finder finder = finderFactory.createFinder(stubCommand.finder);
///
/// // Wait for [Element]:
/// handlerFactory.waitForElement(finder);
///
/// // Alternatively, wait for [Element] absence:
/// handlerFactory.waitForAbsentElement(finder);
///
/// // Submit known [Command]s:
/// for (int index = 0; i < someCommand.times; index++) {
/// await handlerFactory.handleCommand(Tap(someCommand.finder), prober, finderFactory);
/// }
///
/// // Alternatively, use [WidgetController]:
/// for (int index = 0; i < stubCommand.times; index++) {
/// await prober.tap(finder);
/// }
///
/// return const SomeCommandResult('foo bar');
/// }
///
/// @override
/// Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory) {
/// return SomeCommand.deserialize(params, finderFactory);
/// }
/// }
/// ```
///
void enableFlutterDriverExtension({ DataHandler? handler, bool silenceErrors = false, List<FinderExtension>? finders, List<CommandExtension>? commands}) {
assert(WidgetsBinding.instance == null); assert(WidgetsBinding.instance == null);
_DriverBinding(handler, silenceErrors, finders ?? <FinderExtension>[]); _DriverBinding(handler, silenceErrors, finders ?? <FinderExtension>[], commands ?? <CommandExtension>[]);
assert(WidgetsBinding.instance is _DriverBinding); assert(WidgetsBinding.instance is _DriverBinding);
} }
...@@ -150,107 +229,119 @@ typedef CommandHandlerCallback = Future<Result?> Function(Command c); ...@@ -150,107 +229,119 @@ typedef CommandHandlerCallback = Future<Result?> Function(Command c);
/// Signature for functions that deserialize a JSON map to a command object. /// Signature for functions that deserialize a JSON map to a command object.
typedef CommandDeserializerCallback = Command Function(Map<String, String> params); typedef CommandDeserializerCallback = Command Function(Map<String, String> params);
/// Used to expand the new Finder /// Used to expand the new [Finder].
abstract class FinderExtension { abstract class FinderExtension {
/// Identifies the type of finder to be used by the driver extension. /// Identifies the type of finder to be used by the driver extension.
String get finderType; String get finderType;
/// Deserializes the finder from JSON generated by [SerializableFinder.serialize]. /// Deserializes the finder from JSON generated by [SerializableFinder.serialize].
/// [finderFactory] could be used to deserialize nested finders. ///
/// Use [finderFactory] to deserialize nested [Finder]s.
///
/// See also:
/// * [Ancestor], a finder that uses other [Finder]s as parameters.
SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory); SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory);
/// Signature for functions that run the given finder and return the [Element] /// Signature for functions that run the given finder and return the [Element]
/// found, if any, or null otherwise. /// found, if any, or null otherwise.
/// [finderFactory] could be used to create nested finders. ///
/// Call [finderFactory] to create known, nested [Finder]s from [SerializableFinder]s.
Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory); Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory);
} }
/// Used to expand the new [Command].
///
/// See also:
/// * [CommandWithTarget], a base class for [Command]s with [Finder]s.
abstract class CommandExtension {
/// Identifies the type of command to be used by the driver extension.
String get commandKind;
/// Deserializes the command from JSON generated by [Command.serialize].
///
/// Use [finderFactory] to deserialize nested [Finder]s.
/// Usually used for [CommandWithTarget]s.
///
/// Call [commandFactory] to deserialize commands specified as parameters.
///
/// See also:
/// * [CommandWithTarget], a base class for commands with target finders.
/// * [Tap], a command that uses [Finder]s as parameter.
Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory);
/// Calls action for given [command].
/// Returns action [Result].
/// Invoke [prober] functions to perform widget actions.
/// Use [finderFactory] to create [Finder]s from [SerializableFinder].
/// Call [handlerFactory] to invoke other [Command]s or [CommandWithTarget]s.
///
/// The following example shows invoking nested command with [handlerFactory].
///
/// ```dart
/// @override
/// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async {
/// final StubNestedCommand stubCommand = command as StubNestedCommand;
/// for (int index = 0; i < stubCommand.times; index++) {
/// await handlerFactory.handleCommand(Tap(stubCommand.finder), prober, finderFactory);
/// }
/// return const StubCommandResult('stub response');
/// }
/// ```
///
/// Check the example below for direct [WidgetController] usage with [prober]:
///
/// ```dart
/// @override
/// Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async {
/// final StubProberCommand stubCommand = command as StubProberCommand;
/// for (int index = 0; i < stubCommand.times; index++) {
/// await prober.tap(finderFactory.createFinder(stubCommand.finder));
/// }
/// return const StubCommandResult('stub response');
/// }
/// ```
Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory);
}
/// The class that manages communication between a Flutter Driver test and the /// The class that manages communication between a Flutter Driver test and the
/// application being remote-controlled, on the application side. /// application being remote-controlled, on the application side.
/// ///
/// This is not normally used directly. It is instantiated automatically when /// This is not normally used directly. It is instantiated automatically when
/// calling [enableFlutterDriverExtension]. /// calling [enableFlutterDriverExtension].
@visibleForTesting @visibleForTesting
class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory { class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory, DeserializeCommandFactory, CommandHandlerFactory {
/// Creates an object to manage a Flutter Driver connection. /// Creates an object to manage a Flutter Driver connection.
FlutterDriverExtension( FlutterDriverExtension(
this._requestDataHandler, this._requestDataHandler,
this._silenceErrors, { this._silenceErrors, {
List<FinderExtension> finders = const <FinderExtension>[], List<FinderExtension> finders = const <FinderExtension>[],
List<CommandExtension> commands = const <CommandExtension>[],
}) : assert(finders != null) { }) : assert(finders != null) {
_testTextInput.register(); registerTextInput();
_commandHandlers.addAll(<String, CommandHandlerCallback>{
'get_health': _getHealth,
'get_layer_tree': _getLayerTree,
'get_render_tree': _getRenderTree,
'enter_text': _enterText,
'get_text': _getText,
'request_data': _requestData,
'scroll': _scroll,
'scrollIntoView': _scrollIntoView,
'set_frame_sync': _setFrameSync,
'set_semantics': _setSemantics,
'set_text_entry_emulation': _setTextEntryEmulation,
'tap': _tap,
'waitFor': _waitFor,
'waitForAbsent': _waitForAbsent,
'waitForCondition': _waitForCondition,
'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
'waitUntilNoPendingFrame': _waitUntilNoPendingFrame,
'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized,
'get_semantics_id': _getSemanticsId,
'get_offset': _getOffset,
'get_diagnostics_tree': _getDiagnosticsTree,
});
_commandDeserializers.addAll(<String, CommandDeserializerCallback>{
'get_health': (Map<String, String> params) => GetHealth.deserialize(params),
'get_layer_tree': (Map<String, String> params) => GetLayerTree.deserialize(params),
'get_render_tree': (Map<String, String> params) => GetRenderTree.deserialize(params),
'enter_text': (Map<String, String> params) => EnterText.deserialize(params),
'get_text': (Map<String, String> params) => GetText.deserialize(params, this),
'request_data': (Map<String, String> params) => RequestData.deserialize(params),
'scroll': (Map<String, String> params) => Scroll.deserialize(params, this),
'scrollIntoView': (Map<String, String> params) => ScrollIntoView.deserialize(params, this),
'set_frame_sync': (Map<String, String> params) => SetFrameSync.deserialize(params),
'set_semantics': (Map<String, String> params) => SetSemantics.deserialize(params),
'set_text_entry_emulation': (Map<String, String> params) => SetTextEntryEmulation.deserialize(params),
'tap': (Map<String, String> params) => Tap.deserialize(params, this),
'waitFor': (Map<String, String> params) => WaitFor.deserialize(params, this),
'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params, this),
'waitForCondition': (Map<String, String> params) => WaitForCondition.deserialize(params),
'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params),
'waitUntilNoPendingFrame': (Map<String, String> params) => WaitUntilNoPendingFrame.deserialize(params),
'waitUntilFirstFrameRasterized': (Map<String, String> params) => WaitUntilFirstFrameRasterized.deserialize(params),
'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params, this),
'get_offset': (Map<String, String> params) => GetOffset.deserialize(params, this),
'get_diagnostics_tree': (Map<String, String> params) => GetDiagnosticsTree.deserialize(params, this),
});
for(final FinderExtension finder in finders) { for(final FinderExtension finder in finders) {
_finderExtensions[finder.finderType] = finder; _finderExtensions[finder.finderType] = finder;
} }
for(final CommandExtension command in commands) {
_commandExtensions[command.commandKind] = command;
}
} }
final TestTextInput _testTextInput = TestTextInput(); final WidgetController _prober = LiveWidgetController(WidgetsBinding.instance!);
final DataHandler? _requestDataHandler; final DataHandler? _requestDataHandler;
final bool _silenceErrors; final bool _silenceErrors;
void _log(String message) { void _log(String message) {
driverLog('FlutterDriverExtension', message); driverLog('FlutterDriverExtension', message);
} }
final WidgetController _prober = LiveWidgetController(WidgetsBinding.instance!);
final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
final Map<String, FinderExtension> _finderExtensions = <String, FinderExtension>{}; final Map<String, FinderExtension> _finderExtensions = <String, FinderExtension>{};
final Map<String, CommandExtension> _commandExtensions = <String, CommandExtension>{};
/// 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;
/// Processes a driver command configured by [params] and returns a result /// Processes a driver command configured by [params] and returns a result
/// as an arbitrary JSON object. /// as an arbitrary JSON object.
...@@ -266,15 +357,10 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory ...@@ -266,15 +357,10 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory
Future<Map<String, dynamic>> call(Map<String, String> params) async { Future<Map<String, dynamic>> call(Map<String, String> params) async {
final String commandKind = params['command']!; final String commandKind = params['command']!;
try { try {
final CommandHandlerCallback commandHandler = _commandHandlers[commandKind]!; final Command command = deserializeCommand(params, this);
final CommandDeserializerCallback commandDeserializer =
_commandDeserializers[commandKind]!;
if (commandHandler == null || commandDeserializer == null)
throw 'Extension $_extensionMethod does not support command $commandKind';
final Command command = commandDeserializer(params);
assert(WidgetsBinding.instance!.isRootWidgetAttached || !command.requiresRootWidgetAttached, assert(WidgetsBinding.instance!.isRootWidgetAttached || !command.requiresRootWidgetAttached,
'No root widget is attached; have you remembered to call runApp()?'); 'No root widget is attached; have you remembered to call runApp()?');
Future<Result?> responseFuture = commandHandler(command); Future<Result?> responseFuture = handleCommand(command, _prober, this);
if (command.timeout != null) if (command.timeout != null)
responseFuture = responseFuture.timeout(command.timeout ?? Duration.zero); responseFuture = responseFuture.timeout(command.timeout ?? Duration.zero);
final Result? response = await responseFuture; final Result? response = await responseFuture;
...@@ -298,65 +384,6 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory ...@@ -298,65 +384,6 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory
}; };
} }
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());
}
// 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 null;
}
// Waits until at the end of a frame the provided [condition] is [true].
Future<void> _waitUntilFrame(bool condition(), [ Completer<void>? completer ]) {
completer ??= Completer<void>();
if (!condition()) {
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
_waitUntilFrame(condition, completer);
});
} else {
completer.complete();
}
return completer.future;
}
/// 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;
}
@override @override
SerializableFinder deserializeFinder(Map<String, String> json) { SerializableFinder deserializeFinder(Map<String, String> json) {
final String? finderType = json['finderType']; final String? finderType = json['finderType'];
...@@ -369,258 +396,37 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory ...@@ -369,258 +396,37 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory
@override @override
Finder createFinder(SerializableFinder finder) { Finder createFinder(SerializableFinder finder) {
if (_finderExtensions.containsKey(finder.finderType)) { final String finderType = finder.finderType;
return _finderExtensions[finder.finderType]!.createFinder(finder, this); if (_finderExtensions.containsKey(finderType)) {
return _finderExtensions[finderType]!.createFinder(finder, this);
} }
return super.createFinder(finder); return super.createFinder(finder);
} }
Future<TapResult> _tap(Command command) async { @override
final Tap tapCommand = command as Tap; Command deserializeCommand(Map<String, String> params, DeserializeFinderFactory finderFactory) {
final Finder computedFinder = await _waitForElement( final String? kind = params['command'];
createFinder(tapCommand.finder).hitTestable() if(_commandExtensions.containsKey(kind)) {
); return _commandExtensions[kind]!.deserialize(params, finderFactory, this);
await _prober.tap(computedFinder);
return const TapResult();
}
Future<WaitForResult> _waitFor(Command command) async {
final WaitFor waitForCommand = command as WaitFor;
await _waitForElement(createFinder(waitForCommand.finder));
return const WaitForResult();
}
Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent;
await _waitForAbsentElement(createFinder(waitForAbsentCommand.finder));
return const WaitForAbsentResult();
}
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 null;
}
@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 null;
}
/// 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 null;
}
Future<GetSemanticsIdResult> _getSemanticsId(Command command) async {
final GetSemanticsId semanticsCommand = command as GetSemanticsId;
final Finder target = await _waitForElement(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) async {
final GetOffset getOffsetCommand = command as GetOffset;
final Finder finder = await _waitForElement(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) async {
final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree;
final Finder finder = await _waitForElement(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<ScrollResult> _scroll(Command command) async {
final Scroll scrollCommand = command as Scroll;
final Finder target = await _waitForElement(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(1);
_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 const ScrollResult();
}
Future<ScrollResult> _scrollIntoView(Command command) async {
final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView;
final Finder target = await _waitForElement(createFinder(scrollIntoViewCommand.finder));
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment);
return const ScrollResult();
}
Future<GetTextResult> _getText(Command command) async {
final GetText getTextCommand = command as GetText;
final Finder target = await _waitForElement(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;
if (richText.text.runtimeType == TextSpan) {
text = (richText.text as TextSpan).text;
}
} 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.toString()} is currently not supported by getText');
} }
return GetTextResult(text); return super.deserializeCommand(params, finderFactory);
} }
Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async { @override
final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation; @protected
if (setTextEntryEmulationCommand.enabled) { DataHandler? getDataHandler() {
_testTextInput.register(); return _requestDataHandler;
} else {
_testTextInput.unregister();
}
return const SetTextEntryEmulationResult();
} }
Future<EnterTextResult> _enterText(Command command) async { @override
if (!_testTextInput.isRegistered) { Future<Result?> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) {
throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is ' final String kind = command.kind;
'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.'; if(_commandExtensions.containsKey(kind)) {
return _commandExtensions[kind]!.call(command, prober, finderFactory, this);
} }
final EnterText enterTextCommand = command as EnterText;
_testTextInput.enterText(enterTextCommand.text);
return const EnterTextResult();
}
Future<RequestDataResult> _requestData(Command command) async {
final RequestData requestDataCommand = command as RequestData;
return RequestDataResult(_requestDataHandler == null
? 'No requestData Extension registered'
: await _requestDataHandler!(requestDataCommand.message));
}
Future<SetFrameSyncResult> _setFrameSync(Command command) async { return super.handleCommand(command, prober, finderFactory);
final SetFrameSync setFrameSyncCommand = command as SetFrameSync;
_frameSync = setFrameSyncCommand.enabled;
return const SetFrameSyncResult();
}
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);
} }
} }
...@@ -20,6 +20,8 @@ import 'package:flutter_driver/src/common/wait.dart'; ...@@ -20,6 +20,8 @@ import 'package:flutter_driver/src/common/wait.dart';
import 'package:flutter_driver/src/extension/extension.dart'; import 'package:flutter_driver/src/extension/extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'stubs/stub_command.dart';
import 'stubs/stub_command_extension.dart';
import 'stubs/stub_finder.dart'; import 'stubs/stub_finder.dart';
import 'stubs/stub_finder_extension.dart'; import 'stubs/stub_finder_extension.dart';
...@@ -984,6 +986,100 @@ void main() { ...@@ -984,6 +986,100 @@ void main() {
}); });
}); });
group('extension commands', () {
int invokes = 0;
final VoidCallback stubCallback = () => invokes++;
final Widget debugTree = Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Column(
children: <Widget>[
FlatButton(
child: const Text('Whatever'),
key: const ValueKey<String>('Button'),
onPressed: stubCallback,
),
],
),
),
);
setUp(() {
invokes = 0;
});
testWidgets('unknown extension command', (WidgetTester tester) async {
final FlutterDriverExtension driverExtension = FlutterDriverExtension(
(String arg) async => '',
true,
commands: <CommandExtension>[],
);
Future<Map<String, dynamic>> invokeCommand(SerializableFinder finder, int times) async {
final Map<String, String> arguments = StubNestedCommand(finder, times).serialize();
return await driverExtension.call(arguments);
}
await tester.pumpWidget(debugTree);
final Map<String, dynamic> result = await invokeCommand(ByValueKey('Button'), 10);
expect(result['isError'], true);
expect(result['response'] is String, true);
expect(result['response'] as String, contains('Unsupported command kind StubNestedCommand'));
});
testWidgets('nested command', (WidgetTester tester) async {
final FlutterDriverExtension driverExtension = FlutterDriverExtension(
(String arg) async => '',
true,
commands: <CommandExtension>[
StubNestedCommandExtension(),
],
);
Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock
final Map<String, String> arguments = StubNestedCommand(finder, times, timeout: const Duration(seconds: 1)).serialize();
final Map<String, dynamic> response = await driverExtension.call(arguments);
final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
return StubCommandResult(commandResponse['resultParam'] as String);
}
await tester.pumpWidget(debugTree);
const int times = 10;
final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
expect(result.resultParam, 'stub response');
expect(invokes, times);
});
testWidgets('prober command', (WidgetTester tester) async {
final FlutterDriverExtension driverExtension = FlutterDriverExtension(
(String arg) async => '',
true,
commands: <CommandExtension>[
StubProberCommandExtension(),
],
);
Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock
final Map<String, String> arguments = StubProberCommand(finder, times, timeout: const Duration(seconds: 1)).serialize();
final Map<String, dynamic> response = await driverExtension.call(arguments);
final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
return StubCommandResult(commandResponse['resultParam'] as String);
}
await tester.pumpWidget(debugTree);
const int times = 10;
final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
expect(result.resultParam, 'stub response');
expect(invokes, times);
});
});
group('waitUntilFrameSync', () { group('waitUntilFrameSync', () {
FlutterDriverExtension driverExtension; FlutterDriverExtension driverExtension;
Map<String, dynamic> result; Map<String, dynamic> result;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
// @dart = 2.8 // @dart = 2.8
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/common/find.dart'; import 'package:flutter_driver/src/common/find.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
......
// 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 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/flutter_driver.dart';
class StubNestedCommand extends CommandWithTarget {
StubNestedCommand(SerializableFinder finder, this.times, {Duration? timeout})
: super(finder, timeout: timeout);
StubNestedCommand.deserialize(
Map<String, String> json, DeserializeFinderFactory finderFactory)
: times = int.parse(json['times']!),
super.deserialize(json, finderFactory);
@override
Map<String, String> serialize() {
return super.serialize()..addAll(<String, String>{'times': '$times'});
}
@override
String get kind => 'StubNestedCommand';
final int times;
}
class StubProberCommand extends CommandWithTarget {
StubProberCommand(SerializableFinder finder, this.times, {Duration? timeout})
: super(finder, timeout: timeout);
StubProberCommand.deserialize(Map<String, String> json, DeserializeFinderFactory finderFactory)
: times = int.parse(json['times']!),
super.deserialize(json, finderFactory);
@override
Map<String, String> serialize() {
return super.serialize()..addAll(<String, String>{'times': '$times'});
}
@override
String get kind => 'StubProberCommand';
final int times;
}
class StubCommandResult extends Result {
const StubCommandResult(this.resultParam);
final String resultParam;
@override
Map<String, dynamic> toJson() {
return <String, dynamic>{
'resultParam': resultParam,
};
}
}
// 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 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/common/message.dart';
import 'package:flutter_test/flutter_test.dart';
import 'stub_command.dart';
class StubNestedCommandExtension extends CommandExtension {
@override
String get commandKind => 'StubNestedCommand';
@override
Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async {
final StubNestedCommand stubCommand = command as StubNestedCommand;
handlerFactory.waitForElement(finderFactory.createFinder(stubCommand.finder));
for (int index = 0; index < stubCommand.times; index++) {
await handlerFactory.handleCommand(Tap(stubCommand.finder), prober, finderFactory);
}
return const StubCommandResult('stub response');
}
@override
Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory) {
return StubNestedCommand.deserialize(params, finderFactory);
}
}
class StubProberCommandExtension extends CommandExtension {
@override
String get commandKind => 'StubProberCommand';
@override
Future<Result> call(Command command, WidgetController prober, CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async {
final StubProberCommand stubCommand = command as StubProberCommand;
final Finder finder = finderFactory.createFinder(stubCommand.finder);
handlerFactory.waitForElement(finder);
for (int index = 0; index < stubCommand.times; index++) {
await prober.tap(finder);
}
return const StubCommandResult('stub response');
}
@override
Command deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory, DeserializeCommandFactory commandFactory) {
return StubProberCommand.deserialize(params, finderFactory);
}
}
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/src/common/create_finder_factory.dart';
import 'package:flutter_test/src/finders.dart'; import 'package:flutter_test/src/finders.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_driver/src/common/handler_factory.dart';
import 'package:flutter_driver/src/common/find.dart'; import 'package:flutter_driver/src/common/find.dart';
import 'stub_finder.dart'; import 'stub_finder.dart';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment