1 // Copyright 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 
5 package org.chromium.content_public.browser.test.util;
6 
7 import android.app.Activity;
8 import android.graphics.Rect;
9 import android.util.JsonReader;
10 import android.view.View;
11 
12 import org.hamcrest.Matchers;
13 import org.junit.Assert;
14 
15 import org.chromium.base.ContextUtils;
16 import org.chromium.base.annotations.JNINamespace;
17 import org.chromium.base.test.util.Criteria;
18 import org.chromium.base.test.util.CriteriaHelper;
19 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
20 import org.chromium.content.browser.RenderCoordinatesImpl;
21 import org.chromium.content.browser.webcontents.WebContentsImpl;
22 import org.chromium.content_public.browser.WebContents;
23 
24 import java.io.IOException;
25 import java.io.StringReader;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29 
30 /**
31  * Collection of DOM-based utilities.
32  */
33 @JNINamespace("content")
34 public class DOMUtils {
35     private static final long MEDIA_TIMEOUT_SECONDS = 10L;
36     private static final long MEDIA_TIMEOUT_MILLISECONDS = MEDIA_TIMEOUT_SECONDS * 1000;
37 
38     /**
39      * Plays the media with given {@code id}.
40      * @param webContents The WebContents in which the media element lives.
41      * @param id The element's id to be played.
42      */
playMedia(final WebContents webContents, final String id)43     public static void playMedia(final WebContents webContents, final String id)
44             throws TimeoutException {
45         StringBuilder sb = new StringBuilder();
46         sb.append("(function() {");
47         sb.append("  var media = document.getElementById('" + id + "');");
48         sb.append("  if (media) media.play();");
49         sb.append("})();");
50         JavaScriptUtils.executeJavaScriptAndWaitForResult(
51                 webContents, sb.toString(), MEDIA_TIMEOUT_SECONDS, TimeUnit.SECONDS);
52     }
53 
54     /**
55      * Pauses the media with given {@code id}
56      * @param webContents The WebContents in which the media element lives.
57      * @param id The element's id to be paused.
58      */
pauseMedia(final WebContents webContents, final String id)59     public static void pauseMedia(final WebContents webContents, final String id)
60             throws TimeoutException {
61         StringBuilder sb = new StringBuilder();
62         sb.append("(function() {");
63         sb.append("  var media = document.getElementById('" + id + "');");
64         sb.append("  if (media) media.pause();");
65         sb.append("})();");
66         JavaScriptUtils.executeJavaScriptAndWaitForResult(
67                 webContents, sb.toString(), MEDIA_TIMEOUT_SECONDS, TimeUnit.SECONDS);
68     }
69 
70     /**
71      * Returns whether the media with given {@code id} is paused.
72      * @param webContents The WebContents in which the media element lives.
73      * @param id The element's id to check.
74      * @return whether the media is paused.
75      */
isMediaPaused(final WebContents webContents, final String id)76     public static boolean isMediaPaused(final WebContents webContents, final String id)
77             throws TimeoutException {
78         return getNodeField("paused", webContents, id, Boolean.class);
79     }
80 
81     /**
82      * Returns whether the media with given {@code id} has ended.
83      * @param webContents The WebContents in which the media element lives.
84      * @param id The element's id to check.
85      * @return whether the media has ended.
86      */
isMediaEnded(final WebContents webContents, final String id)87     public static boolean isMediaEnded(final WebContents webContents, final String id)
88             throws TimeoutException {
89         return getNodeField("ended", webContents, id, Boolean.class);
90     }
91 
92     /**
93      * Returns the current time of the media with given {@code id}.
94      * @param webContents The WebContents in which the media element lives.
95      * @param id The element's id to check.
96      * @return the current time (in seconds) of the media.
97      */
getCurrentTime(final WebContents webContents, final String id)98     private static double getCurrentTime(final WebContents webContents, final String id)
99             throws TimeoutException {
100         return getNodeField("currentTime", webContents, id, Double.class);
101     }
102 
103     /**
104      * Waits until the playback of the media with given {@code id} has started.
105      * @param webContents The WebContents in which the media element lives.
106      * @param id The element's id to check.
107      */
waitForMediaPlay(final WebContents webContents, final String id)108     public static void waitForMediaPlay(final WebContents webContents, final String id) {
109         CriteriaHelper.pollInstrumentationThread(() -> {
110             try {
111                 // Playback can't be reliably detected until current time moves forward.
112                 Criteria.checkThat(DOMUtils.isMediaPaused(webContents, id), Matchers.is(false));
113                 Criteria.checkThat(
114                         DOMUtils.getCurrentTime(webContents, id), Matchers.greaterThan(0d));
115             } catch (TimeoutException e) {
116                 // Intentionally do nothing
117                 throw new CriteriaNotSatisfiedException(e);
118             }
119         }, MEDIA_TIMEOUT_MILLISECONDS, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
120     }
121 
122     /**
123      * Waits until the playback of the media with given {@code id} has paused before ended.
124      * @param webContents The WebContents in which the media element lives.
125      * @param id The element's id to check.
126      */
waitForMediaPauseBeforeEnd(final WebContents webContents, final String id)127     public static void waitForMediaPauseBeforeEnd(final WebContents webContents, final String id) {
128         CriteriaHelper.pollInstrumentationThread(() -> {
129             try {
130                 Criteria.checkThat(DOMUtils.isMediaPaused(webContents, id), Matchers.is(true));
131                 Criteria.checkThat(DOMUtils.isMediaEnded(webContents, id), Matchers.is(false));
132             } catch (TimeoutException e) {
133                 // Intentionally do nothing
134                 throw new CriteriaNotSatisfiedException(e);
135             }
136         });
137     }
138 
139     /**
140      * Returns whether the document is fullscreen.
141      * @param webContents The WebContents to check.
142      * @return Whether the document is fullsscreen.
143      */
isFullscreen(final WebContents webContents)144     public static boolean isFullscreen(final WebContents webContents) throws TimeoutException {
145         StringBuilder sb = new StringBuilder();
146         sb.append("(function() {");
147         sb.append("  return [document.webkitIsFullScreen];");
148         sb.append("})();");
149 
150         String jsonText =
151                 JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
152         return readValue(jsonText, Boolean.class);
153     }
154 
155     /**
156      * Makes the document exit fullscreen.
157      * @param webContents The WebContents to make fullscreen.
158      */
exitFullscreen(final WebContents webContents)159     public static void exitFullscreen(final WebContents webContents) {
160         StringBuilder sb = new StringBuilder();
161         sb.append("(function() {");
162         sb.append("  if (document.webkitExitFullscreen) document.webkitExitFullscreen();");
163         sb.append("})();");
164 
165         JavaScriptUtils.executeJavaScript(webContents, sb.toString());
166     }
167 
getContainerView(final WebContents webContents)168     private static View getContainerView(final WebContents webContents) {
169         return ((WebContentsImpl) webContents).getViewAndroidDelegate().getContainerView();
170     }
171 
getActivity(final WebContents webContents)172     private static Activity getActivity(final WebContents webContents) {
173         return ContextUtils.activityFromContext(((WebContentsImpl) webContents).getContext());
174     }
175 
176     /**
177      * Returns the rect boundaries for a node by its id.
178      * @param webContents The WebContents in which the node lives.
179      * @param nodeId The id of the node.
180      * @return The rect boundaries for the node.
181      */
getNodeBounds(final WebContents webContents, String nodeId)182     public static Rect getNodeBounds(final WebContents webContents, String nodeId)
183             throws TimeoutException {
184         String jsCode = "document.getElementById('" + nodeId + "')";
185         return getNodeBoundsByJs(webContents, jsCode);
186     }
187 
188     /**
189      * Focus a DOM node by its id.
190      * @param webContents The WebContents in which the node lives.
191      * @param nodeId The id of the node.
192      */
focusNode(final WebContents webContents, String nodeId)193     public static void focusNode(final WebContents webContents, String nodeId)
194             throws TimeoutException {
195         StringBuilder sb = new StringBuilder();
196         sb.append("(function() {");
197         sb.append("  var node = document.getElementById('" + nodeId + "');");
198         sb.append("  if (node) node.focus();");
199         sb.append("})();");
200 
201         JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
202     }
203 
204     /**
205      * Get the id of the currently focused node.
206      * @param webContents The WebContents in which the node lives.
207      * @return The id of the currently focused node.
208      */
getFocusedNode(WebContents webContents)209     public static String getFocusedNode(WebContents webContents) throws TimeoutException {
210         StringBuilder sb = new StringBuilder();
211         sb.append("(function() {");
212         sb.append("  var node = document.activeElement;");
213         sb.append("  if (!node) return null;");
214         sb.append("  return node.id;");
215         sb.append("})();");
216 
217         String id = JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
218 
219         // String results from JavaScript includes surrounding quotes.  Remove them.
220         if (id != null && id.length() >= 2 && id.charAt(0) == '"') {
221             id = id.substring(1, id.length() - 1);
222         }
223         return id;
224     }
225 
226     /**
227      * Click a DOM node by its id, scrolling it into view first.
228      * @param webContents The WebContents in which the node lives.
229      * @param nodeId The id of the node.
230      */
clickNode(final WebContents webContents, String nodeId)231     public static boolean clickNode(final WebContents webContents, String nodeId)
232             throws TimeoutException {
233         return clickNode(webContents, nodeId, true /* goThroughRootAndroidView */);
234     }
235 
236     /**
237      * Click a DOM node by its id, scrolling it into view first.
238      * @param webContents The WebContents in which the node lives.
239      * @param nodeId The id of the node.
240      * @param goThroughRootAndroidView Whether the input should be routed through the Root View for
241      *        the CVC.
242      */
clickNode(final WebContents webContents, String nodeId, boolean goThroughRootAndroidView)243     public static boolean clickNode(final WebContents webContents, String nodeId,
244             boolean goThroughRootAndroidView) throws TimeoutException {
245         return clickNode(
246                 webContents, nodeId, goThroughRootAndroidView, true /* shouldScrollIntoView */);
247     }
248 
249     /**
250      * Click a DOM node by its id.
251      * @param webContents The WebContents in which the node lives.
252      * @param nodeId The id of the node.
253      * @param goThroughRootAndroidView Whether the input should be routed through the Root View for
254      *        the CVC.
255      * @param shouldScrollIntoView Whether to scroll the node into view first.
256      */
clickNode(final WebContents webContents, String nodeId, boolean goThroughRootAndroidView, boolean shouldScrollIntoView)257     public static boolean clickNode(final WebContents webContents, String nodeId,
258             boolean goThroughRootAndroidView, boolean shouldScrollIntoView)
259             throws TimeoutException {
260         if (shouldScrollIntoView) scrollNodeIntoView(webContents, nodeId);
261         int[] clickTarget = getClickTargetForNode(webContents, nodeId);
262         if (goThroughRootAndroidView) {
263             return TouchCommon.singleClickView(
264                     getContainerView(webContents), clickTarget[0], clickTarget[1]);
265         } else {
266             // TODO(mthiesse): It should be sufficient to use getContainerView(webContents) here
267             // directly, but content offsets are only updated in the EventForwarder when the
268             // CompositorViewHolder intercepts touch events.
269             View target =
270                     getContainerView(webContents).getRootView().findViewById(android.R.id.content);
271             return TouchCommon.singleClickViewThroughTarget(
272                     getContainerView(webContents), target, clickTarget[0], clickTarget[1]);
273         }
274     }
275 
276     /**
277      * Click a DOM node returned by JS code, scrolling it into view first.
278      * @param webContents The WebContents in which the node lives.
279      * @param jsCode The JS code to find the node.
280      */
clickNodeByJs(final WebContents webContents, String jsCode)281     public static void clickNodeByJs(final WebContents webContents, String jsCode)
282             throws TimeoutException {
283         scrollNodeIntoViewByJs(webContents, jsCode);
284         int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
285         TouchCommon.singleClickView(getContainerView(webContents), clickTarget[0], clickTarget[1]);
286     }
287 
288     /**
289      * Click a given rect in the page. Does not move the rect into view.
290      * @param webContents The WebContents in which the node lives.
291      * @param rect The rect to click.
292      */
clickRect(final WebContents webContents, Rect rect)293     public static boolean clickRect(final WebContents webContents, Rect rect) {
294         int[] clickTarget = getClickTargetForBounds(webContents, rect);
295         return TouchCommon.singleClickView(
296                 getContainerView(webContents), clickTarget[0], clickTarget[1]);
297     }
298 
299     /**
300      * Starts (synchronously) a drag motion on the specified coordinates of a DOM node by its id,
301      * scrolling it into view first. Normally followed by dragNodeTo() and dragNodeEnd().
302      *
303      * @param webContents The WebContents in which the node lives.
304      * @param nodeId The id of the node.
305      * @param downTime When the drag was started, in millis since the epoch.
306      */
dragNodeStart(final WebContents webContents, String nodeId, long downTime)307     public static void dragNodeStart(final WebContents webContents, String nodeId, long downTime)
308             throws TimeoutException {
309         scrollNodeIntoView(webContents, nodeId);
310         String jsCode = "document.getElementById('" + nodeId + "')";
311         int[] fromTarget = getClickTargetForNodeByJs(webContents, jsCode);
312         TouchCommon.dragStart(getActivity(webContents), fromTarget[0], fromTarget[1], downTime);
313     }
314 
315     /**
316      * Drags / moves (synchronously) to the specified coordinates of a DOM node by its id. Normally
317      * preceded by dragNodeStart() and followed by dragNodeEnd()
318      *
319      * @param webContents The WebContents in which the node lives.
320      * @param fromNodeId The id of the node's coordinates of the initial touch.
321      * @param toNodeId The id of the node's coordinates of the drag destination.
322      * @param stepCount How many move steps to include in the drag.
323      * @param downTime When the drag was started, in millis since the epoch.
324      */
dragNodeTo(final WebContents webContents, String fromNodeId, String toNodeId, int stepCount, long downTime)325     public static void dragNodeTo(final WebContents webContents, String fromNodeId, String toNodeId,
326             int stepCount, long downTime) throws TimeoutException {
327         int[] fromTarget = getClickTargetForNodeByJs(
328                 webContents, "document.getElementById('" + fromNodeId + "')");
329         int[] toTarget = getClickTargetForNodeByJs(
330                 webContents, "document.getElementById('" + toNodeId + "')");
331         TouchCommon.dragTo(getActivity(webContents), fromTarget[0], fromTarget[1], toTarget[0],
332                 toTarget[1], stepCount, downTime);
333     }
334 
335     /**
336      * Finishes (synchronously) a drag / move at the specified coordinate of a DOM node by its id,
337      * scrolling it into view first. Normally preceded by dragNodeStart() and dragNodeTo().
338      *
339      * @param webContents The WebContents in which the node lives.
340      * @param nodeId The id of the node.
341      * @param downTime When the drag was started, in millis since the epoch.
342      */
dragNodeEnd(final WebContents webContents, String nodeId, long downTime)343     public static void dragNodeEnd(final WebContents webContents, String nodeId, long downTime)
344             throws TimeoutException {
345         scrollNodeIntoView(webContents, nodeId);
346         String jsCode = "document.getElementById('" + nodeId + "')";
347         int[] endTarget = getClickTargetForNodeByJs(webContents, jsCode);
348         TouchCommon.dragEnd(getActivity(webContents), endTarget[0], endTarget[1], downTime);
349     }
350 
351     /**
352      * Long-press a DOM node by its id, scrolling it into view first and without release.
353      * @param webContents The WebContents in which the node lives.
354      * @param nodeId The id of the node.
355      * @param downTime When the Long-press was started, in millis since the epoch.
356      */
longPressNodeWithoutUp( final WebContents webContents, String nodeId, long downTime)357     public static void longPressNodeWithoutUp(
358             final WebContents webContents, String nodeId, long downTime) throws TimeoutException {
359         scrollNodeIntoView(webContents, nodeId);
360         String jsCode = "document.getElementById('" + nodeId + "')";
361         longPressNodeWithoutUpByJs(webContents, jsCode, downTime);
362     }
363 
364     /**
365      * Long-press a DOM node by its id, without release.
366      * <p>Note that content view should be located in the current position for a foreseeable
367      * amount of time because this involves sleep to simulate touch to long press transition.
368      * @param webContents The WebContents in which the node lives.
369      * @param jsCode js code that returns an element.
370      * @param downTime When the Long-press was started, in millis since the epoch.
371      */
longPressNodeWithoutUpByJs( final WebContents webContents, String jsCode, long downTime)372     public static void longPressNodeWithoutUpByJs(
373             final WebContents webContents, String jsCode, long downTime) throws TimeoutException {
374         int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
375         TouchCommon.longPressViewWithoutUp(
376                 getContainerView(webContents), clickTarget[0], clickTarget[1], downTime);
377     }
378 
379     /**
380      * Long-press a DOM node by its id, scrolling it into view first.
381      * @param webContents The WebContents in which the node lives.
382      * @param nodeId The id of the node.
383      */
longPressNode(final WebContents webContents, String nodeId)384     public static void longPressNode(final WebContents webContents, String nodeId)
385             throws TimeoutException {
386         scrollNodeIntoView(webContents, nodeId);
387         String jsCode = "document.getElementById('" + nodeId + "')";
388         longPressNodeByJs(webContents, jsCode);
389     }
390 
391     /**
392      * Long-press a DOM node by its id.
393      * <p>Note that content view should be located in the current position for a foreseeable
394      * amount of time because this involves sleep to simulate touch to long press transition.
395      * @param webContents The WebContents in which the node lives.
396      * @param jsCode js code that returns an element.
397      */
longPressNodeByJs(final WebContents webContents, String jsCode)398     public static void longPressNodeByJs(final WebContents webContents, String jsCode)
399             throws TimeoutException {
400         int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
401         TouchCommon.longPressView(getContainerView(webContents), clickTarget[0], clickTarget[1]);
402     }
403 
404     /**
405      * Scrolls the view to ensure that the required DOM node is visible.
406      * @param webContents The WebContents in which the node lives.
407      * @param nodeId The id of the node.
408      */
scrollNodeIntoView(WebContents webContents, String nodeId)409     public static void scrollNodeIntoView(WebContents webContents, String nodeId)
410             throws TimeoutException {
411         scrollNodeIntoViewByJs(webContents, "document.getElementById('" + nodeId + "')");
412     }
413 
414     /**
415      * Scrolls the view to ensure that the required DOM node is visible.
416      * @param webContents The WebContents in which the node lives.
417      * @param jsCode The JS code to find the node.
418      */
scrollNodeIntoViewByJs(WebContents webContents, String jsCode)419     public static void scrollNodeIntoViewByJs(WebContents webContents, String jsCode)
420             throws TimeoutException {
421         JavaScriptUtils.executeJavaScriptAndWaitForResult(
422                 webContents, jsCode + ".scrollIntoView()");
423     }
424 
425     /**
426      * Returns the text contents of a given node.
427      * @param webContents The WebContents in which the node lives.
428      * @param nodeId The id of the node.
429      * @return the text contents of the node.
430      */
getNodeContents(WebContents webContents, String nodeId)431     public static String getNodeContents(WebContents webContents, String nodeId)
432             throws TimeoutException {
433         return getNodeField("textContent", webContents, nodeId, String.class);
434     }
435 
436     /**
437      * Returns the value of a given node.
438      * @param webContents The WebContents in which the node lives.
439      * @param nodeId The id of the node.
440      * @return the value of the node.
441      */
getNodeValue(final WebContents webContents, String nodeId)442     public static String getNodeValue(final WebContents webContents, String nodeId)
443             throws TimeoutException {
444         return getNodeField("value", webContents, nodeId, String.class);
445     }
446 
447     /**
448      * Returns the string value of a field of a given node.
449      * @param fieldName The field to return the value from.
450      * @param webContents The WebContents in which the node lives.
451      * @param nodeId The id of the node.
452      * @return the value of the field.
453      */
getNodeField(String fieldName, final WebContents webContents, String nodeId)454     public static String getNodeField(String fieldName, final WebContents webContents,
455             String nodeId) throws TimeoutException {
456         return getNodeField(fieldName, webContents, nodeId, String.class);
457     }
458 
459     /**
460      * Wait until a given node has non-zero bounds.
461      * @param webContents The WebContents in which the node lives.
462      * @param nodeId The id of the node.
463      */
waitForNonZeroNodeBounds( final WebContents webContents, final String nodeId)464     public static void waitForNonZeroNodeBounds(
465             final WebContents webContents, final String nodeId) {
466         CriteriaHelper.pollInstrumentationThread(() -> {
467             try {
468                 Criteria.checkThat(
469                         DOMUtils.getNodeBounds(webContents, nodeId).isEmpty(), Matchers.is(false));
470             } catch (TimeoutException e) {
471                 // Intentionally do nothing
472                 throw new CriteriaNotSatisfiedException(e);
473             }
474         });
475     }
476 
477     /**
478      * Returns the value of a given field of type {@code valueType} as a {@code T}.
479      * @param fieldName The field to return the value from.
480      * @param webContents The WebContents in which the node lives.
481      * @param nodeId The id of the node.
482      * @param valueType The type of the value to read.
483      * @return the field's value.
484      */
getNodeField(String fieldName, final WebContents webContents, String nodeId, Class<T> valueType)485     public static <T> T getNodeField(String fieldName, final WebContents webContents, String nodeId,
486             Class<T> valueType) throws TimeoutException {
487         StringBuilder sb = new StringBuilder();
488         sb.append("(function() {");
489         sb.append("  var node = document.getElementById('" + nodeId + "');");
490         sb.append("  if (!node) return null;");
491         sb.append("  return [ node." + fieldName + " ];");
492         sb.append("})();");
493 
494         String jsonText =
495                 JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
496         Assert.assertFalse("Failed to retrieve contents for " + nodeId,
497                 jsonText.trim().equalsIgnoreCase("null"));
498         return readValue(jsonText, valueType);
499     }
500 
501     /**
502      * Returns the value of a given attribute of type {@code valueType} as a {@code T}.
503      * @param attributeName The attribute to return the value from.
504      * @param webContents The WebContents in which the node lives.
505      * @param nodeId The id of the node.
506      * @param valueType The type of the value to read.
507      * @return the attributes' value.
508      */
getNodeAttribute(String attributeName, final WebContents webContents, String nodeId, Class<T> valueType)509     public static <T> T getNodeAttribute(String attributeName, final WebContents webContents,
510             String nodeId, Class<T> valueType) throws InterruptedException, TimeoutException {
511         StringBuilder sb = new StringBuilder();
512         sb.append("(function() {");
513         sb.append("  var node = document.getElementById('" + nodeId + "');");
514         sb.append("  if (!node) return null;");
515         sb.append("  return [ node.getAttribute('" + attributeName + "') ];");
516         sb.append("})();");
517 
518         String jsonText =
519                 JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
520         Assert.assertFalse("Failed to retrieve contents for " + nodeId,
521                 jsonText.trim().equalsIgnoreCase("null"));
522         return readValue(jsonText, valueType);
523     }
524 
525     /**
526      * Returns the next value of type {@code valueType} as a {@code T}.
527      * @param jsonText The unparsed json text.
528      * @param valueType The type of the value to read.
529      * @return the read value.
530      */
readValue(String jsonText, Class<T> valueType)531     private static <T> T readValue(String jsonText, Class<T> valueType) {
532         JsonReader jsonReader = new JsonReader(new StringReader(jsonText));
533         T value = null;
534         try {
535             jsonReader.beginArray();
536             if (jsonReader.hasNext()) value = readValue(jsonReader, valueType);
537             jsonReader.endArray();
538             Assert.assertNotNull("Invalid contents returned.", value);
539 
540             jsonReader.close();
541         } catch (IOException exception) {
542             Assert.fail("Failed to evaluate JavaScript: " + jsonText + "\n" + exception);
543         }
544         return value;
545     }
546 
547     /**
548      * Returns the next value of type {@code valueType} as a {@code T}.
549      * @param jsonReader JsonReader instance to be used.
550      * @param valueType The type of the value to read.
551      * @throws IllegalArgumentException If the {@code valueType} isn't known.
552      * @return the read value.
553      */
554     @SuppressWarnings("unchecked")
readValue(JsonReader jsonReader, Class<T> valueType)555     private static <T> T readValue(JsonReader jsonReader, Class<T> valueType) throws IOException {
556         if (valueType.equals(String.class)) return ((T) jsonReader.nextString());
557         if (valueType.equals(Boolean.class)) return ((T) ((Boolean) jsonReader.nextBoolean()));
558         if (valueType.equals(Integer.class)) return ((T) ((Integer) jsonReader.nextInt()));
559         if (valueType.equals(Long.class)) return ((T) ((Long) jsonReader.nextLong()));
560         if (valueType.equals(Double.class)) return ((T) ((Double) jsonReader.nextDouble()));
561 
562         throw new IllegalArgumentException("Cannot read values of type " + valueType);
563     }
564 
565     /**
566      * Returns click target for a given DOM node.
567      * @param webContents The WebContents in which the node lives.
568      * @param nodeId The id of the node.
569      * @return the click target of the node in the form of a [ x, y ] array.
570      */
getClickTargetForNode(WebContents webContents, String nodeId)571     private static int[] getClickTargetForNode(WebContents webContents, String nodeId)
572             throws TimeoutException {
573         String jsCode = "document.getElementById('" + nodeId + "')";
574         return getClickTargetForNodeByJs(webContents, jsCode);
575     }
576 
577     /**
578      * Returns click target for a given DOM node.
579      * @param webContents The WebContents in which the node lives.
580      * @param jsCode The javascript to get the node.
581      * @return the click target of the node in the form of a [ x, y ] array.
582      */
getClickTargetForNodeByJs(WebContents webContents, String jsCode)583     private static int[] getClickTargetForNodeByJs(WebContents webContents, String jsCode)
584             throws TimeoutException {
585         Rect bounds = getNodeBoundsByJs(webContents, jsCode);
586         Assert.assertNotNull(
587                 "Failed to get DOM element bounds of element='" + jsCode + "'.", bounds);
588 
589         return getClickTargetForBounds(webContents, bounds);
590     }
591 
592     /**
593      * Returns click target for the DOM node specified by the rect boundaries.
594      * @param webContents The WebContents in which the node lives.
595      * @param bounds The rect boundaries of a DOM node.
596      * @return the click target of the node in the form of a [ x, y ] array.
597      */
getClickTargetForBounds(WebContents webContents, Rect bounds)598     private static int[] getClickTargetForBounds(WebContents webContents, Rect bounds) {
599         // TODO(nburris): This converts from CSS pixels to physical pixels, but
600         // does not account for visual viewport offset.
601         RenderCoordinatesImpl coord = ((WebContentsImpl) webContents).getRenderCoordinates();
602         int clickX = (int) coord.fromLocalCssToPix(bounds.exactCenterX());
603         int clickY = (int) coord.fromLocalCssToPix(bounds.exactCenterY())
604                 + getMaybeTopControlsHeight(webContents);
605 
606         // This scale will almost always be 1. See the comments on
607         // DisplayAndroid#getAndroidUIScaling().
608         float scale = webContents.getTopLevelNativeWindow().getDisplay().getAndroidUIScaling();
609 
610         return new int[] {(int) (clickX * scale), (int) (clickY * scale)};
611     }
612 
getMaybeTopControlsHeight(final WebContents webContents)613     private static int getMaybeTopControlsHeight(final WebContents webContents) {
614         try {
615             return TestThreadUtils.runOnUiThreadBlocking(
616                     () -> nativeGetTopControlsShrinkBlinkHeight(webContents));
617         } catch (ExecutionException e) {
618             return 0;
619         }
620     }
621 
622     /**
623      * Returns the rect boundaries for a node by the javascript to get the node.
624      * @param webContents The WebContents in which the node lives.
625      * @param jsCode The javascript to get the node.
626      * @return The rect boundaries for the node.
627      */
getNodeBoundsByJs(final WebContents webContents, String jsCode)628     private static Rect getNodeBoundsByJs(final WebContents webContents, String jsCode)
629             throws TimeoutException {
630         StringBuilder sb = new StringBuilder();
631         sb.append("(function() {");
632         sb.append("  var node = " + jsCode + ";");
633         sb.append("  if (!node) return null;");
634         sb.append("  var width = Math.round(node.offsetWidth);");
635         sb.append("  var height = Math.round(node.offsetHeight);");
636         sb.append("  var x = -window.scrollX;");
637         sb.append("  var y = -window.scrollY;");
638         sb.append("  do {");
639         sb.append("    x += node.offsetLeft;");
640         sb.append("    y += node.offsetTop;");
641         sb.append("  } while (node = node.offsetParent);");
642         sb.append("  return [ Math.round(x), Math.round(y), width, height ];");
643         sb.append("})();");
644 
645         String jsonText =
646                 JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
647 
648         Assert.assertFalse("Failed to retrieve bounds for element: " + jsCode,
649                 jsonText.trim().equalsIgnoreCase("null"));
650 
651         JsonReader jsonReader = new JsonReader(new StringReader(jsonText));
652         int[] bounds = new int[4];
653         try {
654             jsonReader.beginArray();
655             int i = 0;
656             while (jsonReader.hasNext()) {
657                 bounds[i++] = jsonReader.nextInt();
658             }
659             jsonReader.endArray();
660             Assert.assertEquals("Invalid bounds returned.", 4, i);
661 
662             jsonReader.close();
663         } catch (IOException exception) {
664             Assert.fail("Failed to evaluate JavaScript: " + jsonText + "\n" + exception);
665         }
666 
667         return new Rect(bounds[0], bounds[1], bounds[0] + bounds[2], bounds[1] + bounds[3]);
668     }
669 
nativeGetTopControlsShrinkBlinkHeight(WebContents webContents)670     private static native int nativeGetTopControlsShrinkBlinkHeight(WebContents webContents);
671 }
672