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