Flutter Driver: command extensions and extension feature cleanup (#67916)
This commit is contained in:
parent
4aa1154b09
commit
d2d072199b
@ -24,5 +24,6 @@
|
||||
/// }
|
||||
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';
|
||||
|
@ -13,6 +13,7 @@
|
||||
/// Protractor (Angular), Espresso (Android) or Earl Gray (iOS).
|
||||
library flutter_driver;
|
||||
|
||||
export 'src/common/deserialization_factory.dart';
|
||||
export 'src/common/diagnostics_tree.dart';
|
||||
export 'src/common/enum_util.dart';
|
||||
export 'src/common/error.dart';
|
||||
|
@ -1,117 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
// 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 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'deserialization_factory.dart';
|
||||
import 'enum_util.dart';
|
||||
import 'find.dart';
|
||||
import 'message.dart';
|
||||
|
@ -6,28 +6,10 @@ import 'dart:convert';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'deserialization_factory.dart';
|
||||
import 'error.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];
|
||||
|
||||
DriverError _createInvalidKeyValueTypeError(String invalidType) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'deserialization_factory.dart';
|
||||
import 'enum_util.dart';
|
||||
import 'find.dart';
|
||||
import 'message.dart';
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'deserialization_factory.dart';
|
||||
import 'find.dart';
|
||||
import 'message.dart';
|
||||
|
||||
|
492
packages/flutter_driver/lib/src/common/handler_factory.dart
Normal file
492
packages/flutter_driver/lib/src/common/handler_factory.dart
Normal file
@ -0,0 +1,492 @@
|
||||
// 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 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'deserialization_factory.dart';
|
||||
import 'find.dart';
|
||||
import 'message.dart';
|
||||
|
||||
|
@ -9,33 +9,21 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.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/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../common/create_finder_factory.dart';
|
||||
import '../common/diagnostics_tree.dart';
|
||||
import '../common/deserialization_factory.dart';
|
||||
import '../common/error.dart';
|
||||
import '../common/find.dart';
|
||||
import '../common/frame_sync.dart';
|
||||
import '../common/geometry.dart';
|
||||
import '../common/gesture.dart';
|
||||
import '../common/health.dart';
|
||||
import '../common/layer_tree.dart';
|
||||
import '../common/handler_factory.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 'wait_conditions.dart';
|
||||
|
||||
const String _extensionMethodName = 'driver';
|
||||
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
|
||||
|
||||
/// Signature for the handler passed to [enableFlutterDriverExtension].
|
||||
///
|
||||
@ -44,16 +32,17 @@ const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
|
||||
typedef DataHandler = Future<String> Function(String? message);
|
||||
|
||||
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 bool _silenceErrors;
|
||||
final List<FinderExtension>? finders;
|
||||
final List<CommandExtension>? commands;
|
||||
|
||||
@override
|
||||
void 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(
|
||||
name: _extensionMethodName,
|
||||
callback: extension.call,
|
||||
@ -89,24 +78,36 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding,
|
||||
/// will still be returned in the `response` field of the result JSON along
|
||||
/// 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
|
||||
/// void main() {
|
||||
/// enableFlutterDriverExtension(finders: <FinderExtension>[ SomeFinderExtension() ]);
|
||||
/// enableFlutterDriverExtension(
|
||||
/// finders: <FinderExtension>[ SomeFinderExtension() ],
|
||||
/// commands: <CommandExtension>[ SomeCommandExtension() ],
|
||||
/// );
|
||||
///
|
||||
/// app.main();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```dart
|
||||
/// class Some extends SerializableFinder {
|
||||
/// const Some(this.title);
|
||||
/// driver.sendCommand(SomeCommand(ByValueKey('Button'), 7));
|
||||
/// ```
|
||||
///
|
||||
/// 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;
|
||||
///
|
||||
/// @override
|
||||
/// String get finderType => 'Some';
|
||||
/// String get finderType => 'SomeFinder';
|
||||
///
|
||||
/// @override
|
||||
/// Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
|
||||
@ -118,14 +119,14 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding,
|
||||
/// ```dart
|
||||
/// class SomeFinderExtension extends FinderExtension {
|
||||
///
|
||||
/// String get finderType => 'Some';
|
||||
/// String get finderType => 'SomeFinder';
|
||||
///
|
||||
/// SerializableFinder deserialize(Map<String, String> params, DeserializeFinderFactory finderFactory) {
|
||||
/// return Some(json['title']);
|
||||
/// return SomeFinder(json['title']);
|
||||
/// }
|
||||
///
|
||||
/// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) {
|
||||
/// Some someFinder = finder as Some;
|
||||
/// Some someFinder = finder as SomeFinder;
|
||||
///
|
||||
/// return find.byElementPredicate((Element element) {
|
||||
/// final Widget widget = element.widget;
|
||||
@ -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);
|
||||
_DriverBinding(handler, silenceErrors, finders ?? <FinderExtension>[]);
|
||||
_DriverBinding(handler, silenceErrors, finders ?? <FinderExtension>[], commands ?? <CommandExtension>[]);
|
||||
assert(WidgetsBinding.instance is _DriverBinding);
|
||||
}
|
||||
|
||||
@ -150,107 +229,119 @@ typedef CommandHandlerCallback = Future<Result?> Function(Command c);
|
||||
/// Signature for functions that deserialize a JSON map to a command object.
|
||||
typedef CommandDeserializerCallback = Command Function(Map<String, String> params);
|
||||
|
||||
/// Used to expand the new Finder
|
||||
/// Used to expand the new [Finder].
|
||||
abstract class FinderExtension {
|
||||
|
||||
/// Identifies the type of finder to be used by the driver extension.
|
||||
String get finderType;
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Signature for functions that run the given finder and return the [Element]
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// application being remote-controlled, on the application side.
|
||||
///
|
||||
/// This is not normally used directly. It is instantiated automatically when
|
||||
/// calling [enableFlutterDriverExtension].
|
||||
@visibleForTesting
|
||||
class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory {
|
||||
class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory, DeserializeCommandFactory, CommandHandlerFactory {
|
||||
/// Creates an object to manage a Flutter Driver connection.
|
||||
FlutterDriverExtension(
|
||||
this._requestDataHandler,
|
||||
this._silenceErrors, {
|
||||
List<FinderExtension> finders = const <FinderExtension>[],
|
||||
List<CommandExtension> commands = const <CommandExtension>[],
|
||||
}) : assert(finders != null) {
|
||||
_testTextInput.register();
|
||||
|
||||
_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),
|
||||
});
|
||||
registerTextInput();
|
||||
|
||||
for(final FinderExtension finder in finders) {
|
||||
_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 bool _silenceErrors;
|
||||
|
||||
void _log(String 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>{};
|
||||
|
||||
/// 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;
|
||||
final Map<String, CommandExtension> _commandExtensions = <String, CommandExtension>{};
|
||||
|
||||
/// Processes a driver command configured by [params] and returns a result
|
||||
/// as an arbitrary JSON object.
|
||||
@ -266,15 +357,10 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory
|
||||
Future<Map<String, dynamic>> call(Map<String, String> params) async {
|
||||
final String commandKind = params['command']!;
|
||||
try {
|
||||
final CommandHandlerCallback commandHandler = _commandHandlers[commandKind]!;
|
||||
final CommandDeserializerCallback commandDeserializer =
|
||||
_commandDeserializers[commandKind]!;
|
||||
if (commandHandler == null || commandDeserializer == null)
|
||||
throw 'Extension $_extensionMethod does not support command $commandKind';
|
||||
final Command command = commandDeserializer(params);
|
||||
final Command command = deserializeCommand(params, this);
|
||||
assert(WidgetsBinding.instance!.isRootWidgetAttached || !command.requiresRootWidgetAttached,
|
||||
'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)
|
||||
responseFuture = responseFuture.timeout(command.timeout ?? Duration.zero);
|
||||
final Result? response = await responseFuture;
|
||||
@ -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
|
||||
SerializableFinder deserializeFinder(Map<String, String> json) {
|
||||
final String? finderType = json['finderType'];
|
||||
@ -369,258 +396,37 @@ class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory
|
||||
|
||||
@override
|
||||
Finder createFinder(SerializableFinder finder) {
|
||||
if (_finderExtensions.containsKey(finder.finderType)) {
|
||||
return _finderExtensions[finder.finderType]!.createFinder(finder, this);
|
||||
final String finderType = finder.finderType;
|
||||
if (_finderExtensions.containsKey(finderType)) {
|
||||
return _finderExtensions[finderType]!.createFinder(finder, this);
|
||||
}
|
||||
|
||||
return super.createFinder(finder);
|
||||
}
|
||||
|
||||
Future<TapResult> _tap(Command command) async {
|
||||
final Tap tapCommand = command as Tap;
|
||||
final Finder computedFinder = await _waitForElement(
|
||||
createFinder(tapCommand.finder).hitTestable()
|
||||
);
|
||||
await _prober.tap(computedFinder);
|
||||
return const TapResult();
|
||||
@override
|
||||
Command deserializeCommand(Map<String, String> params, DeserializeFinderFactory finderFactory) {
|
||||
final String? kind = params['command'];
|
||||
if(_commandExtensions.containsKey(kind)) {
|
||||
return _commandExtensions[kind]!.deserialize(params, finderFactory, this);
|
||||
}
|
||||
|
||||
Future<WaitForResult> _waitFor(Command command) async {
|
||||
final WaitFor waitForCommand = command as WaitFor;
|
||||
await _waitForElement(createFinder(waitForCommand.finder));
|
||||
return const WaitForResult();
|
||||
return super.deserializeCommand(params, finderFactory);
|
||||
}
|
||||
|
||||
Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
|
||||
final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent;
|
||||
await _waitForAbsentElement(createFinder(waitForAbsentCommand.finder));
|
||||
return const WaitForAbsentResult();
|
||||
@override
|
||||
@protected
|
||||
DataHandler? getDataHandler() {
|
||||
return _requestDataHandler;
|
||||
}
|
||||
|
||||
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;
|
||||
@override
|
||||
Future<Result?> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) {
|
||||
final String kind = command.kind;
|
||||
if(_commandExtensions.containsKey(kind)) {
|
||||
return _commandExtensions[kind]!.call(command, prober, finderFactory, this);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
|
||||
final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation;
|
||||
if (setTextEntryEmulationCommand.enabled) {
|
||||
_testTextInput.register();
|
||||
} else {
|
||||
_testTextInput.unregister();
|
||||
}
|
||||
return const SetTextEntryEmulationResult();
|
||||
}
|
||||
|
||||
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;
|
||||
return RequestDataResult(_requestDataHandler == null
|
||||
? 'No requestData Extension registered'
|
||||
: await _requestDataHandler!(requestDataCommand.message));
|
||||
}
|
||||
|
||||
Future<SetFrameSyncResult> _setFrameSync(Command command) async {
|
||||
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);
|
||||
return super.handleCommand(command, prober, finderFactory);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import 'package:flutter_driver/src/common/wait.dart';
|
||||
import 'package:flutter_driver/src/extension/extension.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_extension.dart';
|
||||
|
||||
@ -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', () {
|
||||
FlutterDriverExtension driverExtension;
|
||||
Map<String, dynamic> result;
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
import 'package:flutter_driver/flutter_driver.dart';
|
||||
import 'package:flutter_driver/src/common/find.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
@ -0,0 +1,58 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
// 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 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
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_driver/driver_extension.dart';
|
||||
import 'package:flutter_driver/src/common/handler_factory.dart';
|
||||
import 'package:flutter_driver/src/common/find.dart';
|
||||
|
||||
import 'stub_finder.dart';
|
||||
|
Loading…
x
Reference in New Issue
Block a user