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