Unverified Commit 0cf9d41f authored by Nguyen Phuc Loi's avatar Nguyen Phuc Loi Committed by GitHub

[flutter_driver] support send text input action (#106561)

* Support receive input action

* Fix error syntax

* Fix compile

* Add documents

* Add unit-test

* Update import

* Fixed unit-test and lint

* Add authors for me

* Fixed lint

* Fixed lint

* Add example

* Fixed lint

* Fix gen docs

* Revert code

* Remove flutter_dev

* Update packages/flutter_driver/lib/src/driver/driver.dart
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>

* Update packages/flutter_driver/lib/src/common/action.dart
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>

* Update packages/flutter_driver/lib/src/common/action.dart
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>

* Rename ReceiveAction to SendTextInputAction

* Rename DriverTextInputAction to TextInputAction and fix unit-test

* Reorder import

* Remove space

* Reorder import

* Update text_input.dart

* Update flutter_driver_test.dart

* Update comment to normal comment after dart doc

* Update example

* Update AUTHORS
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>

* Fix analyze

* Add type dart for example

* Add unit-test to check the same entries
Co-authored-by: 's avatarTong Mu <dkwingsmt@users.noreply.github.com>
parent da2e2ebd
...@@ -94,3 +94,4 @@ Twin Sun, LLC <google-contrib@twinsunsolutions.com> ...@@ -94,3 +94,4 @@ Twin Sun, LLC <google-contrib@twinsunsolutions.com>
Taskulu LDA <contributions@taskulu.com> Taskulu LDA <contributions@taskulu.com>
Jonathan Joelson <jon@joelson.co> Jonathan Joelson <jon@joelson.co>
Elsabe Ros <hello@elsabe.dev> Elsabe Ros <hello@elsabe.dev>
Nguyễn Phúc Lợi <nploi1998@gmail.com>
...@@ -258,6 +258,9 @@ class TextInputType { ...@@ -258,6 +258,9 @@ class TextInputType {
/// ///
/// * [TextInput], which configures the platform's keyboard setup. /// * [TextInput], which configures the platform's keyboard setup.
/// * [EditableText], which invokes callbacks when the action button is pressed. /// * [EditableText], which invokes callbacks when the action button is pressed.
//
// This class has been cloned to `flutter_driver/lib/src/common/action.dart` as `TextInputAction`,
// and must be kept in sync.
enum TextInputAction { enum TextInputAction {
/// Logical meaning: There is no relevant input action for the current input /// Logical meaning: There is no relevant input action for the current input
/// source, e.g., [TextField]. /// source, e.g., [TextField].
......
...@@ -26,6 +26,7 @@ export 'src/common/render_tree.dart'; ...@@ -26,6 +26,7 @@ export 'src/common/render_tree.dart';
export 'src/common/request_data.dart'; export 'src/common/request_data.dart';
export 'src/common/semantics.dart'; export 'src/common/semantics.dart';
export 'src/common/text.dart'; export 'src/common/text.dart';
export 'src/common/text_input_action.dart';
export 'src/common/wait.dart'; export 'src/common/wait.dart';
export 'src/driver/common.dart'; export 'src/driver/common.dart';
export 'src/driver/driver.dart'; export 'src/driver/driver.dart';
......
...@@ -26,6 +26,7 @@ import 'render_tree.dart'; ...@@ -26,6 +26,7 @@ import 'render_tree.dart';
import 'request_data.dart'; import 'request_data.dart';
import 'semantics.dart'; import 'semantics.dart';
import 'text.dart'; import 'text.dart';
import 'text_input_action.dart' show SendTextInputAction;
import 'wait.dart'; import 'wait.dart';
/// A factory which creates [Finder]s from [SerializableFinder]s. /// A factory which creates [Finder]s from [SerializableFinder]s.
...@@ -159,6 +160,7 @@ mixin CommandHandlerFactory { ...@@ -159,6 +160,7 @@ mixin CommandHandlerFactory {
case 'get_layer_tree': return _getLayerTree(command); case 'get_layer_tree': return _getLayerTree(command);
case 'get_render_tree': return _getRenderTree(command); case 'get_render_tree': return _getRenderTree(command);
case 'enter_text': return _enterText(command); case 'enter_text': return _enterText(command);
case 'send_text_input_action': return _sendTextInputAction(command);
case 'get_text': return _getText(command, finderFactory); case 'get_text': return _getText(command, finderFactory);
case 'request_data': return _requestData(command); case 'request_data': return _requestData(command);
case 'scroll': return _scroll(command, prober, finderFactory); case 'scroll': return _scroll(command, prober, finderFactory);
...@@ -204,6 +206,16 @@ mixin CommandHandlerFactory { ...@@ -204,6 +206,16 @@ mixin CommandHandlerFactory {
return Result.empty; return Result.empty;
} }
Future<Result> _sendTextInputAction(Command command) async {
if (!_testTextInput.isRegistered) {
throw StateError('Unable to fulfill `FlutterDriver.sendTextInputAction`. Text emulation is '
'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.');
}
final SendTextInputAction sendTextInputAction = command as SendTextInputAction;
_testTextInput.receiveAction(TextInputAction.values[sendTextInputAction.textInputAction.index]);
return Result.empty;
}
Future<RequestDataResult> _requestData(Command command) async { Future<RequestDataResult> _requestData(Command command) async {
final RequestData requestDataCommand = command as RequestData; final RequestData requestDataCommand = command as RequestData;
final DataHandler? dataHandler = getDataHandler(); final DataHandler? dataHandler = getDataHandler();
......
// 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 'enum_util.dart';
import 'message.dart';
EnumIndex<TextInputAction> _textInputActionIndex =
EnumIndex<TextInputAction>(TextInputAction.values);
/// A Flutter Driver command that send a text input action.
class SendTextInputAction extends Command {
/// Creates a command that enters text into the currently focused widget.
const SendTextInputAction(this.textInputAction, {super.timeout});
/// Deserializes this command from the value generated by [serialize].
SendTextInputAction.deserialize(super.json)
: textInputAction =
_textInputActionIndex.lookupBySimpleName(json['action']!),
super.deserialize();
/// The [TextInputAction]
final TextInputAction textInputAction;
@override
String get kind => 'send_text_input_action';
@override
Map<String, String> serialize() => super.serialize()
..addAll(<String, String>{
'action': _textInputActionIndex.toSimpleName(textInputAction),
});
}
/// An action the user has requested the text input control to perform.
///
// This class is identical to [TextInputAction](https://api.flutter.dev/flutter/services/TextInputAction.html).
// This class is cloned from `TextInputAction` and must be kept in sync. The cloning is needed
// because importing is not allowed directly.
enum TextInputAction {
/// Logical meaning: There is no relevant input action for the current input
/// source, e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
/// is decided by the OS. The keyboard will likely show a return key.
///
/// iOS: iOS does not have a keyboard return type of "none." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
none,
/// Logical meaning: Let the OS decide which action is most appropriate.
///
/// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
/// which keyboard action to display. The decision will likely be a done
/// button or a return key.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
/// the action button is "return".
unspecified,
/// Logical meaning: The user is done providing input to a group of inputs
/// (like a form). Some kind of finalization behavior should now take place.
///
/// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
/// button that represents completion, e.g., a checkmark button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in the
/// action button is "Done".
done,
/// Logical meaning: The user has entered some text that represents a
/// destination, e.g., a restaurant name. The "go" button is intended to take
/// the user to a part of the app that corresponds to this destination.
///
/// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
/// button that represents taking "the user to the target of the text they
/// typed", e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
/// action button is "Go".
go,
/// Logical meaning: Execute a search query.
///
/// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
/// button that represents a search, e.g., a magnifying glass button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in the
/// action button is "Search".
search,
/// Logical meaning: Sends something that the user has composed, e.g., an
/// email or a text message.
///
/// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
/// button that represents sending something, e.g., a paper plane button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
/// action button is "Send".
send,
/// Logical meaning: The user is done with the current input source and wants
/// to move to the next one.
///
/// Moves the focus to the next focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
/// button that represents moving forward, e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
/// action button is "Next".
next,
/// Logical meaning: The user wishes to return to the previous input source
/// in the group, e.g., a form with multiple [TextField]s.
///
/// Moves the focus to the previous focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
/// button that represents moving backward, e.g., a left-facing arrow button.
///
/// iOS: iOS does not have a keyboard return type of "previous." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
previous,
/// Logical meaning: In iOS apps, it is common for a "Back" button and
/// "Continue" button to appear at the top of the screen. However, when the
/// keyboard is open, these buttons are often hidden off-screen. Therefore,
/// the purpose of the "Continue" return key on iOS is to make the "Continue"
/// button available when the user is entering text.
///
/// Historical context aside, [TextInputAction.continueAction] can be used any
/// time that the term "Continue" seems most appropriate for the given action.
///
/// Android: Android does not have an IME input type of "continue." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
/// action button is "Continue". This action is only available on iOS 9.0+.
///
/// The reason that this value has "Action" post-fixed to it is because
/// "continue" is a reserved word in Dart, as well as many other languages.
continueAction,
/// Logical meaning: The user wants to join something, e.g., a wireless
/// network.
///
/// Android: Android does not have an IME input type of "join." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
/// action button is "Join".
join,
/// Logical meaning: The user wants routing options, e.g., driving directions.
///
/// Android: Android does not have an IME input type of "route." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
/// action button is "Route".
route,
/// Logical meaning: Initiate a call to emergency services.
///
/// Android: Android does not have an IME input type of "emergencyCall." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
/// in the action button is "Emergency Call".
emergencyCall,
/// Logical meaning: Insert a newline character in the focused text input,
/// e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
/// button that represents a new line, e.g., a carriage return button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in the
/// action button is "return".
///
/// The term [TextInputAction.newline] exists in Flutter but not in Android
/// or iOS. The reason for introducing this term is so that developers can
/// achieve the common result of inserting new lines without needing to
/// understand the various IME actions on Android and return keys on iOS.
/// Thus, [TextInputAction.newline] is a convenience term that alleviates the
/// need to understand the underlying platforms to achieve this common behavior.
newline,
}
...@@ -21,6 +21,7 @@ import '../common/render_tree.dart'; ...@@ -21,6 +21,7 @@ import '../common/render_tree.dart';
import '../common/request_data.dart'; import '../common/request_data.dart';
import '../common/semantics.dart'; import '../common/semantics.dart';
import '../common/text.dart'; import '../common/text.dart';
import '../common/text_input_action.dart';
import '../common/wait.dart'; import '../common/wait.dart';
import 'timeline.dart'; import 'timeline.dart';
import 'vmservice_driver.dart'; import 'vmservice_driver.dart';
...@@ -512,6 +513,34 @@ abstract class FlutterDriver { ...@@ -512,6 +513,34 @@ abstract class FlutterDriver {
await sendCommand(SetTextEntryEmulation(enabled, timeout: timeout)); await sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
} }
/// Simulate the user posting a text input action.
///
/// The available action types can be found in [TextInputAction]. The [sendTextInputAction]
/// does not check whether the [TextInputAction] performed is acceptable
/// based on the client arguments of the text input.
///
/// This can be called even if the [TestTextInput] has not been [TestTextInput.register]ed.
///
/// Example:
/// {@tool snippet}
///
/// ```dart
/// test('submit 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.sendTextInputAction(TextInputAction.done); // submit text
/// });
/// ```
/// {@end-tool}
///
Future<void> sendTextInputAction(TextInputAction action,
{Duration? timeout}) async {
assert(action != null);
await sendCommand(SendTextInputAction(action, timeout: timeout));
}
/// Sends a string and returns a string. /// Sends a string and returns a string.
/// ///
/// This enables generic communication between the driver and the application. /// This enables generic communication between the driver and the application.
......
...@@ -10,6 +10,7 @@ import 'package:fake_async/fake_async.dart'; ...@@ -10,6 +10,7 @@ import 'package:fake_async/fake_async.dart';
import 'package:flutter_driver/src/common/error.dart'; import 'package:flutter_driver/src/common/error.dart';
import 'package:flutter_driver/src/common/health.dart'; import 'package:flutter_driver/src/common/health.dart';
import 'package:flutter_driver/src/common/layer_tree.dart'; import 'package:flutter_driver/src/common/layer_tree.dart';
import 'package:flutter_driver/src/common/text_input_action.dart';
import 'package:flutter_driver/src/common/wait.dart'; import 'package:flutter_driver/src/common/wait.dart';
import 'package:flutter_driver/src/driver/driver.dart'; import 'package:flutter_driver/src/driver/driver.dart';
import 'package:flutter_driver/src/driver/timeline.dart'; import 'package:flutter_driver/src/driver/timeline.dart';
...@@ -351,6 +352,16 @@ void main() { ...@@ -351,6 +352,16 @@ void main() {
}); });
}); });
group('sendTextInputAction', () {
test('sends the SendTextInputAction command with action done', () async {
fakeClient.responses['send_text_input_action'] = makeFakeResponse(<String, dynamic>{});
await driver.sendTextInputAction(TextInputAction.done, timeout: _kTestTimeout);
expect(fakeClient.commandLog, <String>[
'ext.flutter.driver {command: send_text_input_action, timeout: $_kSerializedTestTimeout, action: done}',
]);
});
});
group('getLayerTree', () { group('getLayerTree', () {
test('sends the getLayerTree command', () async { test('sends the getLayerTree command', () async {
fakeClient.responses['get_layer_tree'] = makeFakeResponse(<String, String>{ fakeClient.responses['get_layer_tree'] = makeFakeResponse(<String, String>{
......
// 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/flutter_driver.dart' as flutter_driver;
import 'package:flutter_test/flutter_test.dart';
void main() {
test('flutter_driver.TextInputAction should be sync with TextInputAction',
() {
final List<String> actual = flutter_driver.TextInputAction.values
.map((flutter_driver.TextInputAction action) => action.name)
.toList();
final List<String> matcher = TextInputAction.values
.map((TextInputAction action) => action.name)
.toList();
expect(actual, matcher);
});
}
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