Emulate text entry in FlutterDriver (#13373)
* Emulate text entry in FlutterDriver * document enterText behavior * remove the unnecessary composint TextRange
This commit is contained in:
parent
5a1e639a2f
commit
e27bcd0f9d
@ -83,6 +83,9 @@ class DriverTestAppState extends State<DriverTestApp> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const TextField(
|
||||
key: const ValueKey<String>('enter-text-field'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -4,4 +4,7 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() => runApp(const Center(child: const Text('flutter drive lib/xxx.dart')));
|
||||
void main() => runApp(const Center(child: const Text(
|
||||
'flutter drive lib/xxx.dart',
|
||||
textDirection: TextDirection.ltr,
|
||||
)));
|
||||
|
@ -93,5 +93,14 @@ void main() {
|
||||
await driver.tap(a);
|
||||
await driver.waitForAbsent(menu);
|
||||
});
|
||||
|
||||
test('enters text in a text field', () async {
|
||||
final SerializableFinder textField = find.byValueKey('enter-text-field');
|
||||
await driver.tap(textField);
|
||||
await driver.enterText('Hello!');
|
||||
await driver.waitFor(find.text('Hello!'));
|
||||
await driver.enterText('World!');
|
||||
await driver.waitFor(find.text('World!'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -163,12 +163,13 @@ class ByTooltipMessage extends SerializableFinder {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Flutter Driver finder that finds widgets by [text] inside a `Text` widget.
|
||||
/// A Flutter Driver finder that finds widgets by [text] inside a [Text] or
|
||||
/// [EditableText] widget.
|
||||
class ByText extends SerializableFinder {
|
||||
/// Creates a text finder given the text.
|
||||
ByText(this.text);
|
||||
|
||||
/// The text that appears inside the `Text` widget.
|
||||
/// The text that appears inside the [Text] or [EditableText] widget.
|
||||
final String text;
|
||||
|
||||
@override
|
||||
@ -251,34 +252,3 @@ class ByType extends SerializableFinder {
|
||||
return new ByType(json['type']);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Flutter Driver command that reads the text from a given element.
|
||||
class GetText extends CommandWithTarget {
|
||||
/// [finder] looks for an element that contains a piece of text.
|
||||
GetText(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout);
|
||||
|
||||
/// Deserializes this command from the value generated by [serialize].
|
||||
GetText.deserialize(Map<String, dynamic> json) : super.deserialize(json);
|
||||
|
||||
@override
|
||||
final String kind = 'get_text';
|
||||
}
|
||||
|
||||
/// The result of the [GetText] command.
|
||||
class GetTextResult extends Result {
|
||||
/// Creates a result with the given [text].
|
||||
GetTextResult(this.text);
|
||||
|
||||
/// The text extracted by the [GetText] command.
|
||||
final String text;
|
||||
|
||||
/// Deserializes the result from JSON.
|
||||
static GetTextResult fromJson(Map<String, dynamic> json) {
|
||||
return new GetTextResult(json['text']);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => <String, String>{
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
|
73
packages/flutter_driver/lib/src/common/text.dart
Normal file
73
packages/flutter_driver/lib/src/common/text.dart
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2017 The Chromium 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 'find.dart';
|
||||
import 'message.dart';
|
||||
|
||||
/// A Flutter Driver command that reads the text from a given element.
|
||||
class GetText extends CommandWithTarget {
|
||||
/// [finder] looks for an element that contains a piece of text.
|
||||
GetText(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout);
|
||||
|
||||
/// Deserializes this command from the value generated by [serialize].
|
||||
GetText.deserialize(Map<String, dynamic> json) : super.deserialize(json);
|
||||
|
||||
@override
|
||||
final String kind = 'get_text';
|
||||
}
|
||||
|
||||
/// The result of the [GetText] command.
|
||||
class GetTextResult extends Result {
|
||||
/// Creates a result with the given [text].
|
||||
GetTextResult(this.text);
|
||||
|
||||
/// The text extracted by the [GetText] command.
|
||||
final String text;
|
||||
|
||||
/// Deserializes the result from JSON.
|
||||
static GetTextResult fromJson(Map<String, dynamic> json) {
|
||||
return new GetTextResult(json['text']);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => <String, String>{
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
|
||||
/// A Flutter Driver command that enters text into the currently focused widget.
|
||||
class EnterText extends Command {
|
||||
/// Creates a command that enters text into the currently focused widget.
|
||||
EnterText(this.text, { Duration timeout }) : super(timeout: timeout);
|
||||
|
||||
/// The text extracted by the [GetText] command.
|
||||
final String text;
|
||||
|
||||
/// Deserializes this command from the value generated by [serialize].
|
||||
EnterText.deserialize(Map<String, dynamic> json)
|
||||
: text = json['text'],
|
||||
super.deserialize(json);
|
||||
|
||||
@override
|
||||
final String kind = 'enter_text';
|
||||
|
||||
@override
|
||||
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
|
||||
'text': text,
|
||||
});
|
||||
}
|
||||
|
||||
/// The result of the [EnterText] command.
|
||||
class EnterTextResult extends Result {
|
||||
/// Creates a successful result of entering the text.
|
||||
EnterTextResult();
|
||||
|
||||
/// Deserializes the result from JSON.
|
||||
static EnterTextResult fromJson(Map<String, dynamic> json) {
|
||||
return new EnterTextResult();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => const <String, String>{};
|
||||
}
|
@ -23,6 +23,7 @@ import '../common/message.dart';
|
||||
import '../common/render_tree.dart';
|
||||
import '../common/request_data.dart';
|
||||
import '../common/semantics.dart';
|
||||
import '../common/text.dart';
|
||||
import 'common.dart';
|
||||
import 'timeline.dart';
|
||||
|
||||
@ -417,6 +418,39 @@ class FlutterDriver {
|
||||
return GetTextResult.fromJson(await _sendCommand(new GetText(finder, timeout: timeout))).text;
|
||||
}
|
||||
|
||||
/// Enters `text` into the currently focused text input, such as the
|
||||
/// [EditableText] widget.
|
||||
///
|
||||
/// This method does not use the operating system keyboard to enter text.
|
||||
/// Instead it emulates text entry by sending events identical to those sent
|
||||
/// by the operating system keyboard (the "TextInputClient.updateEditingState"
|
||||
/// method channel call).
|
||||
///
|
||||
/// Generally the behavior is dependent on the implementation of the widget
|
||||
/// receiving the input. Usually, editable widgets, such as [EditableText] and
|
||||
/// those built on top of it would replace the currently entered text with the
|
||||
/// provided `text`.
|
||||
///
|
||||
/// It is assumed that the widget receiving text input is focused prior to
|
||||
/// calling this method. Typically, a test would activate a widget, e.g. using
|
||||
/// [tap], then call this method.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// test('enters text in a text field', () async {
|
||||
/// var textField = find.byValueKey('enter-text-field');
|
||||
/// await driver.tap(textField); // acquire focus
|
||||
/// await driver.enterText('Hello!'); // enter text
|
||||
/// await driver.waitFor(find.text('Hello!')); // verify text appears on UI
|
||||
/// await driver.enterText('World!'); // enter another piece of text
|
||||
/// await driver.waitFor(find.text('World!')); // verify new text appears
|
||||
/// });
|
||||
/// ```
|
||||
Future<Null> enterText(String text, { Duration timeout }) async {
|
||||
await _sendCommand(new EnterText(text, timeout: timeout));
|
||||
}
|
||||
|
||||
/// Sends a string and returns a string.
|
||||
///
|
||||
/// This enables generic communication between the driver and the application.
|
||||
@ -694,7 +728,7 @@ Future<VMServiceClientConnection> _waitAndConnect(String url) async {
|
||||
class CommonFinders {
|
||||
const CommonFinders._();
|
||||
|
||||
/// Finds [Text] widgets containing string equal to [text].
|
||||
/// Finds [Text] and [EditableText] widgets containing string equal to [text].
|
||||
SerializableFinder text(String text) => new ByText(text);
|
||||
|
||||
/// Finds widgets by [key]. Only [String] and [int] values can be used.
|
||||
|
@ -24,6 +24,7 @@ import '../common/message.dart';
|
||||
import '../common/render_tree.dart';
|
||||
import '../common/request_data.dart';
|
||||
import '../common/semantics.dart';
|
||||
import '../common/text.dart';
|
||||
|
||||
const String _extensionMethodName = 'driver';
|
||||
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
|
||||
@ -83,11 +84,16 @@ typedef Finder FinderConstructor(SerializableFinder finder);
|
||||
/// calling [enableFlutterDriverExtension].
|
||||
@visibleForTesting
|
||||
class FlutterDriverExtension {
|
||||
final TestTextInput _testTextInput = new TestTextInput();
|
||||
|
||||
/// Creates an object to manage a Flutter Driver connection.
|
||||
FlutterDriverExtension(this._requestDataHandler) {
|
||||
_testTextInput.register();
|
||||
|
||||
_commandHandlers.addAll(<String, CommandHandlerCallback>{
|
||||
'get_health': _getHealth,
|
||||
'get_render_tree': _getRenderTree,
|
||||
'enter_text': _enterText,
|
||||
'get_text': _getText,
|
||||
'request_data': _requestData,
|
||||
'scroll': _scroll,
|
||||
@ -103,6 +109,7 @@ class FlutterDriverExtension {
|
||||
_commandDeserializers.addAll(<String, CommandDeserializerCallback>{
|
||||
'get_health': (Map<String, String> params) => new GetHealth.deserialize(params),
|
||||
'get_render_tree': (Map<String, String> params) => new GetRenderTree.deserialize(params),
|
||||
'enter_text': (Map<String, String> params) => new EnterText.deserialize(params),
|
||||
'get_text': (Map<String, String> params) => new GetText.deserialize(params),
|
||||
'request_data': (Map<String, String> params) => new RequestData.deserialize(params),
|
||||
'scroll': (Map<String, String> params) => new Scroll.deserialize(params),
|
||||
@ -325,6 +332,12 @@ class FlutterDriverExtension {
|
||||
return new GetTextResult(text.data);
|
||||
}
|
||||
|
||||
Future<EnterTextResult> _enterText(Command command) async {
|
||||
final EnterText enterTextCommand = command;
|
||||
_testTextInput.enterText(enterTextCommand.text);
|
||||
return new EnterTextResult();
|
||||
}
|
||||
|
||||
Future<RequestDataResult> _requestData(Command command) async {
|
||||
final RequestData requestDataCommand = command;
|
||||
return new RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message));
|
||||
|
@ -23,8 +23,8 @@ final CommonFinders find = const CommonFinders._();
|
||||
class CommonFinders {
|
||||
const CommonFinders._();
|
||||
|
||||
/// Finds [Text] widgets containing string equal to the `text`
|
||||
/// argument.
|
||||
/// Finds [Text] and [EditableText] widgets containing string equal to the
|
||||
/// `text` argument.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
@ -410,10 +410,14 @@ class _TextFinder extends MatchFinder {
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
if (candidate.widget is! Text)
|
||||
return false;
|
||||
final Text textWidget = candidate.widget;
|
||||
return textWidget.data == text;
|
||||
if (candidate.widget is Text) {
|
||||
final Text textWidget = candidate.widget;
|
||||
return textWidget.data == text;
|
||||
} else if (candidate.widget is EditableText) {
|
||||
final EditableText editable = candidate.widget;
|
||||
return editable.controller.text == text;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,11 @@ class TestTextInput {
|
||||
|
||||
/// Simulates the user changing the [TextEditingValue] to the given value.
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
expect(_client, isNonZero);
|
||||
// Not using the `expect` function because in the case of a FlutterDriver
|
||||
// test this code does not run in a package:test test zone.
|
||||
if (_client == 0) {
|
||||
throw new TestFailure('_client must be non-zero');
|
||||
}
|
||||
BinaryMessages.handlePlatformMessage(
|
||||
SystemChannels.textInput.name,
|
||||
SystemChannels.textInput.codec.encodeMethodCall(
|
||||
@ -82,7 +86,6 @@ class TestTextInput {
|
||||
void enterText(String text) {
|
||||
updateEditingValue(new TextEditingValue(
|
||||
text: text,
|
||||
composing: new TextRange(start: 0, end: text.length),
|
||||
));
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user