| // 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. | |
| /// @docImport 'widget_tester.dart'; | |
| library; | |
| 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; | |
| _keyHandler = null; | |
| } | |
| Future<dynamic> _handleTextInputCall(MethodCall methodCall) async { | |
| log.add(methodCall); | |
| switch (methodCall.method) { | |
| case 'TextInput.setClient': | |
| final 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 = 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); | |
| var 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 */ | |
| }, | |
| ); | |
| } | |
| } |