diff --git a/dev/integration_tests/ui/lib/driver.dart b/dev/integration_tests/ui/lib/driver.dart index d23a2764d3..cb39d45356 100644 --- a/dev/integration_tests/ui/lib/driver.dart +++ b/dev/integration_tests/ui/lib/driver.dart @@ -83,6 +83,9 @@ class DriverTestAppState extends State { ), ], ), + const TextField( + key: const ValueKey('enter-text-field'), + ), ], ), ), diff --git a/dev/integration_tests/ui/lib/main.dart b/dev/integration_tests/ui/lib/main.dart index 66a92aad5f..c0d0595ca5 100644 --- a/dev/integration_tests/ui/lib/main.dart +++ b/dev/integration_tests/ui/lib/main.dart @@ -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, +))); diff --git a/dev/integration_tests/ui/test_driver/driver_test.dart b/dev/integration_tests/ui/test_driver/driver_test.dart index bca3c3d575..5b177c77bb 100644 --- a/dev/integration_tests/ui/test_driver/driver_test.dart +++ b/dev/integration_tests/ui/test_driver/driver_test.dart @@ -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!')); + }); }); } diff --git a/packages/flutter_driver/lib/src/common/find.dart b/packages/flutter_driver/lib/src/common/find.dart index d5541ad075..1d0d5b5ea0 100644 --- a/packages/flutter_driver/lib/src/common/find.dart +++ b/packages/flutter_driver/lib/src/common/find.dart @@ -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 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 json) { - return new GetTextResult(json['text']); - } - - @override - Map toJson() => { - 'text': text, - }; -} diff --git a/packages/flutter_driver/lib/src/common/text.dart b/packages/flutter_driver/lib/src/common/text.dart new file mode 100644 index 0000000000..bf9990c37d --- /dev/null +++ b/packages/flutter_driver/lib/src/common/text.dart @@ -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 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 json) { + return new GetTextResult(json['text']); + } + + @override + Map toJson() => { + '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 json) + : text = json['text'], + super.deserialize(json); + + @override + final String kind = 'enter_text'; + + @override + Map serialize() => super.serialize()..addAll({ + '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 json) { + return new EnterTextResult(); + } + + @override + Map toJson() => const {}; +} diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index 610998d394..8acb350517 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -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 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 _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. diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index ac968fb4ee..851fb9a085 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -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({ '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({ 'get_health': (Map params) => new GetHealth.deserialize(params), 'get_render_tree': (Map params) => new GetRenderTree.deserialize(params), + 'enter_text': (Map params) => new EnterText.deserialize(params), 'get_text': (Map params) => new GetText.deserialize(params), 'request_data': (Map params) => new RequestData.deserialize(params), 'scroll': (Map params) => new Scroll.deserialize(params), @@ -325,6 +332,12 @@ class FlutterDriverExtension { return new GetTextResult(text.data); } + Future _enterText(Command command) async { + final EnterText enterTextCommand = command; + _testTextInput.enterText(enterTextCommand.text); + return new EnterTextResult(); + } + Future _requestData(Command command) async { final RequestData requestDataCommand = command; return new RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message)); diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 63196f699f..776cc198d2 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -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; } } diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index 065264de75..4288bacc3f 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -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), )); }