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