[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: Tong Mu <dkwingsmt@users.noreply.github.com> * Update packages/flutter_driver/lib/src/common/action.dart Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> * Update packages/flutter_driver/lib/src/common/action.dart Co-authored-by: Tong 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: Tong Mu <dkwingsmt@users.noreply.github.com> * Fix analyze * Add type dart for example * Add unit-test to check the same entries Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -94,3 +94,4 @@ Twin Sun, LLC <google-contrib@twinsunsolutions.com>
|
||||
Taskulu LDA <contributions@taskulu.com>
|
||||
Jonathan Joelson <jon@joelson.co>
|
||||
Elsabe Ros <hello@elsabe.dev>
|
||||
Nguyễn Phúc Lợi <nploi1998@gmail.com>
|
||||
|
||||
@@ -258,6 +258,9 @@ class TextInputType {
|
||||
///
|
||||
/// * [TextInput], which configures the platform's keyboard setup.
|
||||
/// * [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 {
|
||||
/// Logical meaning: There is no relevant input action for the current input
|
||||
/// source, e.g., [TextField].
|
||||
|
||||
@@ -26,6 +26,7 @@ export 'src/common/render_tree.dart';
|
||||
export 'src/common/request_data.dart';
|
||||
export 'src/common/semantics.dart';
|
||||
export 'src/common/text.dart';
|
||||
export 'src/common/text_input_action.dart';
|
||||
export 'src/common/wait.dart';
|
||||
export 'src/driver/common.dart';
|
||||
export 'src/driver/driver.dart';
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'render_tree.dart';
|
||||
import 'request_data.dart';
|
||||
import 'semantics.dart';
|
||||
import 'text.dart';
|
||||
import 'text_input_action.dart' show SendTextInputAction;
|
||||
import 'wait.dart';
|
||||
|
||||
/// A factory which creates [Finder]s from [SerializableFinder]s.
|
||||
@@ -159,6 +160,7 @@ mixin CommandHandlerFactory {
|
||||
case 'get_layer_tree': return _getLayerTree(command);
|
||||
case 'get_render_tree': return _getRenderTree(command);
|
||||
case 'enter_text': return _enterText(command);
|
||||
case 'send_text_input_action': return _sendTextInputAction(command);
|
||||
case 'get_text': return _getText(command, finderFactory);
|
||||
case 'request_data': return _requestData(command);
|
||||
case 'scroll': return _scroll(command, prober, finderFactory);
|
||||
@@ -204,6 +206,16 @@ mixin CommandHandlerFactory {
|
||||
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 {
|
||||
final RequestData requestDataCommand = command as RequestData;
|
||||
final DataHandler? dataHandler = getDataHandler();
|
||||
|
||||
189
packages/flutter_driver/lib/src/common/text_input_action.dart
Normal file
189
packages/flutter_driver/lib/src/common/text_input_action.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
// 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';
|
||||
import '../common/request_data.dart';
|
||||
import '../common/semantics.dart';
|
||||
import '../common/text.dart';
|
||||
import '../common/text_input_action.dart';
|
||||
import '../common/wait.dart';
|
||||
import 'timeline.dart';
|
||||
import 'vmservice_driver.dart';
|
||||
@@ -512,6 +513,34 @@ abstract class FlutterDriver {
|
||||
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.
|
||||
///
|
||||
/// This enables generic communication between the driver and the application.
|
||||
|
||||
@@ -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/health.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/driver/driver.dart';
|
||||
import 'package:flutter_driver/src/driver/timeline.dart';
|
||||
@@ -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', () {
|
||||
test('sends the getLayerTree command', () async {
|
||||
fakeClient.responses['get_layer_tree'] = makeFakeResponse(<String, String>{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user