| // 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 'deprecated.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() => SystemChannels.textInput.setMockMethodCallHandler(_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() => SystemChannels.textInput.setMockMethodCallHandler(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 => SystemChannels.textInput.checkMockMethodCallHandler(_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>; |
| break; |
| case 'TextInput.updateConfig': |
| setClientArgs = methodCall.arguments as Map<String, dynamic>; |
| break; |
| case 'TextInput.clearClient': |
| _client = null; |
| _isVisible = false; |
| _keyHandler = null; |
| onCleared?.call(); |
| break; |
| case 'TextInput.setEditingState': |
| editingState = methodCall.arguments as Map<String, dynamic>; |
| break; |
| case 'TextInput.show': |
| _isVisible = true; |
| if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) { |
| _keyHandler ??= MacOSTestTextInputKeyHandler(_client ?? -1); |
| } |
| break; |
| case 'TextInput.hide': |
| _isVisible = false; |
| _keyHandler = null; |
| break; |
| } |
| } |
| |
| /// 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 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); |
| } |
| } |