Flutter Engine
The Flutter Engine
ListenableEditingStateTest.java
Go to the documentation of this file.
1package io.flutter.plugin.editing;
2
3import static org.junit.Assert.assertEquals;
4import static org.junit.Assert.assertFalse;
5import static org.junit.Assert.assertTrue;
6import static org.mockito.Mockito.mock;
7
8import android.content.Context;
9import android.text.Editable;
10import android.text.Selection;
11import android.view.View;
12import android.view.inputmethod.BaseInputConnection;
13import android.view.inputmethod.EditorInfo;
14import androidx.test.core.app.ApplicationProvider;
15import androidx.test.ext.junit.runners.AndroidJUnit4;
16import io.flutter.embedding.android.KeyboardManager;
17import io.flutter.embedding.engine.systemchannels.TextInputChannel;
18import java.util.ArrayList;
19import org.junit.Before;
20import org.junit.Test;
21import org.junit.runner.RunWith;
22import org.mockito.Mock;
23import org.mockito.MockitoAnnotations;
24import org.robolectric.annotation.Config;
25
26@Config(manifest = Config.NONE)
27@RunWith(AndroidJUnit4.class)
29 private final Context ctx = ApplicationProvider.getApplicationContext();
31
32 private BaseInputConnection getTestInputConnection(View view, Editable mEditable) {
33 new View(ctx);
34 return new BaseInputConnection(view, true) {
35 @Override
36 public Editable getEditable() {
37 return mEditable;
38 }
39 };
40 }
41
42 @Before
43 public void setUp() {
44 MockitoAnnotations.openMocks(this);
45 }
46
47 @Test
48 public void testConstructor() {
49 // When provided valid composing range, should not fail
51 new TextInputChannel.TextEditState("hello", 1, 4, 1, 4), new View(ctx));
52 }
53
54 // -------- Start: Test BatchEditing -------
55 @Test
56 public void testBatchEditing() {
57 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
58 final Listener listener = new Listener();
59 final View testView = new View(ctx);
60 final BaseInputConnection inputConnection = getTestInputConnection(testView, editingState);
61
62 editingState.addEditingStateListener(listener);
63
64 editingState.replace(0, editingState.length(), "update");
65 assertTrue(listener.isCalled());
66 assertTrue(listener.textChanged);
67 assertFalse(listener.selectionChanged);
68 assertFalse(listener.composingRegionChanged);
69
70 assertEquals(-1, editingState.getSelectionStart());
71 assertEquals(-1, editingState.getSelectionEnd());
72
73 listener.reset();
74
75 // Batch edit depth = 1.
76 editingState.beginBatchEdit();
77 editingState.replace(0, editingState.length(), "update1");
78 assertFalse(listener.isCalled());
79 // Batch edit depth = 2.
80 editingState.beginBatchEdit();
81 editingState.replace(0, editingState.length(), "update2");
82 inputConnection.setComposingRegion(0, editingState.length());
83 assertFalse(listener.isCalled());
84 // Batch edit depth = 1.
85 editingState.endBatchEdit();
86 assertFalse(listener.isCalled());
87
88 // Batch edit depth = 2.
89 editingState.beginBatchEdit();
90 assertFalse(listener.isCalled());
91 inputConnection.setSelection(0, 0);
92 assertFalse(listener.isCalled());
93 // Batch edit depth = 1.
94 editingState.endBatchEdit();
95 assertFalse(listener.isCalled());
96
97 // Remove composing region.
98 inputConnection.finishComposingText();
99
100 // Batch edit depth = 0. Last endBatchEdit.
101 editingState.endBatchEdit();
102
103 // Now notify the listener.
104 assertTrue(listener.isCalled());
105 assertTrue(listener.textChanged);
106 assertFalse(listener.composingRegionChanged);
107 }
108
109 @Test
111 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
112 final Listener listener = new Listener();
113 editingState.addEditingStateListener(listener);
114
115 editingState.endBatchEdit();
116 assertFalse(listener.isCalled());
117
118 editingState.replace(0, editingState.length(), "text");
119 assertTrue(listener.isCalled());
120 assertTrue(listener.textChanged);
121
122 listener.reset();
123 // Does not disrupt the followup events.
124 editingState.beginBatchEdit();
125 editingState.replace(0, editingState.length(), "more text");
126 assertFalse(listener.isCalled());
127 editingState.endBatchEdit();
128 assertTrue(listener.isCalled());
129 }
130
131 @Test
133 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
134 final Listener listener = new Listener();
135
136 editingState.beginBatchEdit();
137 editingState.addEditingStateListener(listener);
138 editingState.replace(0, editingState.length(), "update");
139 editingState.endBatchEdit();
140
141 assertTrue(listener.isCalled());
142 assertTrue(listener.textChanged);
143 assertTrue(listener.selectionChanged);
144 assertTrue(listener.composingRegionChanged);
145
146 listener.reset();
147
148 // Verifies the listener is officially added.
149 editingState.replace(0, editingState.length(), "more updates");
150 assertTrue(listener.isCalled());
151 assertTrue(listener.textChanged);
152 editingState.removeEditingStateListener(listener);
153
154 listener.reset();
155 // Now remove before endBatchEdit();
156 editingState.beginBatchEdit();
157 editingState.addEditingStateListener(listener);
158 editingState.replace(0, editingState.length(), "update");
159 editingState.removeEditingStateListener(listener);
160 editingState.endBatchEdit();
161
162 assertFalse(listener.isCalled());
163 }
164
165 @Test
167 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
168 final Listener listener = new Listener();
169 editingState.addEditingStateListener(listener);
170
171 editingState.beginBatchEdit();
172 editingState.replace(0, editingState.length(), "update");
173 editingState.removeEditingStateListener(listener);
174 editingState.endBatchEdit();
175
176 assertFalse(listener.isCalled());
177 }
178
179 @Test
181 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
182
183 final Listener listener =
184 new Listener() {
185 @Override
186 public void didChangeEditingState(
187 boolean textChanged, boolean selectionChanged, boolean composingRegionChanged) {
188 super.didChangeEditingState(textChanged, selectionChanged, composingRegionChanged);
189 editingState.replace(
190 0, editingState.length(), "one does not simply replace the text in the listener");
191 }
192 };
193 editingState.addEditingStateListener(listener);
194
195 editingState.beginBatchEdit();
196 editingState.replace(0, editingState.length(), "update");
197 editingState.endBatchEdit();
198
199 assertTrue(listener.isCalled());
200 assertEquals(1, listener.timesCalled);
201 assertEquals("one does not simply replace the text in the listener", editingState.toString());
202 }
203 // -------- End: Test BatchEditing -------
204
205 @Test
207 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
208 editingState.replace(0, editingState.length(), "text");
209
210 // (-1, -1) clears the composing region.
211 editingState.setComposingRange(-1, -1);
212 assertEquals(-1, editingState.getComposingStart());
213 assertEquals(-1, editingState.getComposingEnd());
214
215 editingState.setComposingRange(-1, 5);
216 assertEquals(-1, editingState.getComposingStart());
217 assertEquals(-1, editingState.getComposingEnd());
218
219 editingState.setComposingRange(2, 3);
220 assertEquals(2, editingState.getComposingStart());
221 assertEquals(3, editingState.getComposingEnd());
222
223 // Empty range is invalid. Clears composing region.
224 editingState.setComposingRange(1, 1);
225 assertEquals(-1, editingState.getComposingStart());
226 assertEquals(-1, editingState.getComposingEnd());
227
228 // Covers everything.
229 editingState.setComposingRange(0, editingState.length());
230 assertEquals(0, editingState.getComposingStart());
231 assertEquals(editingState.length(), editingState.getComposingEnd());
232 }
233
234 @Test
235 public void testClearBatchDeltas() {
236 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
237 editingState.replace(0, editingState.length(), "text");
238 editingState.delete(0, 1);
239 editingState.insert(0, "This is t");
240 editingState.clearBatchDeltas();
241 assertEquals(0, editingState.extractBatchTextEditingDeltas().size());
242 }
243
244 @Test
246 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
247
248 // Creating some deltas.
249 editingState.replace(0, editingState.length(), "test");
250 editingState.delete(0, 1);
251 editingState.insert(0, "This is a t");
252
253 ArrayList<TextEditingDelta> batchDeltas = editingState.extractBatchTextEditingDeltas();
254 assertEquals(3, batchDeltas.size());
255 }
256
257 // -------- Start: Test InputMethods actions -------
258 @Test
260 final ArrayList<String> batchMarkers = new ArrayList<>();
261 final ListenableEditingState editingState =
262 new ListenableEditingState(null, new View(ctx)) {
263 @Override
264 public final void beginBatchEdit() {
265 super.beginBatchEdit();
266 batchMarkers.add("begin");
267 }
268
269 @Override
270 public void endBatchEdit() {
271 super.endBatchEdit();
272 batchMarkers.add("end");
273 }
274 };
275
276 final Listener listener = new Listener();
277 final View testView = new View(ctx);
278 final InputConnectionAdaptor inputConnection =
280 testView,
281 0,
282 mock(TextInputChannel.class),
283 mockKeyboardManager,
284 editingState,
285 new EditorInfo());
286
287 // Make sure begin/endBatchEdit is called on the Editable when the input method calls
288 // InputConnection#begin/endBatchEdit.
289 inputConnection.beginBatchEdit();
290 assertEquals(1, batchMarkers.size());
291 assertEquals("begin", batchMarkers.get(0));
292
293 inputConnection.endBatchEdit();
294 assertEquals(2, batchMarkers.size());
295 assertEquals("end", batchMarkers.get(1));
296 }
297
298 @Test
300 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
301 final Listener listener = new Listener();
302 final View testView = new View(ctx);
303 final InputConnectionAdaptor inputConnection =
305 testView,
306 0,
307 mock(TextInputChannel.class),
308 mockKeyboardManager,
309 editingState,
310 new EditorInfo());
311 editingState.replace(0, editingState.length(), "initial text");
312
313 editingState.addEditingStateListener(listener);
314
315 inputConnection.setSelection(0, 0);
316
317 assertTrue(listener.isCalled());
318 assertFalse(listener.textChanged);
319 assertTrue(listener.selectionChanged);
320 assertFalse(listener.composingRegionChanged);
321
322 listener.reset();
323
324 inputConnection.setSelection(5, 5);
325
326 assertTrue(listener.isCalled());
327 assertFalse(listener.textChanged);
328 assertTrue(listener.selectionChanged);
329 assertFalse(listener.composingRegionChanged);
330 }
331
332 @Test
334 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
335 final Listener listener = new Listener();
336 final View testView = new View(ctx);
337 final InputConnectionAdaptor inputConnection =
339 testView,
340 0,
341 mock(TextInputChannel.class),
342 mockKeyboardManager,
343 editingState,
344 new EditorInfo());
345 editingState.replace(0, editingState.length(), "initial text");
346
347 editingState.addEditingStateListener(listener);
348
349 // setComposingRegion test.
350 inputConnection.setComposingRegion(1, 3);
351 assertTrue(listener.isCalled());
352 assertFalse(listener.textChanged);
353 assertFalse(listener.selectionChanged);
354 assertTrue(listener.composingRegionChanged);
355
356 Selection.setSelection(editingState, 0, 0);
357 listener.reset();
358
359 // setComposingText test: non-empty text, does not move cursor.
360 inputConnection.setComposingText("composing", -1);
361 assertTrue(listener.isCalled());
362 assertTrue(listener.textChanged);
363 assertFalse(listener.selectionChanged);
364 assertTrue(listener.composingRegionChanged);
365
366 listener.reset();
367 // setComposingText test: non-empty text, moves cursor.
368 inputConnection.setComposingText("composing2", 1);
369 assertTrue(listener.isCalled());
370 assertTrue(listener.textChanged);
371 assertTrue(listener.selectionChanged);
372 assertTrue(listener.composingRegionChanged);
373
374 listener.reset();
375 // setComposingText test: empty text.
376 inputConnection.setComposingText("", 1);
377 assertTrue(listener.isCalled());
378 assertTrue(listener.textChanged);
379 assertTrue(listener.selectionChanged);
380 assertTrue(listener.composingRegionChanged);
381
382 // finishComposingText test.
383 inputConnection.setComposingText("composing text", 1);
384 listener.reset();
385 inputConnection.finishComposingText();
386 assertTrue(listener.isCalled());
387 assertFalse(listener.textChanged);
388 assertFalse(listener.selectionChanged);
389 assertTrue(listener.composingRegionChanged);
390 }
391
392 @Test
394 final ListenableEditingState editingState = new ListenableEditingState(null, new View(ctx));
395 final Listener listener = new Listener();
396 final View testView = new View(ctx);
397 final InputConnectionAdaptor inputConnection =
399 testView,
400 0,
401 mock(TextInputChannel.class),
402 mockKeyboardManager,
403 editingState,
404 new EditorInfo());
405 editingState.replace(0, editingState.length(), "initial text");
406
407 editingState.addEditingStateListener(listener);
408 }
409 // -------- End: Test InputMethods actions -------
410
411 public static class Listener implements ListenableEditingState.EditingStateWatcher {
412 public boolean isCalled() {
413 return timesCalled > 0;
414 }
415
416 int timesCalled = 0;
417 boolean textChanged = false;
418 boolean selectionChanged = false;
419 boolean composingRegionChanged = false;
420
421 @Override
423 boolean textChanged, boolean selectionChanged, boolean composingRegionChanged) {
424 timesCalled++;
425 this.textChanged = textChanged;
426 this.selectionChanged = selectionChanged;
427 this.composingRegionChanged = composingRegionChanged;
428 }
429
430 public void reset() {
431 timesCalled = 0;
432 textChanged = false;
433 selectionChanged = false;
434 composingRegionChanged = false;
435 }
436 }
437}
boolean setComposingText(CharSequence text, int newCursorPosition)
void didChangeEditingState(boolean textChanged, boolean selectionChanged, boolean composingRegionChanged)
ArrayList< TextEditingDelta > extractBatchTextEditingDeltas()
void removeEditingStateListener(EditingStateWatcher listener)
SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend)
void addEditingStateListener(EditingStateWatcher listener)
void setComposingRange(int composingStart, int composingEnd)