blob: 83d30eb69f031fc786b71faa298d18a6beda6ed5 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test;
import android.graphics.Rect;
import android.support.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.AwContents;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.components.content_capture.ContentCaptureConsumer;
import org.chromium.components.content_capture.ContentCaptureController;
import org.chromium.components.content_capture.ContentCaptureData;
import org.chromium.components.content_capture.FrameSession;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.util.TestWebServer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Tests for content capture. Those cases could become flaky when renderer is busy, because
* ContentCapture task is run in best effort priority, we will see if this is real problem for
* testing.
*/
@RunWith(AwJUnit4ClassRunner.class)
@CommandLineFlags.Add({"enable-features=ContentCapture"})
public class AwContentCaptureTest {
private static class TestAwContentCatpureController extends ContentCaptureController {
public TestAwContentCatpureController() {
sContentCaptureController = this;
}
@Override
public boolean shouldStartCapture() {
return false;
}
@Override
protected void pullWhitelist() {
String[] whitelist = null;
boolean[] isRegEx = null;
if (mWhiteList != null && mIsRegEx != null) {
mWhiteList.toArray(whitelist);
isRegEx = new boolean[mWhiteList.size()];
int i = 0;
for (boolean r : mIsRegEx) isRegEx[i++] = r;
}
setWhitelist(whitelist, isRegEx);
}
private ArrayList<String> mWhiteList;
private ArrayList<Boolean> mIsRegEx;
}
private static class TestAwContentCaptureConsumer extends ContentCaptureConsumer {
private final static long DEFAULT_TIMEOUT_IN_SECONDS = 10;
public final static int CONTENT_CAPTURED = 1;
public final static int CONTENT_UPDATED = 2;
public final static int CONTENT_REMOVED = 3;
public final static int SESSION_REMOVED = 4;
public TestAwContentCaptureConsumer(WebContents webContents) {
super(webContents);
mCapturedContentIds = new HashSet<Long>();
}
@Override
public void onContentCaptured(
FrameSession parentFrame, ContentCaptureData contentCaptureData) {
mParentFrame = parentFrame;
mCapturedContent = contentCaptureData;
for (ContentCaptureData child : contentCaptureData.getChildren()) {
mCapturedContentIds.add(child.getId());
}
mCallbacks.add(CONTENT_CAPTURED);
mCallbackHelper.notifyCalled();
}
@Override
public void onContentUpdated(
FrameSession parentFrame, ContentCaptureData contentCaptureData) {
mParentFrame = parentFrame;
mUpdatedContent = contentCaptureData;
mCallbacks.add(CONTENT_UPDATED);
mCallbackHelper.notifyCalled();
}
@Override
public void onSessionRemoved(FrameSession session) {
mRemovedSession = session;
mCallbacks.add(SESSION_REMOVED);
mCallbackHelper.notifyCalled();
}
@Override
public void onContentRemoved(FrameSession session, long[] removedIds) {
mCurrentFrameSession = session;
mRemovedIds = removedIds;
// Remove the id from removedIds because id can be reused.
for (long id : removedIds) mCapturedContentIds.remove(id);
mCallbacks.add(CONTENT_REMOVED);
mCallbackHelper.notifyCalled();
}
public FrameSession getParentFrame() {
return mParentFrame;
}
public ContentCaptureData getCapturedContent() {
return mCapturedContent;
}
public ContentCaptureData getUpdatedContent() {
return mUpdatedContent;
}
public FrameSession getCurrentFrameSession() {
return mCurrentFrameSession;
}
public FrameSession getRemovedSession() {
return mRemovedSession;
}
public long[] getRemovedIds() {
return mRemovedIds;
}
public void reset() {
mParentFrame = null;
mCapturedContent = null;
mUpdatedContent = null;
mCurrentFrameSession = null;
mRemovedIds = null;
mCallbacks.clear();
}
public void waitForCallback(int currentCallCount) throws Exception {
waitForCallback(currentCallCount, 1);
}
public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
throws Exception {
mCallbackHelper.waitForCallback(currentCallCount, numberOfCallsToWaitFor,
DEFAULT_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
mCallCount += numberOfCallsToWaitFor;
}
public int getCallCount() {
return mCallCount;
}
public Set<Long> cloneCaptureContentIds() {
return new HashSet<Long>(mCapturedContentIds);
}
public int[] getCallbacks() {
int[] result = new int[mCallbacks.size()];
int index = 0;
for (Integer c : mCallbacks) result[index++] = c;
return result;
}
// Use our own call count to avoid unexpected callback issue.
private int mCallCount;
private volatile Set<Long> mCapturedContentIds;
private volatile FrameSession mParentFrame;
private volatile ContentCaptureData mCapturedContent;
private volatile ContentCaptureData mUpdatedContent;
private volatile FrameSession mCurrentFrameSession;
private volatile FrameSession mRemovedSession;
private volatile long[] mRemovedIds;
private volatile ArrayList<Integer> mCallbacks = new ArrayList<Integer>();
private CallbackHelper mCallbackHelper = new CallbackHelper();
}
private final static String MAIN_FRAME_FILE = "/main_frame.html";
private final static String SECOND_PAGE = "/second_page.html";
@Rule
public AwActivityTestRule mRule = new AwActivityTestRule();
private TestWebServer mWebServer;
private TestAwContentsClient mContentsClient;
private AwContents mAwContents;
private AwTestContainerView mContainerView;
private TestAwContentCaptureConsumer mConsumer;
private TestAwContentCatpureController mController;
private void loadUrlSync(String url) {
try {
mRule.loadUrlSync(
mContainerView.getAwContents(), mContentsClient.getOnPageFinishedHelper(), url);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String executeJavaScriptAndWaitForResult(String code) throws Throwable {
return mRule.executeJavaScriptAndWaitForResult(
mContainerView.getAwContents(), mContentsClient, code);
}
@Before
public void setUp() throws Exception {
mWebServer = TestWebServer.start();
mContentsClient = new TestAwContentsClient();
mContainerView = mRule.createAwTestContainerViewOnMainSync(mContentsClient);
mAwContents = mContainerView.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mController = new TestAwContentCatpureController();
mConsumer = new TestAwContentCaptureConsumer(mAwContents.getWebContents());
mAwContents.setContentCaptureConsumer(mConsumer);
});
}
private void insertElement(String id, String content) {
String script = "var place_holder = document.getElementById('place_holder');"
+ "place_holder.insertAdjacentHTML('beforebegin', '<p id=\\'" + id + "\\'>"
+ content + "</p>');";
runScript(script);
}
private void setInnerHTML(String id, String content) {
String script = "var el = document.getElementById('" + id + "');"
+ "el.innerHTML='" + content + "';";
runScript(script);
}
private void removeElement(String id) {
String script = "var el = document.getElementById('" + id + "');"
+ "document.body.removeChild(el);";
runScript(script);
}
private void runScript(String script) {
try {
executeJavaScriptAndWaitForResult(script);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private void destroyAwContents() {
TestThreadUtils.runOnUiThreadBlocking(() -> { mAwContents.destroy(); });
}
private void scrollToBottom() {
TestThreadUtils.runOnUiThreadBlocking(
() -> { mContainerView.scrollTo(0, mContainerView.getHeight()); });
}
private void changeContent(String id, String content) {
String script = "var el = document.getElementById('" + id + "');"
+ "el.firstChild.textContent = '" + content + "';";
runScript(script);
}
private void scrollToTop() {
TestThreadUtils.runOnUiThreadBlocking(() -> { mContainerView.scrollTo(0, 0); });
}
private static void verifyFrame(Long expectedId, String expectedUrl, ContentCaptureData result)
throws Exception {
if (expectedId == null || expectedId.longValue() == 0) {
Assert.assertNotEquals(0, result.getId());
} else {
Assert.assertEquals(expectedId.longValue(), result.getId());
}
Assert.assertEquals(expectedUrl, result.getValue());
Assert.assertFalse(result.getBounds().isEmpty());
}
private static void verifyFrameSession(FrameSession expected, FrameSession result)
throws Exception {
if (expected == null && (result == null || result.isEmpty())) return;
Assert.assertEquals(expected.size(), result.size());
for (int i = 0; i < expected.size(); i++)
verifyFrame(expected.get(i).getId(), expected.get(i).getValue(), result.get(i));
}
private static void verifyContent(Set<String> expectedContent, Set<Long> unexpectedIds,
Set<Long> expectedIds, ContentCaptureData result) throws Exception {
Assert.assertEquals(expectedContent.size(), result.getChildren().size());
if (expectedIds != null) {
Assert.assertEquals(expectedIds.size(), result.getChildren().size());
}
for (ContentCaptureData child : result.getChildren()) {
Assert.assertTrue(expectedContent.contains(child.getValue()));
expectedContent.remove(child.getValue());
if (unexpectedIds != null) {
Assert.assertFalse(unexpectedIds.contains(child.getId()));
}
if (expectedIds != null) {
Assert.assertTrue(expectedIds.contains(child.getId()));
}
Assert.assertFalse(child.getBounds().isEmpty());
}
Assert.assertTrue(expectedContent.isEmpty());
}
private static void verifyCapturedContent(FrameSession expectedParentSession,
Long expectedFrameId, String expectedUrl, Set<String> expectedContent,
Set<Long> unexpectedContentIds, FrameSession parentResult, ContentCaptureData result)
throws Exception {
verifyFrameSession(expectedParentSession, parentResult);
verifyFrame(expectedFrameId, expectedUrl, result);
verifyContent(expectedContent, unexpectedContentIds, null, result);
}
private static void verifyUpdatedContent(FrameSession expectedParentSession,
Long expectedFrameId, String expectedUrl, Set<String> expectedContent,
Set<Long> expectedContentIds, FrameSession parentResult, ContentCaptureData result)
throws Exception {
verifyFrameSession(expectedParentSession, parentResult);
verifyFrame(expectedFrameId, expectedUrl, result);
verifyContent(expectedContent, null, expectedContentIds, result);
}
private static void verifyRemovedIds(Set<Long> expectedIds, long[] result) throws Exception {
Assert.assertEquals(expectedIds.size(), result.length);
Set<Long> resultSet = new HashSet<Long>(result.length);
for (long id : result) resultSet.add(id);
Assert.assertTrue(expectedIds.containsAll(resultSet));
}
private static void verifyRemovedContent(Long expectedFrameId, String expectedUrl,
Set<Long> expectedIds, FrameSession resultFrame, long[] result) throws Exception {
Assert.assertEquals(1, resultFrame.size());
verifyFrame(expectedFrameId, expectedUrl, resultFrame.get(0));
verifyRemovedIds(expectedIds, result);
}
private static void verifyCallbacks(int[] expectedCallbacks, int[] results) {
Assert.assertArrayEquals(expectedCallbacks, results);
}
private void runAndWaitForCallback(final Runnable testCase) throws Throwable {
runAndWaitForCallback(testCase, 1);
}
private void runAndWaitForCallback(final Runnable testCase, int numberOfCallsToWaitFor)
throws Throwable {
int callCount = mConsumer.getCallCount();
mConsumer.reset();
testCase.run();
mConsumer.waitForCallback(callCount, numberOfCallsToWaitFor);
}
private FrameSession createFrameSession(ContentCaptureData data) {
FrameSession session = new FrameSession(1);
ContentCaptureData c = data;
Rect r = c.getBounds();
session.add(ContentCaptureData.createContentCaptureData(
null, c.getId(), c.getValue(), r.left, r.top, r.width(), r.height()));
return session;
}
private FrameSession createFrameSession(String url) {
FrameSession session = new FrameSession(1);
session.add(ContentCaptureData.createContentCaptureData(null, 0, url, 0, 0, 0, 0));
return session;
}
private FrameSession createFrameSession(ContentCaptureData... frames) {
FrameSession result = new FrameSession(frames.length);
for (ContentCaptureData f : frames) result.addAll(createFrameSession(f));
return result;
}
@After
public void tearDown() {
mWebServer.shutdown();
}
private static Set<String> toStringSet(String... strings) {
Set<String> result = new HashSet<String>();
for (String s : strings) result.add(s);
return result;
}
private static Set<Long> toLongSet(Long... longs) {
Set<Long> result = new HashSet<Long>();
for (Long s : longs) result.add(s);
return result;
}
private static int[] toIntArray(int... callbacks) {
return callbacks;
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testSingleFrame() throws Throwable {
final String response = "<html><head></head><body>"
+ "<div id='place_holder'>"
+ "<p style=\"height: 100vh\">Hello</p>"
+ "<p>world</p>"
+ "</body></html>";
final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null);
runAndWaitForCallback(() -> { loadUrlSync(url); });
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
Long frameId = null;
Set<Long> capturedContentIds = null;
// Verify only on-screen content is captured.
verifyCapturedContent(null, frameId, url, toStringSet("Hello"), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
frameId = Long.valueOf(mConsumer.getCapturedContent().getId());
capturedContentIds = mConsumer.cloneCaptureContentIds();
runAndWaitForCallback(() -> { scrollToBottom(); });
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
verifyCapturedContent(null, frameId, url, toStringSet("world"), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
final String newContentId = "new_content_id";
final String newContent = "new content";
runAndWaitForCallback(() -> {
insertElement(newContentId, newContent);
scrollToTop();
});
// Only new content is captured, the content that has been captured will not be captured
// again.
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
verifyCapturedContent(null, frameId, url, toStringSet(newContent), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
// Changed previous added element, this will trigger remove/capture events.
long removedContentId = mConsumer.getCapturedContent().getChildren().get(0).getId();
final String newContent2 = "new content 2";
capturedContentIds = mConsumer.cloneCaptureContentIds();
runAndWaitForCallback(() -> { setInnerHTML(newContentId, newContent2); }, 2);
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_REMOVED,
TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
verifyRemovedContent(frameId, url, toLongSet(removedContentId),
mConsumer.getCurrentFrameSession(), mConsumer.getRemovedIds());
verifyCapturedContent(null, frameId, url, toStringSet(newContent2), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
// Remove the element.
removedContentId = mConsumer.getCapturedContent().getChildren().get(0).getId();
capturedContentIds = mConsumer.cloneCaptureContentIds();
runAndWaitForCallback(() -> { removeElement(newContentId); });
verifyCallbacks(
toIntArray(TestAwContentCaptureConsumer.CONTENT_REMOVED), mConsumer.getCallbacks());
verifyRemovedContent(frameId, url, toLongSet(removedContentId),
mConsumer.getCurrentFrameSession(), mConsumer.getRemovedIds());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testChangeContent() throws Throwable {
final String response = "<html><head></head><body>"
+ "<div id='editable_id'>Hello</div>"
+ "</div></body></html>";
final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null);
runAndWaitForCallback(() -> { loadUrlSync(url); });
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
Long frameId = null;
Set<Long> capturedContentIds = null;
// Verify only on-screen content is captured.
verifyCapturedContent(null, frameId, url, toStringSet("Hello"), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
// Change the content, we shall get content updated callback.
frameId = Long.valueOf(mConsumer.getCapturedContent().getId());
capturedContentIds = mConsumer.cloneCaptureContentIds();
final String changeContent = "Hello world";
runAndWaitForCallback(() -> { changeContent("editable_id", changeContent); });
verifyCallbacks(
toIntArray(TestAwContentCaptureConsumer.CONTENT_UPDATED), mConsumer.getCallbacks());
verifyUpdatedContent(null, frameId, url, toStringSet(changeContent), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getUpdatedContent());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testRemoveSession() throws Throwable {
final String response = "<html><head></head><body>"
+ "<div id='editable_id'>Hello</div>"
+ "</div></body></html>";
final String response2 = "<html><head></head><body>"
+ "<div id='editable_id'>World</div>"
+ "</div></body></html>";
final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null);
final String url2 = mWebServer.setResponse(SECOND_PAGE, response2, null);
runAndWaitForCallback(() -> { loadUrlSync(url); });
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
Long frameId = null;
Set<Long> capturedContentIds = null;
verifyCapturedContent(null, frameId, url, toStringSet("Hello"), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
// Keep a copy of current session to verify it removed later.
FrameSession removedSession = createFrameSession(mConsumer.getCapturedContent());
capturedContentIds = mConsumer.cloneCaptureContentIds();
int[] expectedCallbacks = toIntArray(TestAwContentCaptureConsumer.SESSION_REMOVED,
TestAwContentCaptureConsumer.CONTENT_CAPTURED);
runAndWaitForCallback(() -> { loadUrlSync(url2); }, expectedCallbacks.length);
verifyCallbacks(expectedCallbacks, mConsumer.getCallbacks());
verifyCapturedContent(null, frameId, url2, toStringSet("World"), capturedContentIds,
mConsumer.getParentFrame(), mConsumer.getCapturedContent());
// Verify previous session has been removed.
verifyFrameSession(removedSession, mConsumer.getRemovedSession());
// Keep a copy of current session to verify it removed later.
removedSession = createFrameSession(mConsumer.getCapturedContent());
runAndWaitForCallback(() -> { destroyAwContents(); });
verifyCallbacks(
toIntArray(TestAwContentCaptureConsumer.SESSION_REMOVED), mConsumer.getCallbacks());
verifyFrameSession(removedSession, mConsumer.getRemovedSession());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testRemoveIframe() throws Throwable {
final String subFrame = "<html><head></head><body>"
+ "<div id='editable_id'>Hello</div>"
+ "</div></body></html>";
final String subFrameUrl = mWebServer.setResponse(SECOND_PAGE, subFrame, null);
final String mainFrame = "<html><head></head><body>"
+ "<iframe id='sub_frame_id' src='" + subFrameUrl + "'></iframe></body></html>";
final String mainFrameUrl = mWebServer.setResponse(MAIN_FRAME_FILE, mainFrame, null);
runAndWaitForCallback(() -> { loadUrlSync(mainFrameUrl); });
verifyCallbacks(toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED),
mConsumer.getCallbacks());
FrameSession expectedParentFrameSession = createFrameSession(mainFrameUrl);
Long frameId = null;
verifyCapturedContent(expectedParentFrameSession, frameId, subFrameUrl,
toStringSet("Hello"), null, mConsumer.getParentFrame(),
mConsumer.getCapturedContent());
FrameSession removedSession = createFrameSession(
mConsumer.getCapturedContent(), mConsumer.getParentFrame().get(0));
runAndWaitForCallback(() -> {
runScript("var frame = document.getElementById('sub_frame_id');"
+ "frame.parentNode.removeChild(frame);");
});
verifyFrameSession(removedSession, mConsumer.getRemovedSession());
}
}