• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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