mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
394 lines
15 KiB
Dart
394 lines
15 KiB
Dart
// 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 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'binding.dart';
|
|
import 'test_async_utils.dart';
|
|
import 'test_text_input_key_handler.dart';
|
|
|
|
export 'package:flutter/services.dart' show TextEditingValue, TextInputAction;
|
|
|
|
/// A testing stub for the system's onscreen keyboard.
|
|
///
|
|
/// Typical app tests will not need to use this class directly.
|
|
///
|
|
/// The [TestWidgetsFlutterBinding] class registers a [TestTextInput] instance
|
|
/// ([TestWidgetsFlutterBinding.testTextInput]) as a stub keyboard
|
|
/// implementation if its [TestWidgetsFlutterBinding.registerTestTextInput]
|
|
/// property returns true when a test starts, and unregisters it when the test
|
|
/// ends (unless it ends with a failure).
|
|
///
|
|
/// See [register], [unregister], and [isRegistered] for details.
|
|
///
|
|
/// The [enterText], [updateEditingValue], [receiveAction], and
|
|
/// [closeConnection] methods can be used even when the [TestTextInput] is not
|
|
/// registered. All other methods will assert if [isRegistered] is false.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [WidgetTester.enterText], which uses this class to simulate keyboard input.
|
|
/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
|
|
/// popup keyboard and initializing its text.
|
|
class TestTextInput {
|
|
/// Create a fake keyboard backend.
|
|
///
|
|
/// The [onCleared] argument may be set to be notified of when the keyboard
|
|
/// is dismissed.
|
|
TestTextInput({ this.onCleared });
|
|
|
|
/// Called when the keyboard goes away.
|
|
///
|
|
/// To use the methods on this API that send fake keyboard messages (such as
|
|
/// [updateEditingValue], [enterText], or [receiveAction]), the keyboard must
|
|
/// first be requested, e.g. using [WidgetTester.showKeyboard].
|
|
final VoidCallback? onCleared;
|
|
|
|
/// Log for method calls.
|
|
///
|
|
/// For all registered channels, handled calls are added to the list. Can
|
|
/// be cleaned using `log.clear()`.
|
|
final List<MethodCall> log = <MethodCall>[];
|
|
|
|
/// Installs this object as a mock handler for [SystemChannels.textInput].
|
|
///
|
|
/// Called by the binding at the top of a test when
|
|
/// [TestWidgetsFlutterBinding.registerTestTextInput] is true.
|
|
void register() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, _handleTextInputCall);
|
|
|
|
/// Removes this object as a mock handler for [SystemChannels.textInput].
|
|
///
|
|
/// After calling this method, the channel will exchange messages with the
|
|
/// Flutter engine instead of the stub.
|
|
///
|
|
/// Called by the binding at the end of a (successful) test when
|
|
/// [TestWidgetsFlutterBinding.registerTestTextInput] is true.
|
|
void unregister() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, null);
|
|
|
|
/// Whether this [TestTextInput] is registered with [SystemChannels.textInput].
|
|
///
|
|
/// The binding uses the [register] and [unregister] methods to control this
|
|
/// value when [TestWidgetsFlutterBinding.registerTestTextInput] is true.
|
|
bool get isRegistered => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(SystemChannels.textInput.name, _handleTextInputCall);
|
|
|
|
int? _client;
|
|
|
|
/// Whether there are any active clients listening to text input.
|
|
bool get hasAnyClients {
|
|
assert(isRegistered);
|
|
return _client != null && _client! > 0;
|
|
}
|
|
|
|
/// The last set of arguments supplied to the `TextInput.setClient` and
|
|
/// `TextInput.updateConfig` methods of this stub implementation.
|
|
Map<String, dynamic>? setClientArgs;
|
|
|
|
/// The last set of arguments that [TextInputConnection.setEditingState] sent
|
|
/// to this stub implementation (i.e. the arguments set to
|
|
/// `TextInput.setEditingState`).
|
|
///
|
|
/// This is a map representation of a [TextEditingValue] object. For example,
|
|
/// it will have a `text` entry whose value matches the most recent
|
|
/// [TextEditingValue.text] that was sent to the embedder.
|
|
Map<String, dynamic>? editingState;
|
|
|
|
/// Whether the onscreen keyboard is visible to the user.
|
|
///
|
|
/// Specifically, this reflects the last call to `TextInput.show` or
|
|
/// `TextInput.hide` received by the stub implementation.
|
|
bool get isVisible {
|
|
assert(isRegistered);
|
|
return _isVisible;
|
|
}
|
|
bool _isVisible = false;
|
|
|
|
// Platform specific key handler that can process unhandled keyboard events.
|
|
TestTextInputKeyHandler? _keyHandler;
|
|
|
|
/// Resets any internal state of this object.
|
|
///
|
|
/// This method is invoked by the testing framework between tests. It should
|
|
/// not ordinarily be called by tests directly.
|
|
void reset() {
|
|
log.clear();
|
|
_client = null;
|
|
setClientArgs = null;
|
|
editingState = null;
|
|
_isVisible = false;
|
|
}
|
|
|
|
Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
|
|
log.add(methodCall);
|
|
switch (methodCall.method) {
|
|
case 'TextInput.setClient':
|
|
final List<dynamic> arguments = methodCall.arguments as List<dynamic>;
|
|
_client = arguments[0] as int;
|
|
setClientArgs = arguments[1] as Map<String, dynamic>;
|
|
case 'TextInput.updateConfig':
|
|
setClientArgs = methodCall.arguments as Map<String, dynamic>;
|
|
case 'TextInput.clearClient':
|
|
_client = null;
|
|
_isVisible = false;
|
|
_keyHandler = null;
|
|
onCleared?.call();
|
|
case 'TextInput.setEditingState':
|
|
editingState = methodCall.arguments as Map<String, dynamic>;
|
|
case 'TextInput.show':
|
|
_isVisible = true;
|
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) {
|
|
_keyHandler ??= MacOSTestTextInputKeyHandler(_client ?? -1);
|
|
}
|
|
case 'TextInput.hide':
|
|
_isVisible = false;
|
|
_keyHandler = null;
|
|
}
|
|
}
|
|
|
|
/// Simulates the user hiding the onscreen keyboard.
|
|
///
|
|
/// This does nothing but set the internal flag.
|
|
void hide() {
|
|
assert(isRegistered);
|
|
_isVisible = false;
|
|
}
|
|
|
|
/// Simulates the user changing the text of the focused text field, and resets
|
|
/// the selection.
|
|
///
|
|
/// Calling this method replaces the content of the connected input field with
|
|
/// `text`, and places the caret at the end of the text.
|
|
///
|
|
/// To update the UI under test after this method is invoked, use
|
|
/// [WidgetTester.pump].
|
|
///
|
|
/// This can be called even if the [TestTextInput] has not been [register]ed.
|
|
///
|
|
/// If this is used to inject text when there is a real IME connection, for
|
|
/// example when using the [integration_test] library, there is a risk that
|
|
/// the real IME will become confused as to the current state of input.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [updateEditingValue], which takes a [TextEditingValue] so that one can
|
|
/// also change the selection.
|
|
void enterText(String text) {
|
|
updateEditingValue(TextEditingValue(
|
|
text: text,
|
|
selection: TextSelection.collapsed(offset: text.length),
|
|
));
|
|
}
|
|
|
|
/// Simulates the user changing the [TextEditingValue] to the given value.
|
|
///
|
|
/// To update the UI under test after this method is invoked, use
|
|
/// [WidgetTester.pump].
|
|
///
|
|
/// This can be called even if the [TestTextInput] has not been [register]ed.
|
|
///
|
|
/// If this is used to inject text when there is a real IME connection, for
|
|
/// example when using the [integration_test] library, there is a risk that
|
|
/// the real IME will become confused as to the current state of input.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [enterText], which is similar but takes only a String and resets the
|
|
/// selection.
|
|
void updateEditingValue(TextEditingValue value) {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.updateEditingState',
|
|
<dynamic>[_client ?? -1, value.toJSON()],
|
|
),
|
|
),
|
|
(ByteData? data) { /* ignored */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates the user pressing one of the [TextInputAction] buttons.
|
|
/// Does not check that the [TextInputAction] performed is an acceptable one
|
|
/// based on the `inputAction` [setClientArgs].
|
|
///
|
|
/// This can be called even if the [TestTextInput] has not been [register]ed.
|
|
///
|
|
/// If this is used to inject an action when there is a real IME connection,
|
|
/// for example when using the [integration_test] library, there is a risk
|
|
/// that the real IME will become confused as to the current state of input.
|
|
Future<void> receiveAction(TextInputAction action) async {
|
|
return TestAsyncUtils.guard(() {
|
|
final Completer<void> completer = Completer<void>();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.performAction',
|
|
<dynamic>[_client ?? -1, action.toString()],
|
|
),
|
|
),
|
|
(ByteData? data) {
|
|
assert(data != null);
|
|
try {
|
|
// Decoding throws a PlatformException if the data represents an
|
|
// error, and that's all we care about here.
|
|
SystemChannels.textInput.codec.decodeEnvelope(data!);
|
|
// If we reach here then no error was found. Complete without issue.
|
|
completer.complete();
|
|
} catch (error) {
|
|
// An exception occurred as a result of receiveAction()'ing. Report
|
|
// that error.
|
|
completer.completeError(error);
|
|
}
|
|
},
|
|
);
|
|
return completer.future;
|
|
});
|
|
}
|
|
|
|
/// Simulates the user closing the text input connection.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// * User pressed the home button and sent the application to background.
|
|
/// * User closed the virtual keyboard.
|
|
///
|
|
/// This can be called even if the [TestTextInput] has not been [register]ed.
|
|
///
|
|
/// If this is used to inject text when there is a real IME connection, for
|
|
/// example when using the [integration_test] library, there is a risk that
|
|
/// the real IME will become confused as to the current state of input.
|
|
void closeConnection() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.onConnectionClosed',
|
|
<dynamic>[_client ?? -1],
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates a scribble interaction starting.
|
|
Future<void> startScribbleInteraction() async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.scribbleInteractionBegan',
|
|
<dynamic>[_client ?? -1,]
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates a scribble interaction finishing.
|
|
Future<void> finishScribbleInteraction() async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.scribbleInteractionFinished',
|
|
<dynamic>[_client ?? -1,]
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates a Scribble focus.
|
|
Future<void> scribbleFocusElement(String elementIdentifier, Offset offset) async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.focusElement',
|
|
<dynamic>[elementIdentifier, offset.dx, offset.dy]
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates iOS asking for the list of Scribble elements during UIIndirectScribbleInteraction.
|
|
Future<List<List<dynamic>>> scribbleRequestElementsInRect(Rect rect) async {
|
|
assert(isRegistered);
|
|
List<List<dynamic>> response = <List<dynamic>>[];
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.requestElementsInRect',
|
|
<dynamic>[rect.left, rect.top, rect.width, rect.height]
|
|
),
|
|
),
|
|
(ByteData? data) {
|
|
response = (SystemChannels.textInput.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
|
|
},
|
|
);
|
|
|
|
return response;
|
|
}
|
|
|
|
/// Simulates iOS inserting a UITextPlaceholder during a long press with the pencil.
|
|
Future<void> scribbleInsertPlaceholder() async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.insertTextPlaceholder',
|
|
<dynamic>[_client ?? -1, 0.0, 0.0]
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Simulates iOS removing a UITextPlaceholder after a long press with the pencil is released.
|
|
Future<void> scribbleRemovePlaceholder() async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall(
|
|
'TextInputClient.removeTextPlaceholder',
|
|
<dynamic>[_client ?? -1]
|
|
),
|
|
),
|
|
(ByteData? data) { /* response from framework is discarded */ },
|
|
);
|
|
}
|
|
|
|
/// Gives text input chance to respond to unhandled key down event.
|
|
Future<void> handleKeyDownEvent(LogicalKeyboardKey key) async {
|
|
await _keyHandler?.handleKeyDownEvent(key);
|
|
}
|
|
|
|
/// Gives text input chance to respond to unhandled key up event.
|
|
Future<void> handleKeyUpEvent(LogicalKeyboardKey key) async {
|
|
await _keyHandler?.handleKeyUpEvent(key);
|
|
}
|
|
|
|
/// Simulates iOS responding to an undo or redo gesture or button.
|
|
Future<void> handleKeyboardUndo(String direction) async {
|
|
assert(isRegistered);
|
|
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
|
SystemChannels.textInput.name,
|
|
SystemChannels.textInput.codec.encodeMethodCall(
|
|
MethodCall('TextInputClient.handleUndo', <dynamic>[direction]),
|
|
),
|
|
(ByteData? data) {/* response from framework is discarded */},
|
|
);
|
|
}
|
|
}
|