| // Copyright 2015 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.android_webview.test; |
| |
| import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS; |
| import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.util.Base64; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.SmallTest; |
| |
| import org.junit.Assert; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.UseParametersRunnerFactory; |
| |
| import org.chromium.android_webview.AwContents; |
| import org.chromium.android_webview.AwContents.VisualStateCallback; |
| import org.chromium.android_webview.AwContentsClient; |
| import org.chromium.android_webview.test.util.CommonResources; |
| import org.chromium.android_webview.test.util.GraphicsTestUtils; |
| import org.chromium.android_webview.test.util.JSUtils; |
| import org.chromium.android_webview.test.util.JavascriptEventObserver; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.base.task.TaskTraits; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.components.embedder_support.util.WebResourceResponseInfo; |
| import org.chromium.content_public.browser.JavascriptInjector; |
| import org.chromium.content_public.browser.LoadUrlParams; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.FilterInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** Visual state related tests. */ |
| @RunWith(Parameterized.class) |
| @UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class) |
| public class VisualStateTest extends AwParameterizedTest { |
| @Rule public AwActivityTestRule mActivityTestRule; |
| |
| private static final String WAIT_FOR_JS_TEST_URL = |
| "file:///android_asset/visual_state_waits_for_js_test.html"; |
| private static final String WAIT_FOR_JS_DETACHED_TEST_URL = |
| "file:///android_asset/visual_state_waits_for_js_detached_test.html"; |
| private static final String ON_PAGE_COMMIT_VISIBLE_TEST_URL = |
| "file:///android_asset/visual_state_on_page_commit_visible_test.html"; |
| private static final String FULLSCREEN_TEST_URL = |
| "file:///android_asset/visual_state_during_fullscreen_test.html"; |
| private static final String UPDATE_COLOR_CONTROL_ID = "updateColorControl"; |
| private static final String ENTER_FULLSCREEN_CONTROL_ID = "enterFullscreenControl"; |
| |
| private TestAwContentsClient mContentsClient = new TestAwContentsClient(); |
| private AwTestContainerView mTestView; |
| |
| private static class DelayedInputStream extends FilterInputStream { |
| private CountDownLatch mLatch = new CountDownLatch(1); |
| |
| DelayedInputStream(InputStream in) { |
| super(in); |
| } |
| |
| @Override |
| @SuppressWarnings("Finally") |
| public int read() throws IOException { |
| try { |
| mLatch.await(); |
| } finally { |
| return super.read(); |
| } |
| } |
| |
| @Override |
| @SuppressWarnings("Finally") |
| public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { |
| try { |
| mLatch.await(); |
| } finally { |
| return super.read(buffer, byteOffset, byteCount); |
| } |
| } |
| |
| public void allowReads() { |
| mLatch.countDown(); |
| } |
| } |
| |
| private static class SlowBlueImage extends WebResourceResponseInfo { |
| // This image delays returning data for 1 (scaled) second in order to simlate a slow network |
| // connection. |
| public static final long IMAGE_LOADING_DELAY_MS = scaleTimeout(1000); |
| |
| public SlowBlueImage() { |
| super( |
| "image/png", |
| "utf-8", |
| new DelayedInputStream( |
| new ByteArrayInputStream( |
| Base64.decode( |
| CommonResources.BLUE_PNG_BASE64, Base64.DEFAULT)))); |
| } |
| |
| @Override |
| public InputStream getData() { |
| final DelayedInputStream stream = (DelayedInputStream) super.getData(); |
| PostTask.postDelayedTask( |
| TaskTraits.UI_DEFAULT, () -> stream.allowReads(), IMAGE_LOADING_DELAY_MS); |
| return stream; |
| } |
| } |
| |
| public VisualStateTest(AwSettingsMutation param) { |
| this.mActivityTestRule = new AwActivityTestRule(param.getMutation()); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testVisualStateCallbackIsReceived() throws Throwable { |
| mTestView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = mTestView.getAwContents(); |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| CommonResources.ABOUT_HTML, |
| "text/html", |
| false); |
| final CallbackHelper ch = new CallbackHelper(); |
| final int chCount = ch.getCallCount(); |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| final long requestId = |
| 0x123456789abcdef0L; // ensure requestId is not truncated. |
| awContents.insertVisualStateCallback( |
| requestId, |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Assert.assertEquals(requestId, id); |
| ch.notifyCalled(); |
| } |
| }); |
| }); |
| ch.waitForCallback(chCount); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testVisualStateCallbackWaitsForContentsToBeOnScreen() throws Throwable { |
| // This test loads a page with a blue background color. It then waits for the DOM tree |
| // in blink to contain the contents of the blue page (which happens when the onPageFinished |
| // event is received). It then flushes the contents and verifies that the blue page |
| // background color is drawn when the flush callback is received. |
| final LoadUrlParams bluePageUrl = createTestPageUrl("blue"); |
| final CountDownLatch testFinishedSignal = new CountDownLatch(1); |
| |
| final AtomicReference<AwContents> awContentsRef = new AtomicReference<>(); |
| final long requestId = 10; |
| final var visualStateCallback = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Assert.assertEquals(requestId, id); |
| Bitmap blueScreenshot = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 1, 1); |
| Assert.assertEquals(Color.BLUE, blueScreenshot.getPixel(0, 0)); |
| testFinishedSignal.countDown(); |
| } |
| }; |
| mTestView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync( |
| new TestAwContentsClient() { |
| @Override |
| public void onPageFinished(String url) { |
| if (bluePageUrl.getUrl().equals(url)) { |
| awContentsRef |
| .get() |
| .insertVisualStateCallback( |
| requestId, visualStateCallback); |
| } |
| } |
| }); |
| final AwContents awContents = mTestView.getAwContents(); |
| awContentsRef.set(awContents); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| awContents.setBackgroundColor(Color.RED); |
| awContents.loadUrl(bluePageUrl); |
| |
| // We have just loaded the blue page, but the graphics pipeline is |
| // asynchronous so at this point the WebView still draws red, ie. the |
| // View background color. |
| // Only when the flush callback is received will we know for certain |
| // that the blue page contents are on screen. |
| Bitmap redScreenshot = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 1, 1); |
| Assert.assertEquals(Color.RED, redScreenshot.getPixel(0, 0)); |
| }); |
| |
| Assert.assertTrue( |
| testFinishedSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @SkipMutations(reason = "This test depends on AwSettings.setImagesEnabled(true)") |
| public void testOnPageCommitVisible() throws Throwable { |
| // This test loads a page with a blue background color. It then waits for the DOM tree |
| // in blink to contain the contents of the blue page (which happens when the onPageFinished |
| // event is received). It then flushes the contents and verifies that the blue page |
| // background color is drawn when the flush callback is received. |
| final CountDownLatch testFinishedSignal = new CountDownLatch(1); |
| final CountDownLatch pageCommitCallbackOccurred = new CountDownLatch(1); |
| |
| final AtomicReference<AwContents> awContentsRef = new AtomicReference<>(); |
| final var visualStateCallback = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Bitmap bitmap = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 256, 256); |
| Assert.assertEquals(Color.BLUE, bitmap.getPixel(128, 128)); |
| testFinishedSignal.countDown(); |
| } |
| }; |
| mTestView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync( |
| new TestAwContentsClient() { |
| @Override |
| public void onPageCommitVisible(String url) { |
| Bitmap bitmap = |
| GraphicsTestUtils.drawAwContents( |
| awContentsRef.get(), 256, 256); |
| Assert.assertEquals(Color.GREEN, bitmap.getPixel(128, 128)); |
| pageCommitCallbackOccurred.countDown(); |
| } |
| |
| @Override |
| public WebResourceResponseInfo shouldInterceptRequest( |
| AwWebResourceRequest request) { |
| if (request.url.equals("intercepted://blue.png")) { |
| try { |
| return new SlowBlueImage(); |
| } catch (Throwable t) { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public void onPageFinished(String url) { |
| super.onPageFinished(url); |
| awContentsRef |
| .get() |
| .insertVisualStateCallback(10, visualStateCallback); |
| } |
| }); |
| |
| final AwContents awContents = mTestView.getAwContents(); |
| awContentsRef.set(awContents); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| awContents.setBackgroundColor(Color.RED); |
| |
| awContents.loadUrl(new LoadUrlParams(ON_PAGE_COMMIT_VISIBLE_TEST_URL)); |
| |
| // We have just loaded the blue page, but the graphics pipeline is |
| // asynchronous so at this point the WebView still draws red, ie. the |
| // View background color. |
| // Only when the flush callback is received will we know for certain |
| // that the blue page contents are on screen. |
| Bitmap bitmap = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 256, 256); |
| Assert.assertEquals(Color.RED, bitmap.getPixel(128, 128)); |
| }); |
| |
| Assert.assertTrue( |
| pageCommitCallbackOccurred.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| Assert.assertTrue( |
| testFinishedSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testVisualStateCallbackWaitsForJs() throws Throwable { |
| // This test checks that when a VisualStateCallback completes the results of executing |
| // any block of JS prior to the time at which the callback was inserted will be visible |
| // in the next draw. For that it loads a page that changes the background color of |
| // the page from JS when a button is clicked. |
| final CountDownLatch readyToUpdateColor = new CountDownLatch(1); |
| final CountDownLatch testFinishedSignal = new CountDownLatch(1); |
| |
| final AtomicReference<AwContents> awContentsRef = new AtomicReference<>(); |
| final var visualStateCallback = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Bitmap blueScreenshot = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 100, 100); |
| Assert.assertEquals(Color.BLUE, blueScreenshot.getPixel(50, 50)); |
| readyToUpdateColor.countDown(); |
| } |
| }; |
| TestAwContentsClient awContentsClient = |
| new TestAwContentsClient() { |
| @Override |
| public void onPageFinished(String url) { |
| super.onPageFinished(url); |
| awContentsRef.get().insertVisualStateCallback(10, visualStateCallback); |
| } |
| }; |
| mTestView = mActivityTestRule.createAwTestContainerViewOnMainSync(awContentsClient); |
| final AwContents awContents = mTestView.getAwContents(); |
| awContentsRef.set(awContents); |
| final WebContents webContents = mTestView.getWebContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| // JS will notify this observer once it has changed the background color of the page. |
| final JavascriptEventObserver jsObserver = new JavascriptEventObserver(); |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> jsObserver.register(awContents.getWebContents(), "jsObserver")); |
| |
| mActivityTestRule.loadUrlSync( |
| awContents, awContentsClient.getOnPageFinishedHelper(), WAIT_FOR_JS_TEST_URL); |
| |
| Assert.assertTrue( |
| readyToUpdateColor.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| JSUtils.clickNodeWithUserGesture(webContents, UPDATE_COLOR_CONTROL_ID); |
| Assert.assertTrue(jsObserver.waitForEvent(WAIT_TIMEOUT_MS)); |
| |
| final var visualStateCallback2 = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Bitmap redScreenshot = |
| GraphicsTestUtils.drawAwContents(awContents, 100, 100); |
| Assert.assertEquals(Color.RED, redScreenshot.getPixel(50, 50)); |
| testFinishedSignal.countDown(); |
| } |
| }; |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> awContents.insertVisualStateCallback(20, visualStateCallback2)); |
| |
| Assert.assertTrue( |
| testFinishedSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testVisualStateCallbackFromJsDuringFullscreenTransitions() throws Throwable { |
| // This test checks that VisualStateCallbacks are delivered correctly during |
| // fullscreen transitions. It loads a page, clicks a button to enter fullscreen, |
| // then inserts a VisualStateCallback once notified from JS and verifies that when the |
| // callback is received the fullscreen contents are rendered correctly in the next draw. |
| final CountDownLatch readyToEnterFullscreenSignal = new CountDownLatch(1); |
| final CountDownLatch testFinishedSignal = new CountDownLatch(1); |
| |
| final AtomicReference<AwContents> awContentsRef = new AtomicReference<>(); |
| final var visualStateCallback = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Bitmap blueScreenshot = |
| GraphicsTestUtils.drawAwContents(awContentsRef.get(), 100, 100); |
| Assert.assertEquals(Color.BLUE, blueScreenshot.getPixel(50, 50)); |
| readyToEnterFullscreenSignal.countDown(); |
| } |
| }; |
| final FullScreenVideoTestAwContentsClient awContentsClient = |
| new FullScreenVideoTestAwContentsClient( |
| mActivityTestRule.getActivity(), |
| mActivityTestRule.isHardwareAcceleratedTest()) { |
| @Override |
| public void onPageFinished(String url) { |
| super.onPageFinished(url); |
| awContentsRef.get().insertVisualStateCallback(10, visualStateCallback); |
| } |
| }; |
| mTestView = mActivityTestRule.createAwTestContainerViewOnMainSync(awContentsClient); |
| final AwContents awContents = mTestView.getAwContents(); |
| awContentsRef.set(awContents); |
| final WebContents webContents = mTestView.getWebContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| awContents.getSettings().setFullscreenSupported(true); |
| |
| // JS will notify this observer once it has entered fullscreen. |
| final JavascriptEventObserver jsObserver = new JavascriptEventObserver(); |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> jsObserver.register(awContents.getWebContents(), "jsObserver")); |
| |
| mActivityTestRule.loadUrlSync( |
| awContents, awContentsClient.getOnPageFinishedHelper(), FULLSCREEN_TEST_URL); |
| |
| Assert.assertTrue( |
| readyToEnterFullscreenSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| JSUtils.clickNodeWithUserGesture(webContents, ENTER_FULLSCREEN_CONTROL_ID); |
| Assert.assertTrue(jsObserver.waitForEvent(WAIT_TIMEOUT_MS)); |
| |
| final var visualStateCallback2 = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| // NOTE: We cannot use drawAwContents here because |
| // the web contents are rendered into the custom |
| // view while in fullscreen. |
| Bitmap redScreenshot = |
| GraphicsTestUtils.drawView( |
| awContentsClient.getCustomView(), 100, 100); |
| Assert.assertEquals(Color.RED, redScreenshot.getPixel(50, 50)); |
| testFinishedSignal.countDown(); |
| } |
| }; |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> awContents.insertVisualStateCallback(20, visualStateCallback2)); |
| |
| Assert.assertTrue( |
| testFinishedSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } |
| |
| private AwTestContainerView createDetachedTestContainerViewOnMainSync( |
| final AwContentsClient awContentsClient) { |
| return TestThreadUtils.runOnUiThreadBlockingNoException( |
| () -> { |
| AwTestContainerView detachedView = |
| mActivityTestRule.createDetachedAwTestContainerView(awContentsClient); |
| detachedView.setClipBounds(new Rect(0, 0, 100, 100)); |
| detachedView.measure(100, 100); |
| detachedView.layout(0, 0, 100, 100); |
| return detachedView; |
| }); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testVisualStateCallbackWhenContainerViewDetached() throws Throwable { |
| final CountDownLatch testFinishedSignal = new CountDownLatch(1); |
| |
| final TestAwContentsClient awContentsClient = new TestAwContentsClient(); |
| mTestView = createDetachedTestContainerViewOnMainSync(awContentsClient); |
| final AwContents awContents = mTestView.getAwContents(); |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| // JS will notify this observer once it has changed the background color of the page. |
| final var visualStateCallback = |
| new VisualStateCallback() { |
| @Override |
| public void onComplete(long id) { |
| Bitmap redScreenshot = |
| GraphicsTestUtils.drawAwContents(awContents, 100, 100); |
| Assert.assertEquals(Color.RED, redScreenshot.getPixel(50, 50)); |
| testFinishedSignal.countDown(); |
| } |
| }; |
| final Object pageChangeNotifier = |
| new Object() { |
| public void onPageChanged() { |
| PostTask.postTask( |
| TaskTraits.UI_DEFAULT, |
| () -> |
| awContents.insertVisualStateCallback( |
| 20, visualStateCallback)); |
| } |
| }; |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| JavascriptInjector.fromWebContents(awContents.getWebContents(), false) |
| .addPossiblyUnsafeInterface( |
| pageChangeNotifier, "pageChangeNotifier", null); |
| awContents.loadUrl(WAIT_FOR_JS_DETACHED_TEST_URL); |
| }); |
| |
| Assert.assertTrue( |
| testFinishedSignal.await( |
| AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } |
| |
| private static final LoadUrlParams createTestPageUrl(String backgroundColor) { |
| return LoadUrlParams.createLoadDataParams( |
| "<html><body bgcolor=" + backgroundColor + "></body></html>", "text/html", false); |
| } |
| } |