1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:typed_data'; 7 8import 'package:flutter/services.dart'; 9import 'package:flutter_test/flutter_test.dart'; 10import 'package:flutter/foundation.dart'; 11 12import 'widget_tester.dart'; 13 14export 'package:flutter/services.dart' show TextEditingValue, TextInputAction; 15 16/// A testing stub for the system's onscreen keyboard. 17/// 18/// Typical app tests will not need to use this class directly. 19/// 20/// See also: 21/// 22/// * [WidgetTester.enterText], which uses this class to simulate keyboard input. 23/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the 24/// popup keyboard and initializing its text. 25class TestTextInput { 26 /// Create a fake keyboard backend. 27 /// 28 /// The [onCleared] argument may be set to be notified of when the keyboard 29 /// is dismissed. 30 TestTextInput({ this.onCleared }); 31 32 /// Called when the keyboard goes away. 33 /// 34 /// To use the methods on this API that send fake keyboard messages (such as 35 /// [updateEditingValue], [enterText], or [receiveAction]), the keyboard must 36 /// first be requested, e.g. using [WidgetTester.showKeyboard]. 37 final VoidCallback onCleared; 38 39 /// Installs this object as a mock handler for [SystemChannels.textInput]. 40 void register() { 41 SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall); 42 _isRegistered = true; 43 } 44 45 /// Removes this object as a mock handler for [SystemChannels.textInput]. 46 /// 47 /// After calling this method, the channel will exchange messages with the 48 /// Flutter engine. Use this with [FlutterDriver] tests that need to display 49 /// on-screen keyboard provided by the operating system. 50 void unregister() { 51 SystemChannels.textInput.setMockMethodCallHandler(null); 52 _isRegistered = false; 53 } 54 55 /// Whether this [TestTextInput] is registered with [SystemChannels.textInput]. 56 /// 57 /// Use [register] and [unregister] methods to control this value. 58 bool get isRegistered => _isRegistered; 59 bool _isRegistered = false; 60 61 /// Whether there are any active clients listening to text input. 62 bool get hasAnyClients => _client > 0; 63 64 int _client = 0; 65 66 /// Arguments supplied to the TextInput.setClient method call. 67 Map<String, dynamic> setClientArgs; 68 69 /// The last set of arguments that [TextInputConnection.setEditingState] sent 70 /// to the embedder. 71 /// 72 /// This is a map representation of a [TextEditingValue] object. For example, 73 /// it will have a `text` entry whose value matches the most recent 74 /// [TextEditingValue.text] that was sent to the embedder. 75 Map<String, dynamic> editingState; 76 77 Future<dynamic> _handleTextInputCall(MethodCall methodCall) async { 78 switch (methodCall.method) { 79 case 'TextInput.setClient': 80 _client = methodCall.arguments[0]; 81 setClientArgs = methodCall.arguments[1]; 82 break; 83 case 'TextInput.clearClient': 84 _client = 0; 85 _isVisible = false; 86 if (onCleared != null) 87 onCleared(); 88 break; 89 case 'TextInput.setEditingState': 90 editingState = methodCall.arguments; 91 break; 92 case 'TextInput.show': 93 _isVisible = true; 94 break; 95 case 'TextInput.hide': 96 _isVisible = false; 97 break; 98 } 99 } 100 101 /// Whether the onscreen keyboard is visible to the user. 102 bool get isVisible => _isVisible; 103 bool _isVisible = false; 104 105 /// Simulates the user changing the [TextEditingValue] to the given value. 106 void updateEditingValue(TextEditingValue value) { 107 // Not using the `expect` function because in the case of a FlutterDriver 108 // test this code does not run in a package:test test zone. 109 if (_client == 0) 110 throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); 111 defaultBinaryMessenger.handlePlatformMessage( 112 SystemChannels.textInput.name, 113 SystemChannels.textInput.codec.encodeMethodCall( 114 MethodCall( 115 'TextInputClient.updateEditingState', 116 <dynamic>[_client, value.toJSON()], 117 ), 118 ), 119 (ByteData data) { /* response from framework is discarded */ }, 120 ); 121 } 122 123 /// Simulates the user typing the given text. 124 void enterText(String text) { 125 updateEditingValue(TextEditingValue( 126 text: text, 127 )); 128 } 129 130 /// Simulates the user pressing one of the [TextInputAction] buttons. 131 /// Does not check that the [TextInputAction] performed is an acceptable one 132 /// based on the `inputAction` [setClientArgs]. 133 Future<void> receiveAction(TextInputAction action) async { 134 return TestAsyncUtils.guard(() { 135 // Not using the `expect` function because in the case of a FlutterDriver 136 // test this code does not run in a package:test test zone. 137 if (_client == 0) { 138 throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); 139 } 140 141 final Completer<void> completer = Completer<void>(); 142 143 defaultBinaryMessenger.handlePlatformMessage( 144 SystemChannels.textInput.name, 145 SystemChannels.textInput.codec.encodeMethodCall( 146 MethodCall( 147 'TextInputClient.performAction', 148 <dynamic>[_client, action.toString()], 149 ), 150 ), 151 (ByteData data) { 152 try { 153 // Decoding throws a PlatformException if the data represents an 154 // error, and that's all we care about here. 155 SystemChannels.textInput.codec.decodeEnvelope(data); 156 157 // No error was found. Complete without issue. 158 completer.complete(); 159 } catch (error) { 160 // An exception occurred as a result of receiveAction()'ing. Report 161 // that error. 162 completer.completeError(error); 163 } 164 }, 165 ); 166 167 return completer.future; 168 }); 169 } 170 171 /// Simulates the user hiding the onscreen keyboard. 172 void hide() { 173 _isVisible = false; 174 } 175} 176