InputLogicTests.java revision 6ec1209a33fe2dc151b86d3f662e22e564e2f4f8
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.SharedPreferences;
22import android.os.Looper;
23import android.os.MessageQueue;
24import android.preference.PreferenceManager;
25import android.test.ServiceTestCase;
26import android.text.InputType;
27import android.text.SpannableStringBuilder;
28import android.text.style.SuggestionSpan;
29import android.util.Log;
30import android.view.LayoutInflater;
31import android.view.ViewGroup;
32import android.view.View;
33import android.view.inputmethod.BaseInputConnection;
34import android.view.inputmethod.EditorInfo;
35import android.view.inputmethod.InputConnection;
36import android.widget.FrameLayout;
37import android.widget.TextView;
38
39import com.android.inputmethod.keyboard.Keyboard;
40import com.android.inputmethod.keyboard.KeyboardActionListener;
41import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService; // for proximity info
42import com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo;
43
44import java.util.Arrays;
45import java.util.HashMap;
46
47public class InputLogicTests extends ServiceTestCase<LatinIME> {
48
49    private static final String PREF_DEBUG_MODE = "debug_mode";
50
51    private LatinIME mLatinIME;
52    private TextView mTextView;
53    private InputConnection mInputConnection;
54
55    public InputLogicTests() {
56        super(LatinIME.class);
57    }
58
59    // returns the previous setting value
60    private boolean setDebugMode(final boolean mode) {
61        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
62        final boolean previousDebugSetting = prefs.getBoolean(PREF_DEBUG_MODE, false);
63        final SharedPreferences.Editor editor = prefs.edit();
64        editor.putBoolean(PREF_DEBUG_MODE, true);
65        editor.commit();
66        return previousDebugSetting;
67    }
68
69    @Override
70    protected void setUp() {
71        try {
72            super.setUp();
73        } catch (Exception e) {
74            e.printStackTrace();
75        }
76        mTextView = new TextView(getContext());
77        mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
78        mTextView.setEnabled(true);
79        setupService();
80        mLatinIME = getService();
81        final boolean previousDebugSetting = setDebugMode(true);
82        mLatinIME.onCreate();
83        setDebugMode(previousDebugSetting);
84        final EditorInfo ei = new EditorInfo();
85        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
86        final InputConnection ic = mTextView.onCreateInputConnection(ei);
87        ei.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
88        final LayoutInflater inflater =
89                (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
90        final ViewGroup vg = new FrameLayout(getContext());
91        final View inputView = inflater.inflate(R.layout.input_view, vg);
92        mLatinIME.setInputView(inputView);
93        mLatinIME.onBindInput();
94        mLatinIME.onCreateInputView();
95        mLatinIME.onStartInputView(ei, false);
96        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
97        mInputConnection = ic;
98        changeLanguage("en_US");
99    }
100
101    // We need to run the messages added to the handler from LatinIME. The only way to do
102    // that is to call Looper#loop() on the right looper, so we're going to get the looper
103    // object and call #loop() here. The messages in the handler actually run on the UI
104    // thread of the keyboard by design of the handler, so we want to call it synchronously
105    // on the same thread that the tests are running on to mimic the actual environment as
106    // closely as possible.
107    // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method
108    // is called, so we need to do that at the right time so that #loop() returns at some
109    // point and we don't end up in an infinite loop.
110    // After we quit, the looper is still technically ready to process more messages but
111    // the handler will refuse to enqueue any because #quit() has been called and it
112    // explicitly tests for it on message enqueuing, so we'll have to reset it so that
113    // it lets us continue normal operation.
114    private void runMessages() {
115        // Here begins deep magic.
116        final Looper looper = mLatinIME.mHandler.getLooper();
117        mLatinIME.mHandler.post(new Runnable() {
118                @Override
119                public void run() {
120                    looper.quit();
121                }
122            });
123        // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue).
124        // Once #quit() is called remaining messages are not processed, which is why we post
125        // a message that calls it instead of calling it directly.
126        looper.loop();
127
128        // Once #quit() has been called, the message queue has an "mQuiting" field that prevents
129        // any subsequent post in this queue. However the queue itself is still fully functional!
130        // If we have a way of resetting "queue.mQuiting" then we can continue using it as normal,
131        // coming back to this method to run the messages.
132        MessageQueue queue = looper.getQueue();
133        try {
134            // However there is no way of doing it externally, and mQuiting is private.
135            // So... get out the big guns.
136            java.lang.reflect.Field f = MessageQueue.class.getDeclaredField("mQuiting");
137            f.setAccessible(true); // What do you mean "private"?
138            f.setBoolean(queue, false);
139        } catch (NoSuchFieldException e) {
140            throw new RuntimeException(e);
141        } catch (IllegalAccessException e) {
142            throw new RuntimeException(e);
143        }
144    }
145
146    // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME.
147    private void type(final int codePoint) {
148        // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the
149        // code (although multitouch/slide input and other factors make the sequencing complicated).
150        // They are supposed to be entirely deconnected from the input logic from LatinIME point of
151        // view and only delegates to the parts of the code that care. So we don't include them here
152        // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies,
153        // but keep them in mind if something breaks. Commenting them out as is should work.
154        //mLatinIME.onPressKey(codePoint);
155        mLatinIME.onCodeInput(codePoint,
156                KeyboardActionListener.SPELL_CHECKER_COORDINATE,
157                KeyboardActionListener.SPELL_CHECKER_COORDINATE);
158        //mLatinIME.onReleaseKey(codePoint, false);
159    }
160
161    private void type(final String stringToType) {
162        for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) {
163            type(stringToType.codePointAt(i));
164        }
165    }
166
167    private void waitForDictionaryToBeLoaded() {
168        int remainingAttempts = 10;
169        while (remainingAttempts > 0 && !mLatinIME.mSuggest.hasMainDictionary()) {
170            try {
171                Thread.sleep(200);
172            } catch (InterruptedException e) {
173                // Don't do much
174            } finally {
175                --remainingAttempts;
176            }
177        }
178        if (!mLatinIME.mSuggest.hasMainDictionary()) {
179            throw new RuntimeException("Can't initialize the main dictionary");
180        }
181    }
182
183    private void changeLanguage(final String locale) {
184        SubtypeSwitcher.getInstance().updateSubtype(
185                new ArbitrarySubtype(locale, LatinIME.SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE));
186        waitForDictionaryToBeLoaded();
187    }
188
189
190    // Helper to avoid writing the try{}catch block each time
191    private static void sleep(final int milliseconds) {
192        try {
193            Thread.sleep(milliseconds);
194        } catch (InterruptedException e) {}
195    }
196
197    public void testTypeWord() {
198        final String WORD_TO_TYPE = "abcd";
199        type(WORD_TO_TYPE);
200        assertEquals("type word", WORD_TO_TYPE, mTextView.getText().toString());
201    }
202
203    public void testPickSuggestionThenBackspace() {
204        final String WORD_TO_TYPE = "this";
205        final String EXPECTED_RESULT = "this";
206        type(WORD_TO_TYPE);
207        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
208        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
209        type(Keyboard.CODE_DELETE);
210        assertEquals("press suggestion then backspace", EXPECTED_RESULT,
211                mTextView.getText().toString());
212    }
213
214    public void testPickAutoCorrectionThenBackspace() {
215        final String WORD_TO_TYPE = "tgis";
216        final String WORD_TO_PICK = "this";
217        final String EXPECTED_RESULT = "tgis";
218        type(WORD_TO_TYPE);
219        // Choose the auto-correction, which is always in position 0. For "tgis", the
220        // auto-correction should be "this".
221        mLatinIME.pickSuggestionManually(0, WORD_TO_PICK);
222        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
223        assertEquals("pick typed word over auto-correction then backspace", WORD_TO_PICK,
224                mTextView.getText().toString());
225        type(Keyboard.CODE_DELETE);
226        assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT,
227                mTextView.getText().toString());
228    }
229
230    public void testPickTypedWordOverAutoCorrectionThenBackspace() {
231        final String WORD_TO_TYPE = "tgis";
232        final String EXPECTED_RESULT = "tgis";
233        type(WORD_TO_TYPE);
234        // Choose the typed word, which should be in position 1 (because position 0 should
235        // be occupied by the "this" auto-correction, as checked by testAutoCorrect())
236        mLatinIME.pickSuggestionManually(1, WORD_TO_TYPE);
237        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
238        assertEquals("pick typed word over auto-correction then backspace", WORD_TO_TYPE,
239                mTextView.getText().toString());
240        type(Keyboard.CODE_DELETE);
241        assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT,
242                mTextView.getText().toString());
243    }
244
245    public void testPickDifferentSuggestionThenBackspace() {
246        final String WORD_TO_TYPE = "tgis";
247        final String WORD_TO_PICK = "thus";
248        final String EXPECTED_RESULT = "tgis";
249        type(WORD_TO_TYPE);
250        // Choose the second suggestion, which should be in position 2 and should be "thus"
251        // when "tgis is typed.
252        mLatinIME.pickSuggestionManually(2, WORD_TO_PICK);
253        mLatinIME.onUpdateSelection(0, 0, WORD_TO_TYPE.length(), WORD_TO_TYPE.length(), -1, -1);
254        assertEquals("pick different suggestion then backspace", WORD_TO_PICK,
255                mTextView.getText().toString());
256        type(Keyboard.CODE_DELETE);
257        assertEquals("pick different suggestion then backspace", EXPECTED_RESULT,
258                mTextView.getText().toString());
259    }
260
261    public void testDeleteSelection() {
262        final String STRING_TO_TYPE = "some text delete me some text";
263        final int SELECTION_START = 10;
264        final int SELECTION_END = 19;
265        final String EXPECTED_RESULT = "some text  some text";
266        type(STRING_TO_TYPE);
267        // There is no IMF to call onUpdateSelection for us so we must do it by hand.
268        // Send once to simulate the cursor actually responding to the move caused by typing.
269        // This is necessary because LatinIME is bookkeeping to avoid confusing a real cursor
270        // move with a move triggered by LatinIME inputting stuff.
271        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
272        mInputConnection.setSelection(SELECTION_START, SELECTION_END);
273        // And now we simulate the user actually selecting some text.
274        mLatinIME.onUpdateSelection(0, 0, SELECTION_START, SELECTION_END, -1, -1);
275        type(Keyboard.CODE_DELETE);
276        assertEquals("delete selection", EXPECTED_RESULT, mTextView.getText().toString());
277    }
278
279    public void testAutoCorrect() {
280        final String STRING_TO_TYPE = "tgis ";
281        final String EXPECTED_RESULT = "this ";
282        type(STRING_TO_TYPE);
283        assertEquals("simple auto-correct", EXPECTED_RESULT, mTextView.getText().toString());
284    }
285
286    public void testAutoCorrectForFrench() {
287        final String STRING_TO_TYPE = "irq ";
288        final String EXPECTED_RESULT = "ira ";
289        changeLanguage("fr");
290        type(STRING_TO_TYPE);
291        assertEquals("simple auto-correct for French", EXPECTED_RESULT,
292                mTextView.getText().toString());
293    }
294
295    public void testAutoCorrectWithPeriod() {
296        final String STRING_TO_TYPE = "tgis.";
297        final String EXPECTED_RESULT = "this.";
298        type(STRING_TO_TYPE);
299        assertEquals("auto-correct with period", EXPECTED_RESULT, mTextView.getText().toString());
300    }
301
302    public void testAutoCorrectWithPeriodThenRevert() {
303        final String STRING_TO_TYPE = "tgis.";
304        final String EXPECTED_RESULT = "tgis.";
305        type(STRING_TO_TYPE);
306        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
307        type(Keyboard.CODE_DELETE);
308        assertEquals("auto-correct with period then revert", EXPECTED_RESULT,
309                mTextView.getText().toString());
310    }
311
312    public void testDoubleSpace() {
313        final String STRING_TO_TYPE = "this  ";
314        final String EXPECTED_RESULT = "this. ";
315        type(STRING_TO_TYPE);
316        assertEquals("double space make a period", EXPECTED_RESULT, mTextView.getText().toString());
317    }
318
319    public void testCancelDoubleSpace() {
320        final String STRING_TO_TYPE = "this  ";
321        final String EXPECTED_RESULT = "this  ";
322        type(STRING_TO_TYPE);
323        type(Keyboard.CODE_DELETE);
324        assertEquals("double space make a period", EXPECTED_RESULT, mTextView.getText().toString());
325    }
326
327    public void testBackspaceAtStartAfterAutocorrect() {
328        final String STRING_TO_TYPE = "tgis ";
329        final String EXPECTED_RESULT = "this ";
330        final int NEW_CURSOR_POSITION = 0;
331        type(STRING_TO_TYPE);
332        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
333        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
334        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
335        type(Keyboard.CODE_DELETE);
336        assertEquals("auto correct then move cursor to start of line then backspace",
337                EXPECTED_RESULT, mTextView.getText().toString());
338    }
339
340    public void testAutoCorrectThenMoveCursorThenBackspace() {
341        final String STRING_TO_TYPE = "and tgis ";
342        final String EXPECTED_RESULT = "andthis ";
343        final int NEW_CURSOR_POSITION = STRING_TO_TYPE.indexOf('t');
344        type(STRING_TO_TYPE);
345        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
346        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
347        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
348        type(Keyboard.CODE_DELETE);
349        assertEquals("auto correct then move cursor then backspace",
350                EXPECTED_RESULT, mTextView.getText().toString());
351    }
352
353    public void testNoSpaceAfterManualPick() {
354        final String WORD_TO_TYPE = "this";
355        final String EXPECTED_RESULT = WORD_TO_TYPE;
356        type(WORD_TO_TYPE);
357        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
358        assertEquals("no space after manual pick", EXPECTED_RESULT,
359                mTextView.getText().toString());
360    }
361
362    public void testManualPickThenType() {
363        final String WORD1_TO_TYPE = "this";
364        final String WORD2_TO_TYPE = "is";
365        final String EXPECTED_RESULT = "this is";
366        type(WORD1_TO_TYPE);
367        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
368        type(WORD2_TO_TYPE);
369        assertEquals("manual pick then type", EXPECTED_RESULT, mTextView.getText().toString());
370    }
371
372    public void testManualPickThenSeparator() {
373        final String WORD1_TO_TYPE = "this";
374        final String WORD2_TO_TYPE = "!";
375        final String EXPECTED_RESULT = "this!";
376        type(WORD1_TO_TYPE);
377        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
378        type(WORD2_TO_TYPE);
379        assertEquals("manual pick then separator", EXPECTED_RESULT, mTextView.getText().toString());
380    }
381
382    public void testWordThenSpaceThenPunctuationFromStripTwice() {
383        final String WORD_TO_TYPE = "this ";
384        final String PUNCTUATION_FROM_STRIP = "!";
385        final String EXPECTED_RESULT = "this!! ";
386        type(WORD_TO_TYPE);
387        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
388        runMessages();
389        assertTrue("type word then type space should display punctuation strip",
390                mLatinIME.isShowingPunctuationList());
391        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
392        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
393        assertEquals("type word then type space then punctuation from strip twice", EXPECTED_RESULT,
394                mTextView.getText().toString());
395    }
396
397    public void testManualPickThenSeparatorForFrench() {
398        final String WORD1_TO_TYPE = "test";
399        final String WORD2_TO_TYPE = "!";
400        final String EXPECTED_RESULT = "test !";
401        changeLanguage("fr");
402        type(WORD1_TO_TYPE);
403        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
404        type(WORD2_TO_TYPE);
405        assertEquals("manual pick then separator for French", EXPECTED_RESULT,
406                mTextView.getText().toString());
407    }
408
409    public void testWordThenSpaceThenPunctuationFromStripTwiceForFrench() {
410        final String WORD_TO_TYPE = "test ";
411        final String PUNCTUATION_FROM_STRIP = "!";
412        final String EXPECTED_RESULT = "test !!";
413        changeLanguage("fr");
414        type(WORD_TO_TYPE);
415        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
416        runMessages();
417        assertTrue("type word then type space should display punctuation strip",
418                mLatinIME.isShowingPunctuationList());
419        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
420        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
421        assertEquals("type word then type space then punctuation from strip twice for French",
422                EXPECTED_RESULT, mTextView.getText().toString());
423    }
424
425    public void testWordThenSpaceThenPunctuationFromKeyboardTwice() {
426        final String WORD_TO_TYPE = "this !!";
427        final String EXPECTED_RESULT = "this !!";
428        type(WORD_TO_TYPE);
429        assertEquals("manual pick then space then punctuation from keyboard twice", EXPECTED_RESULT,
430                mTextView.getText().toString());
431    }
432
433    public void testManualPickThenPunctuationFromStripTwiceThenType() {
434        final String WORD1_TO_TYPE = "this";
435        final String WORD2_TO_TYPE = "is";
436        final String PUNCTUATION_FROM_STRIP = "!";
437        final String EXPECTED_RESULT = "this!! is";
438        type(WORD1_TO_TYPE);
439        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
440        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
441        mLatinIME.pickSuggestionManually(0, PUNCTUATION_FROM_STRIP);
442        type(WORD2_TO_TYPE);
443        assertEquals("pick word then pick punctuation twice then type", EXPECTED_RESULT,
444                mTextView.getText().toString());
445    }
446
447    public void testManualPickThenSpaceThenType() {
448        final String WORD1_TO_TYPE = "this";
449        final String WORD2_TO_TYPE = " is";
450        final String EXPECTED_RESULT = "this is";
451        type(WORD1_TO_TYPE);
452        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
453        type(WORD2_TO_TYPE);
454        assertEquals("manual pick then space then type", EXPECTED_RESULT,
455                mTextView.getText().toString());
456    }
457
458    public void testManualPickThenManualPick() {
459        final String WORD1_TO_TYPE = "this";
460        final String WORD2_TO_PICK = "is";
461        final String EXPECTED_RESULT = "this is";
462        type(WORD1_TO_TYPE);
463        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
464        // Here we fake picking a word through bigram prediction. This test is taking
465        // advantage of the fact that Latin IME blindly trusts the caller of #pickSuggestionManually
466        // to actually pass the right string.
467        mLatinIME.pickSuggestionManually(1, WORD2_TO_PICK);
468        assertEquals("manual pick then manual pick", EXPECTED_RESULT,
469                mTextView.getText().toString());
470    }
471
472    public void testManualPickThenManualPickWithPunctAtStart() {
473        final String WORD1_TO_TYPE = "this";
474        final String WORD2_TO_PICK = "!is";
475        final String EXPECTED_RESULT = "this!is";
476        type(WORD1_TO_TYPE);
477        mLatinIME.pickSuggestionManually(0, WORD1_TO_TYPE);
478        mLatinIME.pickSuggestionManually(1, WORD2_TO_PICK);
479        assertEquals("manual pick then manual pick a word with punct at start", EXPECTED_RESULT,
480                mTextView.getText().toString());
481    }
482
483    public void testDeleteWholeComposingWord() {
484        final String WORD_TO_TYPE = "this";
485        type(WORD_TO_TYPE);
486        for (int i = 0; i < WORD_TO_TYPE.length(); ++i) {
487            type(Keyboard.CODE_DELETE);
488        }
489        assertEquals("delete whole composing word", "", mTextView.getText().toString());
490    }
491
492    public void testManuallyPickedWordThenColon() {
493        final String WORD_TO_TYPE = "this";
494        final String PUNCTUATION = ":";
495        final String EXPECTED_RESULT = "this:";
496        type(WORD_TO_TYPE);
497        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
498        type(PUNCTUATION);
499        assertEquals("manually pick word then colon",
500                EXPECTED_RESULT, mTextView.getText().toString());
501    }
502
503    public void testManuallyPickedWordThenOpenParen() {
504        final String WORD_TO_TYPE = "this";
505        final String PUNCTUATION = "(";
506        final String EXPECTED_RESULT = "this (";
507        type(WORD_TO_TYPE);
508        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
509        type(PUNCTUATION);
510        assertEquals("manually pick word then open paren",
511                EXPECTED_RESULT, mTextView.getText().toString());
512    }
513
514    public void testManuallyPickedWordThenCloseParen() {
515        final String WORD_TO_TYPE = "this";
516        final String PUNCTUATION = ")";
517        final String EXPECTED_RESULT = "this)";
518        type(WORD_TO_TYPE);
519        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
520        type(PUNCTUATION);
521        assertEquals("manually pick word then close paren",
522                EXPECTED_RESULT, mTextView.getText().toString());
523    }
524
525    public void testManuallyPickedWordThenSmiley() {
526        final String WORD_TO_TYPE = "this";
527        final String SPECIAL_KEY = ":-)";
528        final String EXPECTED_RESULT = "this :-)";
529        type(WORD_TO_TYPE);
530        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
531        mLatinIME.onTextInput(SPECIAL_KEY);
532        assertEquals("manually pick word then press the smiley key",
533                EXPECTED_RESULT, mTextView.getText().toString());
534    }
535
536    public void testManuallyPickedWordThenDotCom() {
537        final String WORD_TO_TYPE = "this";
538        final String SPECIAL_KEY = ".com";
539        final String EXPECTED_RESULT = "this.com";
540        type(WORD_TO_TYPE);
541        mLatinIME.pickSuggestionManually(0, WORD_TO_TYPE);
542        mLatinIME.onTextInput(SPECIAL_KEY);
543        assertEquals("manually pick word then press the .com key",
544                EXPECTED_RESULT, mTextView.getText().toString());
545    }
546
547    public void testTypeWordTypeDotThenPressDotCom() {
548        final String WORD_TO_TYPE = "this.";
549        final String SPECIAL_KEY = ".com";
550        final String EXPECTED_RESULT = "this.com";
551        type(WORD_TO_TYPE);
552        mLatinIME.onTextInput(SPECIAL_KEY);
553        assertEquals("type word type dot then press the .com key",
554                EXPECTED_RESULT, mTextView.getText().toString());
555    }
556
557    public void testAutoCorrectionWithSingleQuoteInside() {
558        final String WORD_TO_TYPE = "you'f ";
559        final String EXPECTED_RESULT = "you'd ";
560        type(WORD_TO_TYPE);
561        assertEquals("auto-correction with single quote inside",
562                EXPECTED_RESULT, mTextView.getText().toString());
563    }
564
565    public void testAutoCorrectionWithSingleQuotesAround() {
566        final String WORD_TO_TYPE = "'tgis' ";
567        final String EXPECTED_RESULT = "'this' ";
568        type(WORD_TO_TYPE);
569        assertEquals("auto-correction with single quotes around",
570                EXPECTED_RESULT, mTextView.getText().toString());
571    }
572
573    // A helper class to ease span tests
574    private static class Span {
575        final SpannableStringBuilder mInputText;
576        final SuggestionSpan mSpan;
577        final int mStart;
578        final int mEnd;
579        // The supplied CharSequence should be an instance of SpannableStringBuilder,
580        // and it should contain exactly zero or one SuggestionSpan. Otherwise, an exception
581        // is thrown.
582        public Span(final CharSequence inputText) {
583            mInputText = (SpannableStringBuilder)inputText;
584            final SuggestionSpan[] spans =
585                    mInputText.getSpans(0, mInputText.length(), SuggestionSpan.class);
586            if (0 == spans.length) {
587                mSpan = null;
588                mStart = -1;
589                mEnd = -1;
590            } else if (1 == spans.length) {
591                mSpan = spans[0];
592                mStart = mInputText.getSpanStart(mSpan);
593                mEnd = mInputText.getSpanEnd(mSpan);
594            } else {
595                throw new RuntimeException("Expected one SuggestionSpan, found " + spans.length);
596            }
597        }
598        public boolean isAutoCorrectionIndicator() {
599            return 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & mSpan.getFlags());
600        }
601    }
602
603    static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200; // The message is posted with a 100 ms delay
604    public void testBlueUnderline() {
605        final String STRING_TO_TYPE = "tgis";
606        final int EXPECTED_SPAN_START = 0;
607        final int EXPECTED_SPAN_END = 4;
608        type(STRING_TO_TYPE);
609        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
610        runMessages();
611        final Span span = new Span(mTextView.getText());
612        assertEquals("show blue underline, span start", EXPECTED_SPAN_START, span.mStart);
613        assertEquals("show blue underline, span end", EXPECTED_SPAN_END, span.mEnd);
614        assertEquals("show blue underline, span color", true, span.isAutoCorrectionIndicator());
615    }
616
617    public void testBlueUnderlineDisappears() {
618        final String STRING_1_TO_TYPE = "tgis";
619        final String STRING_2_TO_TYPE = "q";
620        final int EXPECTED_SPAN_START = 0;
621        final int EXPECTED_SPAN_END = 5;
622        type(STRING_1_TO_TYPE);
623        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
624        runMessages();
625        type(STRING_2_TO_TYPE);
626        // We haven't have time to look into the dictionary yet, so the line should still be
627        // blue to avoid any flicker.
628        final Span spanBefore = new Span(mTextView.getText());
629        assertEquals("extend blue underline, span start", EXPECTED_SPAN_START, spanBefore.mStart);
630        assertEquals("extend blue underline, span end", EXPECTED_SPAN_END, spanBefore.mEnd);
631        assertEquals("extend blue underline, span color", true,
632                spanBefore.isAutoCorrectionIndicator());
633        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
634        runMessages();
635        // Now we have been able to re-evaluate the word, there shouldn't be an auto-correction span
636        final Span spanAfter = new Span(mTextView.getText());
637        assertNull("hide blue underline", spanAfter.mSpan);
638    }
639
640    public void testBlueUnderlineOnBackspace() {
641        final String STRING_TO_TYPE = "tgis";
642        final int EXPECTED_SPAN_START = 0;
643        final int EXPECTED_SPAN_END = 4;
644        type(STRING_TO_TYPE);
645        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
646        runMessages();
647        type(Keyboard.CODE_SPACE);
648        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
649        runMessages();
650        type(Keyboard.CODE_DELETE);
651        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
652        runMessages();
653        type(Keyboard.CODE_DELETE);
654        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
655        runMessages();
656        final Span span = new Span(mTextView.getText());
657        assertEquals("show blue underline after backspace, span start",
658                EXPECTED_SPAN_START, span.mStart);
659        assertEquals("show blue underline after backspace, span end",
660                EXPECTED_SPAN_END, span.mEnd);
661        assertEquals("show blue underline after backspace, span color", true,
662                span.isAutoCorrectionIndicator());
663    }
664
665    public void testBlueUnderlineDisappearsWhenCursorMoved() {
666        final String STRING_TO_TYPE = "tgis";
667        final int NEW_CURSOR_POSITION = 0;
668        type(STRING_TO_TYPE);
669        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
670        // Simulate the onUpdateSelection() event
671        mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1);
672        runMessages();
673        // Here the blue underline has been set. testBlueUnderline() is testing for this already,
674        // so let's not test it here again.
675        // Now simulate the user moving the cursor.
676        mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION);
677        mLatinIME.onUpdateSelection(0, 0, NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1);
678        sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
679        runMessages();
680        final Span span = new Span(mTextView.getText());
681        assertNull("blue underline removed when cursor is moved", span.mSpan);
682    }
683    // TODO: Add some tests for non-BMP characters
684}
685