Unverified Commit e27bcd0f authored by Yegor's avatar Yegor Committed by GitHub

Emulate text entry in FlutterDriver (#13373)

* Emulate text entry in FlutterDriver

* document enterText behavior

* remove the unnecessary composint TextRange
parent 5a1e639a
......@@ -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,
};
}
// 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),
));
}
......
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