Upstream version 7.35.139.0
[platform/framework/web/crosswalk.git] / src / content / public / android / javatests / src / org / chromium / content / browser / input / SelectionHandleTest.java
1 // Copyright 2013 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
5 package org.chromium.content.browser.input;
6
7 import android.graphics.Point;
8 import android.graphics.Rect;
9 import android.os.SystemClock;
10 import android.test.FlakyTest;
11 import android.test.suitebuilder.annotation.MediumTest;
12 import android.text.Editable;
13 import android.text.Selection;
14 import android.view.MotionEvent;
15
16 import org.chromium.base.ThreadUtils;
17 import org.chromium.base.test.util.Feature;
18 import org.chromium.base.test.util.UrlUtils;
19 import org.chromium.content.browser.ContentView;
20 import org.chromium.content.browser.RenderCoordinates;
21 import org.chromium.content.browser.test.util.Criteria;
22 import org.chromium.content.browser.test.util.CriteriaHelper;
23 import org.chromium.content.browser.test.util.DOMUtils;
24 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
25 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
26 import org.chromium.content.browser.test.util.TestTouchUtils;
27 import org.chromium.content.browser.test.util.TouchCommon;
28 import org.chromium.content_shell_apk.ContentShellTestBase;
29
30 import java.util.concurrent.Callable;
31
32 public class SelectionHandleTest extends ContentShellTestBase {
33     private static final String META_DISABLE_ZOOM =
34         "<meta name=\"viewport\" content=\"" +
35         "height=device-height," +
36         "width=device-width," +
37         "initial-scale=1.0," +
38         "minimum-scale=1.0," +
39         "maximum-scale=1.0," +
40         "\" />";
41
42     // For these we use a tiny font-size so that we can be more strict on the expected handle
43     // positions.
44     private static final String TEXTAREA_ID = "textarea";
45     private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
46             "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
47             "<textarea id=\"" + TEXTAREA_ID +
48             "\" cols=\"40\" rows=\"20\" style=\"font-size:6px\">" +
49             "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " +
50             "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " +
51             "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " +
52             "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " +
53             "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " +
54             "o f c a e e u t o l t n m d s l b r m." +
55             "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " +
56             "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " +
57             "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " +
58             "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " +
59             "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " +
60             "o f c a e e u t o l t n m d s l b r m." +
61             "</textarea>" +
62             "</body></html>");
63
64     private static final String NONEDITABLE_DIV_ID = "noneditable";
65     private static final String NONEDITABLE_DATA_URL = UrlUtils.encodeHtmlDataUri(
66             "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
67             "<div id=\"" + NONEDITABLE_DIV_ID + "\" style=\"width:200; font-size:6px\">" +
68             "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " +
69             "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " +
70             "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " +
71             "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " +
72             "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " +
73             "o f c a e e u t o l t n m d s l b r m." +
74             "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " +
75             "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " +
76             "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " +
77             "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " +
78             "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " +
79             "o f c a e e u t o l t n m d s l b r m." +
80             "</div>" +
81             "</body></html>");
82
83     // TODO(cjhopman): These tolerances should be based on the actual width/height of a
84     // character/line.
85     private static final int HANDLE_POSITION_X_TOLERANCE_PIX = 20;
86     private static final int HANDLE_POSITION_Y_TOLERANCE_PIX = 30;
87
88     private enum TestPageType {
89         EDITABLE(TEXTAREA_ID, TEXTAREA_DATA_URL, true),
90         NONEDITABLE(NONEDITABLE_DIV_ID, NONEDITABLE_DATA_URL, false);
91
92         final String nodeId;
93         final String dataUrl;
94         final boolean selectionShouldBeEditable;
95
96         TestPageType(String nodeId, String dataUrl, boolean selectionShouldBeEditable) {
97             this.nodeId = nodeId;
98             this.dataUrl = dataUrl;
99             this.selectionShouldBeEditable = selectionShouldBeEditable;
100         }
101     }
102
103     private void launchWithUrl(String url) throws Throwable {
104         launchContentShellWithUrl(url);
105         assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
106         assertWaitForPageScaleFactorMatch(1.0f);
107
108         // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
109         // brought up.
110         getImeAdapter().setInputMethodManagerWrapper(
111                 new TestInputMethodManagerWrapper(getContentViewCore()));
112     }
113
114     private void assertWaitForHasSelectionPosition()
115             throws Throwable {
116         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
117             @Override
118             public boolean isSatisfied() {
119                 int start = getSelectionStart();
120                 int end = getSelectionEnd();
121                 return start > 0 && start == end;
122             }
123         }));
124     }
125
126     /**
127      * Verifies that when a long-press is performed on static page text,
128      * selection handles appear and that handles can be dragged to extend the
129      * selection. Does not check exact handle position as this will depend on
130      * screen size; instead, position is expected to be correct within
131      * HANDLE_POSITION_TOLERANCE_PIX.
132      *
133      * Test is flaky: crbug.com/290375
134      * @MediumTest
135      * @Feature({ "TextSelection", "Main" })
136      */
137     @FlakyTest
138     public void testNoneditableSelectionHandles() throws Throwable {
139         doSelectionHandleTest(TestPageType.NONEDITABLE);
140     }
141
142     /**
143      * Verifies that when a long-press is performed on editable text (within a
144      * textarea), selection handles appear and that handles can be dragged to
145      * extend the selection. Does not check exact handle position as this will
146      * depend on screen size; instead, position is expected to be correct within
147      * HANDLE_POSITION_TOLERANCE_PIX.
148      */
149     @MediumTest
150     @Feature({ "TextSelection" })
151     public void testEditableSelectionHandles() throws Throwable {
152         doSelectionHandleTest(TestPageType.EDITABLE);
153     }
154
155     private void doSelectionHandleTest(TestPageType pageType) throws Throwable {
156         launchWithUrl(pageType.dataUrl);
157
158         clickNodeToShowSelectionHandles(pageType.nodeId);
159         assertWaitForSelectionEditableEquals(pageType.selectionShouldBeEditable);
160
161         HandleView startHandle = getStartHandle();
162         HandleView endHandle = getEndHandle();
163
164         Rect nodeWindowBounds = getNodeBoundsPix(pageType.nodeId);
165
166         int leftX = (nodeWindowBounds.left + nodeWindowBounds.centerX()) / 2;
167         int centerX = nodeWindowBounds.centerX();
168         int rightX = (nodeWindowBounds.right + nodeWindowBounds.centerX()) / 2;
169
170         int topY = (nodeWindowBounds.top + nodeWindowBounds.centerY()) / 2;
171         int centerY = nodeWindowBounds.centerY();
172         int bottomY = (nodeWindowBounds.bottom + nodeWindowBounds.centerY()) / 2;
173
174         // Drag start handle up and to the left. The selection start should decrease.
175         dragHandleAndCheckSelectionChange(startHandle, leftX, topY, -1, 0);
176         // Drag end handle down and to the right. The selection end should increase.
177         dragHandleAndCheckSelectionChange(endHandle, rightX, bottomY, 0, 1);
178         // Drag start handle back to the middle. The selection start should increase.
179         dragHandleAndCheckSelectionChange(startHandle, centerX, centerY, 1, 0);
180         // Drag end handle up and to the left past the start handle. Both selection start and end
181         // should decrease.
182         dragHandleAndCheckSelectionChange(endHandle, leftX, topY, -1, -1);
183         // Drag start handle down and to the right past the end handle. Both selection start and end
184         // should increase.
185         dragHandleAndCheckSelectionChange(startHandle, rightX, bottomY, 1, 1);
186
187         clickToDismissHandles();
188     }
189
190     private void dragHandleAndCheckSelectionChange(HandleView handle, int dragToX, int dragToY,
191             final int expectedStartChange, final int expectedEndChange) throws Throwable {
192         String initialText = getContentViewCore().getSelectedText();
193         final int initialSelectionEnd = getSelectionEnd();
194         final int initialSelectionStart = getSelectionStart();
195
196         dragHandleTo(handle, dragToX, dragToY, 10);
197         assertWaitForEitherHandleNear(dragToX, dragToY);
198
199         if (getContentViewCore().isSelectionEditable()) {
200             assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
201                 @Override
202                 public boolean isSatisfied() {
203                     int startChange = getSelectionStart() - initialSelectionStart;
204                     // TODO(cjhopman): Due to http://crbug.com/244633 we can't really assert that
205                     // there is no change when we expect to be able to.
206                     if (expectedStartChange != 0) {
207                         if ((int) Math.signum(startChange) != expectedStartChange) return false;
208                     }
209
210                     int endChange = getSelectionEnd() - initialSelectionEnd;
211                     if (expectedEndChange != 0) {
212                         if ((int) Math.signum(endChange) != expectedEndChange) return false;
213                     }
214
215                     return true;
216                 }
217             }));
218         }
219
220         assertWaitForHandleViewStopped(getStartHandle());
221         assertWaitForHandleViewStopped(getEndHandle());
222     }
223
224     private void assertWaitForSelectionEditableEquals(final boolean expected) throws Throwable {
225         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
226             @Override
227             public boolean isSatisfied() {
228                 return getContentViewCore().isSelectionEditable() == expected;
229             }
230         }));
231     }
232
233     private void assertWaitForHandleViewStopped(final HandleView handle) throws Throwable {
234         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
235             private Point position = new Point(-1, -1);
236             @Override
237             public boolean isSatisfied() {
238                 Point lastPosition = position;
239                 position = getHandlePosition(handle);
240                 return !handle.isDragging() &&
241                         position.equals(lastPosition);
242             }
243         }));
244     }
245
246     /**
247      * Verifies that when a selection is made within static page text, that the
248      * contextual action bar of the correct type is displayed. Also verified
249      * that the bar disappears upon deselection.
250      */
251     @MediumTest
252     @Feature({ "TextSelection" })
253     public void testNoneditableSelectionActionBar() throws Throwable {
254         doSelectionActionBarTest(TestPageType.NONEDITABLE);
255     }
256
257     /**
258      * Verifies that when a selection is made within editable text, that the
259      * contextual action bar of the correct type is displayed. Also verified
260      * that the bar disappears upon deselection.
261      */
262     @MediumTest
263     @Feature({ "TextSelection" })
264     public void testEditableSelectionActionBar() throws Throwable {
265         doSelectionActionBarTest(TestPageType.EDITABLE);
266     }
267
268     private void doSelectionActionBarTest(TestPageType pageType) throws Throwable {
269         launchWithUrl(pageType.dataUrl);
270         assertFalse(getContentViewCore().isSelectActionBarShowing());
271         clickNodeToShowSelectionHandles(pageType.nodeId);
272         assertWaitForSelectActionBarShowingEquals(true);
273         clickToDismissHandles();
274         assertWaitForSelectActionBarShowingEquals(false);
275     }
276
277     private static Point getHandlePosition(final HandleView handle) {
278         return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Point>() {
279             @Override
280             public Point call() {
281                 return new Point(handle.getAdjustedPositionX(), handle.getAdjustedPositionY());
282             }
283         });
284     }
285
286     private static boolean isHandleNear(HandleView handle, int x, int y) {
287         Point position = getHandlePosition(handle);
288         return (Math.abs(position.x - x) < HANDLE_POSITION_X_TOLERANCE_PIX) &&
289                 (Math.abs(position.y - y) < HANDLE_POSITION_Y_TOLERANCE_PIX);
290     }
291
292     private void assertWaitForHandleNear(final HandleView handle, final int x, final int y)
293             throws Throwable {
294         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
295             @Override
296             public boolean isSatisfied() {
297                 return isHandleNear(handle, x, y);
298             }
299         }));
300     }
301
302     private void assertWaitForEitherHandleNear(final int x, final int y) throws Throwable {
303         final HandleView startHandle = getStartHandle();
304         final HandleView endHandle = getEndHandle();
305         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
306             @Override
307             public boolean isSatisfied() {
308                 return isHandleNear(startHandle, x, y) || isHandleNear(endHandle, x, y);
309             }
310         }));
311     }
312
313     private void assertWaitForHandlesShowingEquals(final boolean shouldBeShowing) throws Throwable {
314         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
315             @Override
316             public boolean isSatisfied() {
317                 SelectionHandleController shc =
318                         getContentViewCore().getSelectionHandleControllerForTest();
319                 boolean isShowing = shc != null && shc.isShowing();
320                 return shouldBeShowing == isShowing;
321             }
322         }));
323     }
324
325
326     private void dragHandleTo(final HandleView handle, final int dragToX, final int dragToY,
327             final int steps) throws Throwable {
328         ContentView view = getContentView();
329         assertTrue(ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
330             @Override
331             public Boolean call() {
332                 int adjustedX = handle.getAdjustedPositionX();
333                 int adjustedY = handle.getAdjustedPositionY();
334                 int realX = handle.getPositionX();
335                 int realY = handle.getPositionY();
336
337                 int realDragToX = dragToX + (realX - adjustedX);
338                 int realDragToY = dragToY + (realY - adjustedY);
339
340                 ContentView view = getContentView();
341                 int[] fromLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
342                         view, realX, realY);
343                 int[] toLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
344                         view, realDragToX, realDragToY);
345
346                 long downTime = SystemClock.uptimeMillis();
347                 MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
348                         fromLocation[0], fromLocation[1], 0);
349                 handle.dispatchTouchEvent(event);
350
351                 if (!handle.isDragging()) return false;
352
353                 for (int i = 0; i < steps; i++) {
354                     float scale = (float) (i + 1) / steps;
355                     int x = fromLocation[0] + (int) (scale * (toLocation[0] - fromLocation[0]));
356                     int y = fromLocation[1] + (int) (scale * (toLocation[1] - fromLocation[1]));
357                     long eventTime = SystemClock.uptimeMillis();
358                     event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE,
359                             x, y, 0);
360                     handle.dispatchTouchEvent(event);
361                 }
362                 long upTime = SystemClock.uptimeMillis();
363                 event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP,
364                         toLocation[0], toLocation[1], 0);
365                 handle.dispatchTouchEvent(event);
366
367                 return !handle.isDragging();
368             }
369         }));
370     }
371
372     private Rect getNodeBoundsPix(String nodeId) throws Throwable {
373         Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
374                 new TestCallbackHelperContainer(getContentView()), nodeId);
375
376         RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
377         int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
378         int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
379
380         int left = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
381         int right = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
382         int top = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
383         int bottom = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
384
385         return new Rect(left, top, right, bottom);
386     }
387
388     private void clickNodeToShowSelectionHandles(String nodeId) throws Throwable {
389         Rect nodeWindowBounds = getNodeBoundsPix(nodeId);
390
391         TouchCommon touchCommon = new TouchCommon(this);
392         int centerX = nodeWindowBounds.centerX();
393         int centerY = nodeWindowBounds.centerY();
394         touchCommon.longPressView(getContentView(), centerX, centerY);
395
396         assertWaitForHandlesShowingEquals(true);
397         assertWaitForHandleViewStopped(getStartHandle());
398
399         // No words wrap in the sample text so handles should be at the same y
400         // position.
401         assertEquals(getStartHandle().getPositionY(), getEndHandle().getPositionY());
402     }
403
404     private void clickToDismissHandles() throws Throwable {
405         TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
406         new TouchCommon(this).singleClickView(getContentView(), 0, 0);
407         assertWaitForHandlesShowingEquals(false);
408     }
409
410     private void assertWaitForSelectActionBarShowingEquals(final boolean shouldBeShowing)
411             throws InterruptedException {
412         assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
413             @Override
414             public boolean isSatisfied() {
415                 return shouldBeShowing == getContentViewCore().isSelectActionBarShowing();
416             }
417         }));
418     }
419
420     public void assertWaitForHasInputConnection() {
421         try {
422             assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
423                 @Override
424                 public boolean isSatisfied() {
425                     return getContentViewCore().getInputConnectionForTest() != null;
426                 }
427             }));
428         } catch (InterruptedException e) {
429             fail();
430         }
431     }
432
433     private ImeAdapter getImeAdapter() {
434         return getContentViewCore().getImeAdapterForTest();
435     }
436
437     private int getSelectionStart() {
438         return Selection.getSelectionStart(getEditable());
439     }
440
441     private int getSelectionEnd() {
442         return Selection.getSelectionEnd(getEditable());
443     }
444
445     private Editable getEditable() {
446         // We have to wait for the input connection (with the IME) to be created before accessing
447         // the ContentViewCore's editable.
448         assertWaitForHasInputConnection();
449         return getContentViewCore().getEditableForTest();
450     }
451
452     private HandleView getStartHandle() {
453         SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
454         return shc.getStartHandleViewForTest();
455     }
456
457     private HandleView getEndHandle() {
458         SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
459         return shc.getEndHandleViewForTest();
460     }
461 }