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 @@
/// }
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';
......
// 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 @@
// 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';
......
This diff is collapsed.
......@@ -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';
......
......@@ -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';
......
// 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 @@
// 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';
......
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