1// Copyright (c) 2012 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
5package org.chromium.content.browser.input;
6
7import android.content.ClipData;
8import android.content.ClipboardManager;
9import android.content.Context;
10import android.graphics.Rect;
11import android.test.suitebuilder.annotation.MediumTest;
12import android.text.Editable;
13import android.text.Selection;
14import android.view.KeyEvent;
15
16import org.chromium.base.test.util.DisabledTest;
17import org.chromium.base.test.util.Feature;
18import org.chromium.base.test.util.UrlUtils;
19import org.chromium.content.browser.ContentView;
20import org.chromium.content.browser.RenderCoordinates;
21import org.chromium.content.browser.test.util.Criteria;
22import org.chromium.content.browser.test.util.CriteriaHelper;
23import org.chromium.content.browser.test.util.DOMUtils;
24import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
25import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
26import org.chromium.content.browser.test.util.TestTouchUtils;
27import org.chromium.content.browser.test.util.TouchCommon;
28import org.chromium.content_shell_apk.ContentShellTestBase;
29
30public class InsertionHandleTest extends ContentShellTestBase {
31    private static final String META_DISABLE_ZOOM =
32        "<meta name=\"viewport\" content=\"" +
33        "height=device-height," +
34        "width=device-width," +
35        "initial-scale=1.0," +
36        "minimum-scale=1.0," +
37        "maximum-scale=1.0," +
38        "\" />";
39
40    private static final String TEXTAREA_ID = "textarea";
41    private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
42            "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
43            "<textarea id=\"" + TEXTAREA_ID + "\" cols=\"20\" rows=\"10\">" +
44            "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " +
45            "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " +
46            "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute " +
47            "irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " +
48            "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
49            "officia deserunt mollit anim id est laborum." +
50            "</textarea>" +
51            "</body></html>");
52
53    private static final String INPUT_TEXT_ID = "input_text";
54    private static final String INPUT_TEXT_DATA_URL = UrlUtils.encodeHtmlDataUri(
55            "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
56            "<input id=\"input_text\" type=\"text\" value=\"" +
57            "T0D0(cjhopman): put amusing sample text here. Make sure it is at least " +
58            "100 characters.  123456789012345678901234567890\" size=20></input>" +
59            "</body></html>");
60
61    // Offset to compensate for the fact that the handle is below the text.
62    private static final int VERTICAL_OFFSET = 10;
63    private static final int HANDLE_POSITION_TOLERANCE = 20;
64    private static final String PASTE_TEXT = "**test text to paste**";
65
66
67    public void launchWithUrl(String url) throws Throwable {
68        launchContentShellWithUrl(url);
69        assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
70        assertWaitForPageScaleFactorMatch(1.0f);
71
72        // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
73        // brought up.
74        getImeAdapter().setInputMethodManagerWrapper(
75                new TestInputMethodManagerWrapper(getContentViewCore()));
76    }
77
78    @MediumTest
79    @Feature({"TextSelection", "TextInput", "Main"})
80    public void testUnselectHidesHandle() throws Throwable {
81        launchWithUrl(TEXTAREA_DATA_URL);
82        clickNodeToShowInsertionHandle(TEXTAREA_ID);
83
84        // Unselecting should cause the handle to disappear.
85        getImeAdapter().unselect();
86        assertTrue(waitForHandleShowingEquals(false));
87    }
88
89
90    @MediumTest
91    @Feature({"TextSelection", "TextInput", "Main"})
92    public void testKeyEventHidesHandle() throws Throwable {
93        launchWithUrl(TEXTAREA_DATA_URL);
94        clickNodeToShowInsertionHandle(TEXTAREA_ID);
95
96        getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X));
97        getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_X));
98        assertTrue(waitForHandleShowingEquals(false));
99    }
100
101    /**
102     * @MediumTest
103     * @Feature({"TextSelection", "TextInput", "Main"})
104     * http://crbug.com/169648
105     */
106    @DisabledTest
107    public void testDragInsertionHandle() throws Throwable {
108        launchWithUrl(TEXTAREA_DATA_URL);
109
110        clickNodeToShowInsertionHandle(TEXTAREA_ID);
111
112        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
113        HandleView handle = ihc.getHandleViewForTest();
114
115        int initialX = handle.getPositionX();
116        int initialY = handle.getPositionY();
117        int dragToX = initialX + 120;
118        int dragToY = initialY + 120;
119
120        dragHandleTo(dragToX, dragToY);
121        assertWaitForHandleNear(dragToX, dragToY);
122    }
123
124
125    @MediumTest
126    @Feature({"TextSelection", "TextInput", "Main"})
127    public void testPasteAtInsertionHandle() throws Throwable {
128        launchWithUrl(TEXTAREA_DATA_URL);
129
130        clickNodeToShowInsertionHandle(TEXTAREA_ID);
131
132        int offset = getSelectionStart();
133        String initialText = getEditableText();
134
135        saveToClipboard(PASTE_TEXT);
136        pasteOnMainSync();
137
138        String expectedText =
139               initialText.substring(0, offset) + PASTE_TEXT + initialText.substring(offset);
140        assertTrue(waitForEditableTextEquals(expectedText));
141        assertTrue(waitForHandleShowingEquals(false));
142    }
143
144    /**
145     * @MediumTest
146     * @Feature({"TextSelection", "TextInput", "Main"})
147     * http://crbug.com/169648
148     */
149    @DisabledTest
150    public void testDragInsertionHandleInputText() throws Throwable {
151        launchWithUrl(INPUT_TEXT_DATA_URL);
152
153        clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
154
155        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
156        HandleView handle = ihc.getHandleViewForTest();
157
158        int initialX = handle.getPositionX();
159        int initialY = handle.getPositionY();
160        int dragToX = initialX + 120;
161        int dragToY = initialY;
162        dragHandleTo(dragToX, dragToY);
163        assertWaitForHandleNear(dragToX, initialY);
164
165        TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
166
167        initialX = handle.getPositionX();
168        initialY = handle.getPositionY();
169        dragToY = initialY + 120;
170        dragHandleTo(initialX, dragToY);
171        // Vertical drag should not change the y-position.
172        assertWaitForHandleNear(initialX, initialY);
173    }
174
175    /**
176     * @MediumTest
177     * @Feature({"TextSelection", "TextInput", "Main"})
178     * http://crbug.com/169648
179     */
180    @DisabledTest
181    public void testDragInsertionHandleInputTextOutsideBounds() throws Throwable {
182        launchWithUrl(INPUT_TEXT_DATA_URL);
183
184        clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
185
186        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
187        HandleView handle = ihc.getHandleViewForTest();
188
189        int initialX = handle.getPositionX();
190        int initialY = handle.getPositionY();
191        int dragToX = initialX;
192        int dragToY = initialY + 150;
193
194        // A vertical drag should not move the insertion handle.
195        dragHandleTo(dragToX, dragToY);
196        assertWaitForHandleNear(initialX, initialY);
197
198        // The input box does not go to the edge of the screen, and neither should the insertion
199        // handle.
200        dragToX = getContentView().getWidth();
201        dragHandleTo(dragToX, dragToY);
202        assertTrue(handle.getPositionX() < dragToX - 100);
203    }
204
205    @Override
206    protected void tearDown() throws Exception {
207        super.tearDown();
208        // No way to just clear clipboard, so setting to empty string instead.
209        saveToClipboard("");
210    }
211
212    private void clickNodeToShowInsertionHandle(String nodeId) throws Throwable {
213        // On the first click the keyboard will be displayed but no insertion handles. On the second
214        // click (only if it changes the selection), the insertion handle is displayed. So that the
215        // second click changes the selection, the two clicks should be in sufficiently different
216        // locations.
217        Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
218                new TestCallbackHelperContainer(getContentView()), nodeId);
219
220        RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
221        int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
222        int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
223        float left = renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
224        float right = renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
225        float top = renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
226        float bottom = renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
227
228        TouchCommon touchCommon = new TouchCommon(this);
229        touchCommon.singleClickView(getContentView(),
230                (int)(left + 3 * (right - left) / 4), (int)(top + (bottom - top) / 2));
231
232
233        TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
234        assertTrue(waitForHasSelectionPosition());
235
236        // TODO(cjhopman): Wait for keyboard display finished?
237        touchCommon.singleClickView(getContentView(),
238                (int)(left + (right - left) / 4), (int)(top + (bottom - top) / 2));
239        assertTrue(waitForHandleShowingEquals(true));
240        assertTrue(waitForHandleViewStopped());
241    }
242
243    private boolean waitForHandleViewStopped() throws Throwable {
244        // If the polling interval is too short, slowly moving may be detected as not moving.
245        final int POLLING_INTERVAL = 200;
246        return CriteriaHelper.pollForCriteria(new Criteria() {
247            int mPositionX = -1;
248            int mPositionY = -1;
249            @Override
250            public boolean isSatisfied() {
251                int lastPositionX = mPositionX;
252                int lastPositionY = mPositionY;
253                InsertionHandleController ihc =
254                        getContentViewCore().getInsertionHandleControllerForTest();
255                HandleView handle = ihc.getHandleViewForTest();
256                mPositionX = handle.getPositionX();
257                mPositionY = handle.getPositionY();
258                return !handle.isDragging() &&
259                        mPositionX == lastPositionX && mPositionY == lastPositionY;
260            }
261        }, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL, POLLING_INTERVAL);
262    }
263
264    private boolean waitForEditableTextEquals(final String expectedText)
265            throws Throwable {
266        return CriteriaHelper.pollForCriteria(new Criteria() {
267            @Override
268            public boolean isSatisfied() {
269                return getEditableText().trim().equals(expectedText.trim());
270            }
271        });
272    }
273
274    private boolean waitForHasSelectionPosition()
275            throws Throwable {
276        return CriteriaHelper.pollForCriteria(new Criteria() {
277            @Override
278            public boolean isSatisfied() {
279                int start = getSelectionStart();
280                int end = getSelectionEnd();
281                return start > 0 && start == end;
282            }
283        });
284    }
285
286    private void dragHandleTo(int dragToX, int dragToY, int steps) throws Throwable {
287        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
288        HandleView handle = ihc.getHandleViewForTest();
289        int initialX = handle.getPositionX();
290        int initialY = handle.getPositionY();
291        ContentView view = getContentView();
292
293        int fromLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, initialX,
294                initialY + VERTICAL_OFFSET);
295        int toLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, dragToX,
296                dragToY + VERTICAL_OFFSET);
297
298        long downTime = TestTouchUtils.dragStart(getInstrumentation(), fromLocation[0],
299                fromLocation[1]);
300        assertWaitForHandleDraggingEquals(true);
301        TestTouchUtils.dragTo(getInstrumentation(), fromLocation[0], toLocation[0],
302                fromLocation[1], toLocation[1], steps, downTime);
303        TestTouchUtils.dragEnd(getInstrumentation(), toLocation[0], toLocation[1], downTime);
304        assertWaitForHandleDraggingEquals(false);
305        assertTrue(waitForHandleViewStopped());
306    }
307
308    private void dragHandleTo(int dragToX, int dragToY) throws Throwable {
309        dragHandleTo(dragToX, dragToY, 5);
310    }
311
312    private void assertWaitForHandleDraggingEquals(final boolean expected) throws Throwable {
313        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
314        final HandleView handle = ihc.getHandleViewForTest();
315        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
316            @Override
317            public boolean isSatisfied() {
318                return handle.isDragging() == expected;
319            }
320        }));
321    }
322
323    private static boolean isHandleNear(HandleView handle, int x, int y) {
324        return (Math.abs(handle.getPositionX() - x) < HANDLE_POSITION_TOLERANCE) &&
325                (Math.abs(handle.getPositionY() - VERTICAL_OFFSET - y) < HANDLE_POSITION_TOLERANCE);
326    }
327
328    private void assertWaitForHandleNear(final int x, final int y) throws Throwable {
329        InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
330        final HandleView handle = ihc.getHandleViewForTest();
331        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
332            @Override
333            public boolean isSatisfied() {
334                return isHandleNear(handle, x, y);
335            }
336        }));
337    }
338
339    private boolean waitForHandleShowingEquals(final boolean shouldBeShowing)
340            throws Throwable {
341        return CriteriaHelper.pollForCriteria(new Criteria() {
342            @Override
343            public boolean isSatisfied() {
344                InsertionHandleController ihc =
345                        getContentViewCore().getInsertionHandleControllerForTest();
346                boolean isShowing = ihc != null && ihc.isShowing();
347                return isShowing == shouldBeShowing;
348            }
349        });
350    }
351
352    private void pasteOnMainSync() {
353        getInstrumentation().runOnMainSync(new Runnable() {
354            @Override
355            public void run() {
356                getContentViewCore().getInsertionHandleControllerForTest().paste();
357            }
358        });
359    }
360
361    private int getSelectionStart() {
362        return Selection.getSelectionStart(getEditable());
363    }
364
365    private int getSelectionEnd() {
366        return Selection.getSelectionEnd(getEditable());
367    }
368
369    private Editable getEditable() {
370        return getContentViewCore().getEditableForTest();
371    }
372
373    private String getEditableText() {
374        return getEditable().toString();
375    }
376
377    private void saveToClipboard(String text) {
378        ClipboardManager clipMgr =
379                (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
380        clipMgr.setPrimaryClip(ClipData.newPlainText(null, text));
381    }
382
383    private ImeAdapter getImeAdapter() {
384        return getContentViewCore().getImeAdapterForTest();
385    }
386}
387