test_text_input.dart 7.92 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:typed_data';
7 8

import 'package:flutter/services.dart';
9
import 'package:flutter_test/flutter_test.dart';
10
import 'package:flutter/foundation.dart';
11

12 13
import 'widget_tester.dart';

14 15
export 'package:flutter/services.dart' show TextEditingValue, TextInputAction;

16 17 18 19 20 21 22 23 24 25
/// 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 {
26 27 28 29 30 31 32 33 34 35 36
  /// 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].
37
  final VoidCallback? onCleared;
38

39
  /// The messenger which sends the bytes for this channel, not null.
40
  BinaryMessenger get _binaryMessenger => ServicesBinding.instance!.defaultBinaryMessenger;
41

Dan Field's avatar
Dan Field committed
42 43 44 45 46 47 48 49 50 51 52 53
  /// 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();
  }
54
  /// Installs this object as a mock handler for [SystemChannels.textInput].
55
  void register() => SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall);
56

57 58 59 60 61
  /// 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.
62
  void unregister() => SystemChannels.textInput.setMockMethodCallHandler(null);
63

64 65 66
  /// Log for method calls.
  ///
  /// For all registered channels, handled calls are added to the list. Can
67
  /// be cleaned using `log.clear()`.
68 69
  final List<MethodCall> log = <MethodCall>[];

70 71 72
  /// Whether this [TestTextInput] is registered with [SystemChannels.textInput].
  ///
  /// Use [register] and [unregister] methods to control this value.
73
  bool get isRegistered => SystemChannels.textInput.checkMockMethodCallHandler(_handleTextInputCall);
74

75
  /// Whether there are any active clients listening to text input.
76 77 78 79
  bool get hasAnyClients {
    assert(isRegistered);
    return _client > 0;
  }
80

81
  int _client = 0;
82

83
  /// Arguments supplied to the TextInput.setClient method call.
84
  Map<String, dynamic>? setClientArgs;
85

86 87 88 89 90 91
  /// 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.
92
  Map<String, dynamic>? editingState;
93

94
  Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
95
    log.add(methodCall);
96
    switch (methodCall.method) {
97
      case 'TextInput.setClient':
98 99
        _client = methodCall.arguments[0] as int;
        setClientArgs = methodCall.arguments[1] as Map<String, dynamic>;
100
        break;
101 102 103
      case 'TextInput.updateConfig':
        setClientArgs = methodCall.arguments as Map<String, dynamic>;
        break;
104 105 106
      case 'TextInput.clearClient':
        _client = 0;
        _isVisible = false;
107
        if (onCleared != null)
108
          onCleared!();
109
        break;
110
      case 'TextInput.setEditingState':
111
        editingState = methodCall.arguments as Map<String, dynamic>;
112
        break;
113 114 115 116 117 118
      case 'TextInput.show':
        _isVisible = true;
        break;
      case 'TextInput.hide':
        _isVisible = false;
        break;
119 120 121
    }
  }

122
  /// Whether the onscreen keyboard is visible to the user.
123 124 125 126
  bool get isVisible {
    assert(isRegistered);
    return _isVisible;
  }
127 128 129
  bool _isVisible = false;

  /// Simulates the user changing the [TextEditingValue] to the given value.
130
  void updateEditingValue(TextEditingValue value) {
131
    assert(isRegistered);
132 133
    // Not using the `expect` function because in the case of a FlutterDriver
    // test this code does not run in a package:test test zone.
134
    if (_client == 0)
135
      throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
136
    _binaryMessenger.handlePlatformMessage(
137 138
      SystemChannels.textInput.name,
      SystemChannels.textInput.codec.encodeMethodCall(
139
        MethodCall(
140
          'TextInputClient.updateEditingState',
141
          <dynamic>[_client, value.toJSON()],
142 143
        ),
      ),
144
      (ByteData? data) { /* response from framework is discarded */ },
145
    );
146 147
  }

148 149 150 151 152 153
  /// 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() {
154
    assert(isRegistered);
155 156 157 158 159 160 161 162 163 164 165 166
    // 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,]
        ),
      ),
167
      (ByteData? data) { /* response from framework is discarded */ },
168 169 170
    );
  }

171
  /// Simulates the user typing the given text.
172
  void enterText(String text) {
173
    assert(isRegistered);
174
    updateEditingValue(TextEditingValue(
175 176 177
      text: text,
    ));
  }
178

179 180 181
  /// 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].
182
  Future<void> receiveAction(TextInputAction action) async {
183
    assert(isRegistered);
184 185 186 187
    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) {
188
        throw TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
189 190
      }

191
      final Completer<void> completer = Completer<void>();
192

193
      _binaryMessenger.handlePlatformMessage(
194 195
        SystemChannels.textInput.name,
        SystemChannels.textInput.codec.encodeMethodCall(
196
          MethodCall(
197 198 199
            'TextInputClient.performAction',
            <dynamic>[_client, action.toString()],
          ),
200
        ),
201 202
        (ByteData? data) {
          assert(data != null);
203 204 205
          try {
            // Decoding throws a PlatformException if the data represents an
            // error, and that's all we care about here.
206
            SystemChannels.textInput.codec.decodeEnvelope(data!);
207 208 209 210 211 212 213 214 215 216 217 218 219

            // 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;
    });
220 221
  }

222 223
  /// Simulates the user hiding the onscreen keyboard.
  void hide() {
224
    assert(isRegistered);
225 226
    _isVisible = false;
  }
227
}