| // 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 'dart:typed_data'; |
| |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:flutter/foundation.dart'; |
| |
| import 'widget_tester.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. |
| /// |
| /// 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; |
| |
| /// The messenger which sends the bytes for this channel, not null. |
| BinaryMessenger get _binaryMessenger => ServicesBinding.instance!.defaultBinaryMessenger; |
| |
| /// Resets any internal state of this object and calls [register]. |
| /// |
| /// This method is invoked by the testing framework between tests. It should |
| /// not ordinarily be called by tests directly. |
| void resetAndRegister() { |
| log.clear(); |
| editingState = null; |
| setClientArgs = null; |
| _client = 0; |
| _isVisible = false; |
| register(); |
| } |
| /// Installs this object as a mock handler for [SystemChannels.textInput]. |
| 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. Use this with [FlutterDriver] tests that need to display |
| /// on-screen keyboard provided by the operating system. |
| void unregister() => SystemChannels.textInput.setMockMethodCallHandler(null); |
| |
| /// 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>[]; |
| |
| /// Whether this [TestTextInput] is registered with [SystemChannels.textInput]. |
| /// |
| /// Use [register] and [unregister] methods to control this value. |
| bool get isRegistered => SystemChannels.textInput.checkMockMethodCallHandler(_handleTextInputCall); |
| |
| /// Whether there are any active clients listening to text input. |
| bool get hasAnyClients { |
| assert(isRegistered); |
| return _client > 0; |
| } |
| |
| int _client = 0; |
| |
| /// Arguments supplied to the TextInput.setClient method call. |
| Map<String, dynamic>? setClientArgs; |
| |
| /// The last set of arguments that [TextInputConnection.setEditingState] sent |
| /// to the embedder. |
| /// |
| /// 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; |
| |
| Future<dynamic> _handleTextInputCall(MethodCall methodCall) async { |
| log.add(methodCall); |
| switch (methodCall.method) { |
| case 'TextInput.setClient': |
| _client = methodCall.arguments[0] as int; |
| setClientArgs = methodCall.arguments[1] as Map<String, dynamic>; |
| break; |
| case 'TextInput.updateConfig': |
| setClientArgs = methodCall.arguments as Map<String, dynamic>; |
| break; |
| case 'TextInput.clearClient': |
| _client = 0; |
| _isVisible = false; |
| if (onCleared != null) |
| onCleared!(); |
| break; |
| case 'TextInput.setEditingState': |
| editingState = methodCall.arguments as Map<String, dynamic>; |
| break; |
| case 'TextInput.show': |
| _isVisible = true; |
| break; |
| case 'TextInput.hide': |
| _isVisible = false; |
| break; |
| } |
| } |
| |
| /// Whether the onscreen keyboard is visible to the user. |
| bool get isVisible { |
| assert(isRegistered); |
| return _isVisible; |
| } |
| bool _isVisible = false; |
| |
| /// Simulates the user changing the [TextEditingValue] to the given value. |
| void updateEditingValue(TextEditingValue value) { |
| assert(isRegistered); |
| // Not using the `expect` function because in the case of a FlutterDriver |
| // test this code does not run in a package:test test zone. |
| if (_client == 0) |
| throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); |
| _binaryMessenger.handlePlatformMessage( |
| SystemChannels.textInput.name, |
| SystemChannels.textInput.codec.encodeMethodCall( |
| MethodCall( |
| 'TextInputClient.updateEditingState', |
| <dynamic>[_client, value.toJSON()], |
| ), |
| ), |
| (ByteData? data) { /* response from framework is discarded */ }, |
| ); |
| } |
| |
| /// 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. |
| void closeConnection() { |
| assert(isRegistered); |
| // Not using the `expect` function because in the case of a FlutterDriver |
| // test this code does not run in a package:test test zone. |
| if (_client == 0) |
| throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); |
| _binaryMessenger.handlePlatformMessage( |
| SystemChannels.textInput.name, |
| SystemChannels.textInput.codec.encodeMethodCall( |
| MethodCall( |
| 'TextInputClient.onConnectionClosed', |
| <dynamic>[_client,] |
| ), |
| ), |
| (ByteData? data) { /* response from framework is discarded */ }, |
| ); |
| } |
| |
| /// Simulates the user typing the given text. |
| void enterText(String text) { |
| assert(isRegistered); |
| updateEditingValue(TextEditingValue( |
| text: text, |
| )); |
| } |
| |
| /// 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]. |
| Future<void> receiveAction(TextInputAction action) async { |
| assert(isRegistered); |
| return TestAsyncUtils.guard(() { |
| // Not using the `expect` function because in the case of a FlutterDriver |
| // test this code does not run in a package:test test zone. |
| if (_client == 0) { |
| throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); |
| } |
| |
| final Completer<void> completer = Completer<void>(); |
| |
| _binaryMessenger.handlePlatformMessage( |
| SystemChannels.textInput.name, |
| SystemChannels.textInput.codec.encodeMethodCall( |
| MethodCall( |
| 'TextInputClient.performAction', |
| <dynamic>[_client, 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!); |
| |
| // 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 hiding the onscreen keyboard. |
| void hide() { |
| assert(isRegistered); |
| _isVisible = false; |
| } |
| } |