// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content_public.browser.test.util;
import static org.hamcrest.CoreMatchers.is;
import android.app.Activity;
import android.graphics.Rect;
import android.util.JsonReader;
import android.view.View;
import org.hamcrest.Matchers;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.junit.Assert;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.content.browser.RenderCoordinatesImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.WebContents;
import java.io.IOException;
import java.io.StringReader;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Collection of DOM-based utilities. */
@JNINamespace("content")
public class DOMUtils {
private static final long MEDIA_TIMEOUT_SECONDS = 10L;
private static final long MEDIA_TIMEOUT_MILLISECONDS = MEDIA_TIMEOUT_SECONDS * 1000;
private static final String RESULT_OK = "RESULT_OK";
private static final String RESULT_ELEMENT_NOT_FOUND = "RESULT_ELEMENT_NOT_FOUND";
/**
* Plays the media with given {@code id}.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to be played.
*/
public static void playMedia(final WebContents webContents, final String id)
throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var media = document.getElementById('" + id + "');");
sb.append(" if (media) media.play();");
sb.append("})();");
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, sb.toString(), MEDIA_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* Pauses the media with given {@code id}
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to be paused.
*/
public static void pauseMedia(final WebContents webContents, final String id)
throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var media = document.getElementById('" + id + "');");
sb.append(" if (media) media.pause();");
sb.append("})();");
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, sb.toString(), MEDIA_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* Returns whether the media with given {@code id} is paused.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to check.
* @return whether the media is paused.
*/
public static boolean isMediaPaused(final WebContents webContents, final String id)
throws TimeoutException {
return getNodeField("paused", webContents, id, Boolean.class);
}
/**
* Returns whether the media with given {@code id} has ended.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to check.
* @return whether the media has ended.
*/
public static boolean isMediaEnded(final WebContents webContents, final String id)
throws TimeoutException {
return getNodeField("ended", webContents, id, Boolean.class);
}
/**
* Returns the current time of the media with given {@code id}.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to check.
* @return the current time (in seconds) of the media.
*/
private static double getCurrentTime(final WebContents webContents, final String id)
throws TimeoutException {
return getNodeField("currentTime", webContents, id, Double.class);
}
/**
* Waits until the playback of the media with given {@code id} has started.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to check.
*/
public static void waitForMediaPlay(final WebContents webContents, final String id) {
CriteriaHelper.pollInstrumentationThread(
() -> {
try {
// Playback can't be reliably detected until current time moves forward.
Criteria.checkThat(
DOMUtils.isMediaPaused(webContents, id), Matchers.is(false));
Criteria.checkThat(
DOMUtils.getCurrentTime(webContents, id), Matchers.greaterThan(0d));
} catch (TimeoutException e) {
// Intentionally do nothing
throw new CriteriaNotSatisfiedException(e);
}
},
MEDIA_TIMEOUT_MILLISECONDS,
CriteriaHelper.DEFAULT_POLLING_INTERVAL);
}
/**
* Waits until the playback of the media with given {@code id} has paused before ended.
* @param webContents The WebContents in which the media element lives.
* @param id The element's id to check.
*/
public static void waitForMediaPauseBeforeEnd(final WebContents webContents, final String id) {
CriteriaHelper.pollInstrumentationThread(
() -> {
try {
Criteria.checkThat(
DOMUtils.isMediaPaused(webContents, id), Matchers.is(true));
Criteria.checkThat(
DOMUtils.isMediaEnded(webContents, id), Matchers.is(false));
} catch (TimeoutException e) {
// Intentionally do nothing
throw new CriteriaNotSatisfiedException(e);
}
});
}
/**
* Returns whether the document is fullscreen.
* @param webContents The WebContents to check.
* @return Whether the document is fullsscreen.
*/
public static boolean isFullscreen(final WebContents webContents) throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" return [document.webkitIsFullScreen];");
sb.append("})();");
String jsonText =
JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
return readValue(jsonText, Boolean.class);
}
/**
* Makes the document exit fullscreen.
* @param webContents The WebContents to make fullscreen.
*/
public static void exitFullscreen(final WebContents webContents) {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" if (document.webkitExitFullscreen) document.webkitExitFullscreen();");
sb.append("})();");
JavaScriptUtils.executeJavaScript(webContents, sb.toString());
}
private static View getContainerView(final WebContents webContents) {
return ((WebContentsImpl) webContents).getViewAndroidDelegate().getContainerView();
}
private static Activity getActivity(final WebContents webContents) {
return ContextUtils.activityFromContext(((WebContentsImpl) webContents).getContext());
}
/**
* Returns the rect boundaries for a node by its id.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @return The rect boundaries for the node.
*/
public static Rect getNodeBounds(final WebContents webContents, String nodeId)
throws TimeoutException {
String jsCode = "document.getElementById('" + nodeId + "')";
return getNodeBoundsByJs(webContents, jsCode);
}
/**
* Focus a DOM node by its id.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static void focusNode(final WebContents webContents, String nodeId)
throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var node = document.getElementById('" + nodeId + "');");
sb.append(" if (node) node.focus();");
sb.append("})();");
JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
}
/**
* Get the id of the currently focused node.
* @param webContents The WebContents in which the node lives.
* @return The id of the currently focused node.
*/
public static String getFocusedNode(WebContents webContents) throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var node = document.activeElement;");
sb.append(" if (!node) return null;");
sb.append(" return node.id;");
sb.append("})();");
String id = JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
// String results from JavaScript includes surrounding quotes. Remove them.
if (id != null && id.length() >= 2 && id.charAt(0) == '"') {
id = id.substring(1, id.length() - 1);
}
return id;
}
/**
* Click a DOM node by its id, scrolling it into view first.
* Warning: This method might cause flakiness in the tests
* See http://crbug.com/1327063
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static boolean clickNode(final WebContents webContents, String nodeId)
throws TimeoutException {
return clickNode(webContents, nodeId, /* goThroughRootAndroidView= */ true);
}
/**
* Click a DOM node by its id, scrolling it into view first.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param goThroughRootAndroidView Whether the input should be routed through the Root View for
* the CVC.
*/
public static boolean clickNode(
final WebContents webContents, String nodeId, boolean goThroughRootAndroidView)
throws TimeoutException {
return clickNode(
webContents, nodeId, goThroughRootAndroidView, /* shouldScrollIntoView= */ true);
}
/**
* Click a DOM node by its id.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param goThroughRootAndroidView Whether the input should be routed through the Root View for
* the CVC.
* @param shouldScrollIntoView Whether to scroll the node into view first.
*/
public static boolean clickNode(
final WebContents webContents,
String nodeId,
boolean goThroughRootAndroidView,
boolean shouldScrollIntoView)
throws TimeoutException {
if (shouldScrollIntoView) scrollNodeIntoView(webContents, nodeId);
int[] clickTarget = getClickTargetForNode(webContents, nodeId);
if (goThroughRootAndroidView) {
return TouchCommon.singleClickView(
getContainerView(webContents), clickTarget[0], clickTarget[1]);
} else {
// TODO(mthiesse): It should be sufficient to use getContainerView(webContents) here
// directly, but content offsets are only updated in the EventForwarder when the
// CompositorViewHolder intercepts touch events.
View target =
getContainerView(webContents).getRootView().findViewById(android.R.id.content);
return TouchCommon.singleClickViewThroughTarget(
getContainerView(webContents), target, clickTarget[0], clickTarget[1]);
}
}
/**
* Click a DOM node returned by JS code, scrolling it into view first.
* @param webContents The WebContents in which the node lives.
* @param jsCode The JS code to find the node.
*/
public static void clickNodeByJs(final WebContents webContents, String jsCode)
throws TimeoutException {
scrollNodeIntoViewByJs(webContents, jsCode);
int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
TouchCommon.singleClickView(getContainerView(webContents), clickTarget[0], clickTarget[1]);
}
/**
* Click a given rect in the page. Does not move the rect into view.
* @param webContents The WebContents in which the node lives.
* @param rect The rect to click.
*/
public static boolean clickRect(final WebContents webContents, Rect rect) {
int[] clickTarget = getClickTargetForBounds(webContents, rect);
return TouchCommon.singleClickView(
getContainerView(webContents), clickTarget[0], clickTarget[1]);
}
/**
* Starts (synchronously) a drag motion on the specified coordinates of a DOM node by its id,
* scrolling it into view first. Normally followed by dragNodeTo() and dragNodeEnd().
*
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param downTime When the drag was started, in millis since the epoch.
*/
public static void dragNodeStart(final WebContents webContents, String nodeId, long downTime)
throws TimeoutException {
scrollNodeIntoView(webContents, nodeId);
String jsCode = "document.getElementById('" + nodeId + "')";
int[] fromTarget = getClickTargetForNodeByJs(webContents, jsCode);
TouchCommon.dragStart(getActivity(webContents), fromTarget[0], fromTarget[1], downTime);
}
/**
* Drags / moves (synchronously) to the specified coordinates of a DOM node by its id. Normally
* preceded by dragNodeStart() and followed by dragNodeEnd()
*
* @param webContents The WebContents in which the node lives.
* @param fromNodeId The id of the node's coordinates of the initial touch.
* @param toNodeId The id of the node's coordinates of the drag destination.
* @param stepCount How many move steps to include in the drag.
* @param downTime When the drag was started, in millis since the epoch.
*/
public static void dragNodeTo(
final WebContents webContents,
String fromNodeId,
String toNodeId,
int stepCount,
long downTime)
throws TimeoutException {
int[] fromTarget =
getClickTargetForNodeByJs(
webContents, "document.getElementById('" + fromNodeId + "')");
int[] toTarget =
getClickTargetForNodeByJs(
webContents, "document.getElementById('" + toNodeId + "')");
TouchCommon.dragTo(
getActivity(webContents),
fromTarget[0],
fromTarget[1],
toTarget[0],
toTarget[1],
stepCount,
downTime);
}
/**
* Finishes (synchronously) a drag / move at the specified coordinate of a DOM node by its id,
* scrolling it into view first. Normally preceded by dragNodeStart() and dragNodeTo().
*
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param downTime When the drag was started, in millis since the epoch.
*/
public static void dragNodeEnd(final WebContents webContents, String nodeId, long downTime)
throws TimeoutException {
scrollNodeIntoView(webContents, nodeId);
String jsCode = "document.getElementById('" + nodeId + "')";
int[] endTarget = getClickTargetForNodeByJs(webContents, jsCode);
TouchCommon.dragEnd(getActivity(webContents), endTarget[0], endTarget[1], downTime);
}
/**
* Long-press a DOM node by its id, scrolling it into view first and without release.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param downTime When the Long-press was started, in millis since the epoch.
*/
public static void longPressNodeWithoutUp(
final WebContents webContents, String nodeId, long downTime) throws TimeoutException {
scrollNodeIntoView(webContents, nodeId);
String jsCode = "document.getElementById('" + nodeId + "')";
longPressNodeWithoutUpByJs(webContents, jsCode, downTime);
}
/**
* Long-press a DOM node by its id, without release.
* <p>Note that content view should be located in the current position for a foreseeable
* amount of time because this involves sleep to simulate touch to long press transition.
* @param webContents The WebContents in which the node lives.
* @param jsCode js code that returns an element.
* @param downTime When the Long-press was started, in millis since the epoch.
*/
public static void longPressNodeWithoutUpByJs(
final WebContents webContents, String jsCode, long downTime) throws TimeoutException {
int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
TouchCommon.longPressViewWithoutUp(
getContainerView(webContents), clickTarget[0], clickTarget[1], downTime);
}
/**
* Long-press a DOM node by its id, scrolling it into view first.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static void longPressNode(final WebContents webContents, String nodeId)
throws TimeoutException {
scrollNodeIntoView(webContents, nodeId);
String jsCode = "document.getElementById('" + nodeId + "')";
longPressNodeByJs(webContents, jsCode);
}
/**
* Long-press a DOM node by its id.
* <p>Note that content view should be located in the current position for a foreseeable
* amount of time because this involves sleep to simulate touch to long press transition.
* @param webContents The WebContents in which the node lives.
* @param jsCode js code that returns an element.
*/
public static void longPressNodeByJs(final WebContents webContents, String jsCode)
throws TimeoutException {
int[] clickTarget = getClickTargetForNodeByJs(webContents, jsCode);
TouchCommon.longPressView(getContainerView(webContents), clickTarget[0], clickTarget[1]);
}
/**
* Scrolls the view to ensure that the required DOM node is visible.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static void scrollNodeIntoView(WebContents webContents, String nodeId)
throws TimeoutException {
scrollNodeIntoViewByJs(webContents, "document.getElementById('" + nodeId + "')");
}
/**
* Scrolls the view to ensure that the required DOM node is visible.
* @param webContents The WebContents in which the node lives.
* @param jsCode The JS code to find the node.
*/
public static void scrollNodeIntoViewByJs(WebContents webContents, String jsCode)
throws TimeoutException {
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, jsCode + ".scrollIntoView()");
}
/**
* Returns the text contents of a given node.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @return the text contents of the node.
*/
public static String getNodeContents(WebContents webContents, String nodeId)
throws TimeoutException {
return getNodeField("textContent", webContents, nodeId, String.class);
}
/**
* Returns the value of a given node.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @return the value of the node.
*/
public static String getNodeValue(final WebContents webContents, String nodeId)
throws TimeoutException {
return getNodeField("value", webContents, nodeId, String.class);
}
/**
* Returns the string value of a field of a given node.
* @param fieldName The field to return the value from.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @return the value of the field.
*/
public static String getNodeField(
String fieldName, final WebContents webContents, String nodeId)
throws TimeoutException {
return getNodeField(fieldName, webContents, nodeId, String.class);
}
/**
* Wait until a given node has non-zero bounds.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static void waitForNonZeroNodeBounds(
final WebContents webContents, final String nodeId) {
CriteriaHelper.pollInstrumentationThread(
() -> {
try {
Criteria.checkThat(
DOMUtils.getNodeBounds(webContents, nodeId).isEmpty(),
Matchers.is(false));
} catch (TimeoutException e) {
// Intentionally do nothing
throw new CriteriaNotSatisfiedException(e);
}
});
}
/**
* Returns the value of a given field of type {@code valueType} as a {@code T}.
* @param fieldName The field to return the value from.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param valueType The type of the value to read.
* @return the field's value.
*/
public static <T> T getNodeField(
String fieldName, final WebContents webContents, String nodeId, Class<T> valueType)
throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var node = document.getElementById('" + nodeId + "');");
sb.append(" if (!node) return null;");
sb.append(" return [ node." + fieldName + " ];");
sb.append("})();");
String jsonText =
JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
Assert.assertFalse(
"Failed to retrieve contents for " + nodeId,
jsonText.trim().equalsIgnoreCase("null"));
return readValue(jsonText, valueType);
}
/**
* Returns the value of a given attribute of type {@code valueType} as a {@code T} or null.
* @param attributeName The attribute to return the value from.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @param valueType The type of the value to read.
* @return the attributes' value or null if there is no attribute with such attributeName.
*/
public static <T> T getNodeAttribute(
String attributeName, final WebContents webContents, String nodeId, Class<T> valueType)
throws InterruptedException, TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var node = document.getElementById('" + nodeId + "');");
sb.append(" if (!node) return null;");
sb.append(" var nodeAttr = node.getAttribute('" + attributeName + "');");
sb.append(" if (!nodeAttr) return null;");
sb.append(" return [ nodeAttr ];");
sb.append("})();");
String jsonText =
JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
if (jsonText.trim().equalsIgnoreCase("null")) {
return null;
}
return readValue(jsonText, valueType);
}
/**
* Click a DOM node by its id using a js MouseEvent with a fake gesture.
* This function is more reliable than {@link #clickNode(WebContents, String)},
* but it doesn't simulate a screen touch.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
*/
public static void clickNodeWithJavaScript(WebContents webContents, String nodeId) {
WebContentsUtils.evaluateJavaScriptWithUserGesture(
webContents, createScriptToClickNode(nodeId), null);
}
/**
* Returns the next value of type {@code valueType} as a {@code T}.
* @param jsonText The unparsed json text.
* @param valueType The type of the value to read.
* @return the read value.
*/
private static <T> T readValue(String jsonText, Class<T> valueType) {
JsonReader jsonReader = new JsonReader(new StringReader(jsonText));
T value = null;
try {
jsonReader.beginArray();
if (jsonReader.hasNext()) value = readValue(jsonReader, valueType);
jsonReader.endArray();
Assert.assertNotNull("Invalid contents returned.", value);
jsonReader.close();
} catch (IOException exception) {
Assert.fail("Failed to evaluate JavaScript: " + jsonText + "\n" + exception);
}
return value;
}
/**
* Returns the next value of type {@code valueType} as a {@code T}.
* @param jsonReader JsonReader instance to be used.
* @param valueType The type of the value to read.
* @throws IllegalArgumentException If the {@code valueType} isn't known.
* @return the read value.
*/
@SuppressWarnings("unchecked")
private static <T> T readValue(JsonReader jsonReader, Class<T> valueType) throws IOException {
if (valueType.equals(String.class)) return ((T) jsonReader.nextString());
if (valueType.equals(Boolean.class)) return ((T) ((Boolean) jsonReader.nextBoolean()));
if (valueType.equals(Integer.class)) return ((T) ((Integer) jsonReader.nextInt()));
if (valueType.equals(Long.class)) return ((T) ((Long) jsonReader.nextLong()));
if (valueType.equals(Double.class)) return ((T) ((Double) jsonReader.nextDouble()));
throw new IllegalArgumentException("Cannot read values of type " + valueType);
}
/**
* Returns click target for a given DOM node.
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the node.
* @return the click target of the node in the form of a [ x, y ] array.
*/
private static int[] getClickTargetForNode(WebContents webContents, String nodeId)
throws TimeoutException {
String jsCode = "document.getElementById('" + nodeId + "')";
return getClickTargetForNodeByJs(webContents, jsCode);
}
/**
* Returns click target for a given DOM node.
* @param webContents The WebContents in which the node lives.
* @param jsCode The javascript to get the node.
* @return the click target of the node in the form of a [ x, y ] array.
*/
private static int[] getClickTargetForNodeByJs(WebContents webContents, String jsCode)
throws TimeoutException {
Rect bounds = getNodeBoundsByJs(webContents, jsCode);
Assert.assertNotNull(
"Failed to get DOM element bounds of element='" + jsCode + "'.", bounds);
return getClickTargetForBounds(webContents, bounds);
}
/**
* Returns click target for the DOM node specified by the rect boundaries.
* @param webContents The WebContents in which the node lives.
* @param bounds The rect boundaries of a DOM node.
* @return the click target of the node in the form of a [ x, y ] array.
*/
private static int[] getClickTargetForBounds(WebContents webContents, Rect bounds) {
// TODO(nburris): This converts from CSS pixels to physical pixels, but
// does not account for visual viewport offset.
RenderCoordinatesImpl coord = ((WebContentsImpl) webContents).getRenderCoordinates();
int clickX = (int) coord.fromLocalCssToPix(bounds.exactCenterX());
int clickY =
(int) coord.fromLocalCssToPix(bounds.exactCenterY())
+ getMaybeTopControlsHeight(webContents);
return new int[] {clickX, clickY};
}
private static int getMaybeTopControlsHeight(final WebContents webContents) {
return ThreadUtils.runOnUiThreadBlocking(
() -> DOMUtilsJni.get().getTopControlsShrinkBlinkHeight(webContents));
}
/**
* Returns the rect boundaries for a node by the javascript to get the node.
* @param webContents The WebContents in which the node lives.
* @param jsCode The javascript to get the node.
* @return The rect boundaries for the node.
*/
private static Rect getNodeBoundsByJs(final WebContents webContents, String jsCode)
throws TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
sb.append(" var node = " + jsCode + ";");
sb.append(" if (!node) return null;");
sb.append(" var width = Math.round(node.offsetWidth);");
sb.append(" var height = Math.round(node.offsetHeight);");
sb.append(" var x = -window.scrollX;");
sb.append(" var y = -window.scrollY;");
sb.append(" do {");
sb.append(" x += node.offsetLeft;");
sb.append(" y += node.offsetTop;");
sb.append(" } while (node = node.offsetParent);");
sb.append(" return [ Math.round(x), Math.round(y), width, height ];");
sb.append("})();");
String jsonText =
JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents, sb.toString());
Assert.assertFalse(
"Failed to retrieve bounds for element: " + jsCode,
jsonText.trim().equalsIgnoreCase("null"));
JsonReader jsonReader = new JsonReader(new StringReader(jsonText));
int[] bounds = new int[4];
try {
jsonReader.beginArray();
int i = 0;
while (jsonReader.hasNext()) {
bounds[i++] = jsonReader.nextInt();
}
jsonReader.endArray();
Assert.assertEquals("Invalid bounds returned.", 4, i);
jsonReader.close();
} catch (IOException exception) {
Assert.fail("Failed to evaluate JavaScript: " + jsonText + "\n" + exception);
}
return new Rect(bounds[0], bounds[1], bounds[0] + bounds[2], bounds[1] + bounds[3]);
}
private static String createScriptToClickNode(String nodeId) {
String script = "document.getElementById('" + nodeId + "').click();";
return script;
}
/**
* Prints the text into the text field node simulating the keyboard input. The node needs to be
* focused at first to bring up the keyboard.
*
* @param webContents The WebContents in which the node lives.
* @param nodeId The id of the text input node.
* @param input The text to be entered into the text field.
*/
public static void enterInputIntoTextField(WebContents webContents, String nodeId, String input)
throws TimeoutException {
Assert.assertTrue(
"Input should be a non-empty string", input != null && input.length() > 0);
ImeAdapter imeAdapter = WebContentsUtils.getImeAdapter(webContents);
TestInputMethodManagerWrapper inputMethodManagerWrapper =
TestInputMethodManagerWrapper.create(imeAdapter);
imeAdapter.setInputMethodManagerWrapper(inputMethodManagerWrapper);
// Click the text field node, so that it would get focus.
DOMUtils.clickNode(webContents, nodeId);
CriteriaHelper.pollInstrumentationThread(
() -> {
try {
Criteria.checkThat(DOMUtils.getFocusedNode(webContents), is(nodeId));
} catch (TimeoutException e) {
throw new CriteriaNotSatisfiedException(e);
}
});
// Wait for the text field to get focused and the virtual keyboard to be activated.
CriteriaHelper.pollInstrumentationThread(
() -> {
Criteria.checkThat(
inputMethodManagerWrapper.isActive(
DOMUtils.getContainerView(webContents)),
is(true));
});
// Enter the text.
imeAdapter.setComposingTextForTest(input, 1);
// Wait for the input to finish. After finishing the input, it will update the selection to
// move the cursor to the right position. This indicated that the input has finished.
waitForTextFieldValue(webContents, nodeId, input);
}
private static void waitForTextFieldValue(
WebContents webContents, String textFieldId, String value) throws TimeoutException {
StringBuilder func = new StringBuilder();
func.append("function valueCheck() {");
func.append(" var element = document.getElementById('" + textFieldId + "');");
func.append(" return element && element.value == '" + value + "';");
func.append("}");
func.append("(async function() {");
func.append("var res = await new Promise(resolve => {");
func.append(" if (valueCheck()) {");
func.append(" return resolve('" + RESULT_OK + "');");
func.append(" } else {");
func.append(" var element = document.getElementById('" + textFieldId + "');");
func.append(" if (!element)");
func.append(" return resolve('" + RESULT_ELEMENT_NOT_FOUND + "');");
func.append(" element.oninput = function() {");
func.append(" if (valueCheck()) {");
func.append(" element.oninput = undefined;");
func.append(" return resolve('" + RESULT_OK + "');");
func.append(" }");
func.append(" };");
func.append(" }");
func.append("});");
func.append("window.domAutomationController.send([res]);");
func.append("})();");
String jsonText =
JavaScriptUtils.runJavascriptWithAsyncResult(webContents, func.toString());
Assert.assertFalse(
"Failed to verify input for field " + textFieldId,
jsonText.trim().equalsIgnoreCase("null"));
String result = readValue(jsonText, String.class);
if (RESULT_ELEMENT_NOT_FOUND.equals(result)) {
Assert.fail(
"Expected to find element with id " + textFieldId + ", but didn't find any.");
}
if (!RESULT_OK.equals(result)) {
Assert.fail(
"Actual value of the field "
+ textFieldId
+ " is different from the expected value "
+ value
+ ".");
}
}
@NativeMethods
interface Natives {
int getTopControlsShrinkBlinkHeight(WebContents webContents);
}
}