diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt | 660 |
1 files changed, 660 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt new file mode 100644 index 0000000000..65a07d384d --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt @@ -0,0 +1,660 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test + +import android.graphics.SurfaceTexture +import android.net.Uri +import android.view.PointerIcon +import android.view.Surface +import androidx.annotation.AnyThread +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports +import org.json.JSONObject +import org.junit.Assume.assumeThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.geckoview.* // ktlint-disable no-wildcard-imports +import org.mozilla.geckoview.ContentBlocking.CookieBannerMode +import org.mozilla.geckoview.GeckoDisplay.SurfaceInfo +import org.mozilla.geckoview.GeckoSession.ContentDelegate +import org.mozilla.geckoview.GeckoSession.NavigationDelegate +import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest +import org.mozilla.geckoview.GeckoSession.ProgressDelegate +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay +import java.io.ByteArrayInputStream + +@RunWith(AndroidJUnit4::class) +@MediumTest +class ContentDelegateTest : BaseSessionTest() { + @Test fun titleChange() { + mainSession.loadTestPath(TITLE_CHANGE_HTML_PATH) + + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 2) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat( + "Title should match", + title, + equalTo(forEachCall("Title1", "Title2")), + ) + } + }) + } + + @Test fun openInAppRequest() { + // Testing WebResponse behavior + val data = "Hello, World.".toByteArray() + val fileHeader = "attachment; filename=\"hello-world.txt\"" + val requestExternal = true + val skipConfirmation = true + var response = WebResponse.Builder(HELLO_HTML_PATH) + .statusCode(200) + .body(ByteArrayInputStream(data)) + .addHeader("Content-Type", "application/txt") + .addHeader("Content-Length", data.size.toString()) + .addHeader("Content-Disposition", fileHeader) + .requestExternalApp(requestExternal) + .skipConfirmation(skipConfirmation) + .build() + assertThat( + "Filename matches as expected", + response.headers["Content-Disposition"], + equalTo(fileHeader), + ) + assertThat( + "Request external response matches as expected.", + requestExternal, + equalTo(response.requestExternalApp), + ) + assertThat( + "Skipping the confirmation matches as expected.", + skipConfirmation, + equalTo(response.skipConfirmation), + ) + } + + @Test fun downloadOneRequest() { + // disable test on pgo for frequently failing Bug 1543355 + assumeThat(sessionRule.env.isDebugBuild, equalTo(true)) + + mainSession.loadTestPath(DOWNLOAD_HTML_PATH) + + sessionRule.waitUntilCalled(object : NavigationDelegate, ContentDelegate { + + @AssertCalled(count = 2) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return null + } + + @AssertCalled(false) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + + @AssertCalled(count = 1) + override fun onExternalResponse(session: GeckoSession, response: WebResponse) { + assertThat("Uri should start with data:", response.uri, startsWith("blob:")) + assertThat("We should download the thing", String(response.body?.readBytes()!!), equalTo("Downloaded Data")) + // The headers below are special headers that we try to get for responses of any kind (http, blob, etc.) + // Note the case of the header keys. In the WebResponse object, all of them are lower case. + assertThat("Content type should match", response.headers.get("content-type"), equalTo("text/plain")) + assertThat("Content length should be non-zero", response.headers.get("Content-Length")!!.toLong(), greaterThan(0L)) + assertThat("Filename should match", response.headers.get("cONTent-diSPOsiTion"), equalTo("attachment; filename=\"download.txt\"")) + assertThat("Request external response should not be set.", response.requestExternalApp, equalTo(false)) + assertThat("Should not skip the confirmation on a regular download.", response.skipConfirmation, equalTo(false)) + } + }) + } + + @IgnoreCrash + @Test + fun crashContent() { + // TODO: bug 1710940 + assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) + + mainSession.loadUri(CONTENT_CRASH_URL) + mainSession.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onCrash(session: GeckoSession) { + assertThat( + "Session should be closed after a crash", + session.isOpen, + equalTo(false), + ) + } + }) + + // Recover immediately + mainSession.open() + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitUntilCalled(object : ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + @IgnoreCrash + @WithDisplay(width = 10, height = 10) + @Test + fun crashContent_tapAfterCrash() { + // TODO: bug 1710940 + assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) + + mainSession.delegateUntilTestEnd(object : ContentDelegate { + override fun onCrash(session: GeckoSession) { + mainSession.open() + mainSession.loadTestPath(HELLO_HTML_PATH) + } + }) + + mainSession.synthesizeTap(5, 5) + mainSession.loadUri(CONTENT_CRASH_URL) + mainSession.waitForPageStop() + + mainSession.synthesizeTap(5, 5) + mainSession.reload() + mainSession.waitForPageStop() + } + + @AnyThread + fun killAllContentProcesses() { + val contentProcessPids = sessionRule.getAllSessionPids() + for (pid in contentProcessPids) { + sessionRule.killContentProcess(pid) + } + } + + @IgnoreCrash + @Test + fun killContent() { + killAllContentProcesses() + mainSession.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onKill(session: GeckoSession) { + assertThat( + "Session should be closed after being killed", + session.isOpen, + equalTo(false), + ) + } + }) + + mainSession.open() + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitUntilCalled(object : ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + private fun goFullscreen() { + sessionRule.setPrefsUntilTestEnd(mapOf("full-screen-api.allow-trusted-requests-only" to false)) + mainSession.loadTestPath(FULLSCREEN_PATH) + mainSession.waitForPageStop() + val promise = mainSession.evaluatePromiseJS("document.querySelector('#fullscreen').requestFullscreen()") + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) { + assertThat("Div went fullscreen", fullScreen, equalTo(true)) + } + }) + promise.value + } + + private fun waitForFullscreenExit() { + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) { + assertThat("Div left fullscreen", fullScreen, equalTo(false)) + } + }) + } + + @Test fun fullscreen() { + goFullscreen() + val promise = mainSession.evaluatePromiseJS("document.exitFullscreen()") + waitForFullscreenExit() + promise.value + } + + @Test fun sessionExitFullscreen() { + goFullscreen() + mainSession.exitFullScreen() + waitForFullscreenExit() + } + + @Test fun firstComposite() { + val display = mainSession.acquireDisplay() + val texture = SurfaceTexture(0) + texture.setDefaultBufferSize(100, 100) + val surface = Surface(texture) + display.surfaceChanged(SurfaceInfo.Builder(surface).size(100, 100).build()) + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onFirstComposite(session: GeckoSession) { + } + }) + display.surfaceDestroyed() + display.surfaceChanged(SurfaceInfo.Builder(surface).size(100, 100).build()) + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onFirstComposite(session: GeckoSession) { + } + }) + display.surfaceDestroyed() + mainSession.releaseDisplay(display) + } + + @WithDisplay(width = 10, height = 10) + @Test + fun firstContentfulPaint() { + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onFirstContentfulPaint(session: GeckoSession) { + } + }) + } + + @Test fun webAppManifestPref() { + val initialState = sessionRule.runtime.settings.getWebManifestEnabled() + val jsToRun = "document.querySelector('link[rel=manifest]').relList.supports('manifest');" + + // Check pref'ed off + sessionRule.runtime.settings.setWebManifestEnabled(false) + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitForPageStop(mainSession) + + var result = equalTo(mainSession.evaluateJS(jsToRun) as Boolean) + + assertThat("Disabling pref makes relList.supports('manifest') return false", false, result) + + // Check pref'ed on + sessionRule.runtime.settings.setWebManifestEnabled(true) + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitForPageStop(mainSession) + + result = equalTo(mainSession.evaluateJS(jsToRun) as Boolean) + assertThat("Enabling pref makes relList.supports('manifest') return true", true, result) + + sessionRule.runtime.settings.setWebManifestEnabled(initialState) + } + + @Test fun webAppManifest() { + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitUntilCalled(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page load should succeed", success, equalTo(true)) + } + + @AssertCalled(count = 1) + override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) { + // These values come from the manifest at assets/www/manifest.webmanifest + assertThat("name should match", manifest.getString("name"), equalTo("App")) + assertThat("short_name should match", manifest.getString("short_name"), equalTo("app")) + assertThat("display should match", manifest.getString("display"), equalTo("standalone")) + + // The color here is "cadetblue" converted to #aarrggbb. + assertThat("theme_color should match", manifest.getString("theme_color"), equalTo("#ff5f9ea0")) + assertThat("background_color should match", manifest.getString("background_color"), equalTo("#eec0ffee")) + assertThat("start_url should match", manifest.getString("start_url"), endsWith("/assets/www/start/index.html")) + + val icon = manifest.getJSONArray("icons").getJSONObject(0) + + val iconSrc = Uri.parse(icon.getString("src")) + assertThat("icon should have a valid src", iconSrc, notNullValue()) + assertThat("icon src should be absolute", iconSrc.isAbsolute, equalTo(true)) + assertThat("icon should have sizes", icon.getString("sizes"), not(isEmptyOrNullString())) + assertThat("icon type should match", icon.getString("type"), equalTo("image/gif")) + } + }) + } + + @Test fun previewImage() { + mainSession.loadTestPath(METATAGS_PATH) + mainSession.waitUntilCalled(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onPreviewImage(session: GeckoSession, previewImageUrl: String) { + assertThat("Preview image should match", previewImageUrl, equalTo("https://test.com/og-image-url")) + } + }) + } + + @Test fun viewportFit() { + mainSession.loadTestPath(VIEWPORT_PATH) + mainSession.waitUntilCalled(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page load should succeed", success, equalTo(true)) + } + + @AssertCalled(count = 1) + override fun onMetaViewportFitChange(session: GeckoSession, viewportFit: String) { + assertThat("viewport-fit should match", viewportFit, equalTo("cover")) + } + }) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitUntilCalled(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page load should succeed", success, equalTo(true)) + } + + @AssertCalled(count = 1) + override fun onMetaViewportFitChange(session: GeckoSession, viewportFit: String) { + assertThat("viewport-fit should match", viewportFit, equalTo("auto")) + } + }) + } + + @Test fun closeRequest() { + if (!sessionRule.env.isAutomation) { + sessionRule.setPrefsUntilTestEnd(mapOf("dom.allow_scripts_to_close_windows" to true)) + } + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("window.close()") + mainSession.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onCloseRequest(session: GeckoSession) { + } + }) + } + + @Test fun windowOpenClose() { + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + val newSession = sessionRule.createClosedSession() + mainSession.delegateDuringNextWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return GeckoResult.fromValue(newSession) + } + }) + + mainSession.evaluateJS("const w = window.open('about:blank'); w.close()") + + newSession.waitUntilCalled(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onCloseRequest(session: GeckoSession) { + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + } + }) + } + + @Test fun cookieBannerDetectedEvent() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "cookiebanners.service.mode" to CookieBannerMode.COOKIE_BANNER_MODE_REJECT, + ), + ) + + val detectHandled = GeckoResult<Void>() + mainSession.delegateUntilTestEnd(object : GeckoSession.ContentDelegate { + override fun onCookieBannerDetected( + session: GeckoSession, + ) { + detectHandled.complete(null) + } + }) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + mainSession.triggerCookieBannerDetected() + + sessionRule.waitForResult(detectHandled) + } + + @Test fun cookieBannerHandledEvent() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "cookiebanners.service.mode" to CookieBannerMode.COOKIE_BANNER_MODE_REJECT, + ), + ) + + val handleHandled = GeckoResult<Void>() + mainSession.delegateUntilTestEnd(object : GeckoSession.ContentDelegate { + override fun onCookieBannerHandled( + session: GeckoSession, + ) { + handleHandled.complete(null) + } + }) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + mainSession.triggerCookieBannerHandled() + + sessionRule.waitForResult(handleHandled) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun setCursor() { + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.body.style.cursor = 'wait'") + mainSession.synthesizeMouseMove(50, 50) + + mainSession.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onPointerIconChange(session: GeckoSession, icon: PointerIcon) { + // PointerIcon has no compare method. + } + }) + + val delegate = mainSession.contentDelegate + mainSession.contentDelegate = null + mainSession.evaluateJS("document.body.style.cursor = 'text'") + for (i in 51..70) { + mainSession.synthesizeMouseMove(i, 50) + // No wait function since we remove content delegate. + mainSession.waitForJS("new Promise(resolve => window.setTimeout(resolve, 100))") + } + mainSession.contentDelegate = delegate + } + + /** + * Preferences to induce wanted behaviour. + */ + private fun setHangReportTestPrefs(timeout: Int = 20000) { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "dom.max_script_run_time" to 1, + "dom.max_chrome_script_run_time" to 1, + "dom.max_ext_content_script_run_time" to 1, + "dom.ipc.cpow.timeout" to 100, + "browser.hangNotification.waitPeriod" to timeout, + ), + ) + } + + /** + * With no delegate set, the default behaviour is to stop hung scripts. + */ + @NullDelegate(ContentDelegate::class) + @Test + fun stopHungProcessDefault() { + setHangReportTestPrefs() + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.delegateUntilTestEnd(object : ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat( + "The script did not complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Started"), + ) + } + }) + sessionRule.waitForPageStop(mainSession) + } + + /** + * With no overriding implementation for onSlowScript, the default behaviour is to stop hung + * scripts. + */ + @Test fun stopHungProcessNull() { + setHangReportTestPrefs() + sessionRule.delegateUntilTestEnd(object : ContentDelegate, ProgressDelegate { + // default onSlowScript returns null + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat( + "The script did not complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Started"), + ) + } + }) + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.waitForPageStop(mainSession) + } + + /** + * Test that, with a 'do nothing' delegate, the hung process completes after its delay + */ + @Test fun stopHungProcessDoNothing() { + setHangReportTestPrefs() + var scriptHungReportCount = 0 + sessionRule.delegateUntilTestEnd(object : ContentDelegate, ProgressDelegate { + @AssertCalled() + override fun onSlowScript(geckoSession: GeckoSession, scriptFileName: String): GeckoResult<SlowScriptResponse> { + scriptHungReportCount += 1 + return GeckoResult.fromValue(null) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("The delegate was informed of the hang repeatedly", scriptHungReportCount, greaterThan(1)) + assertThat( + "The script did complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Finished"), + ) + } + }) + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.waitForPageStop(mainSession) + } + + /** + * Test that the delegate is called and can stop a hung script + */ + @Test fun stopHungProcess() { + setHangReportTestPrefs() + sessionRule.delegateUntilTestEnd(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onSlowScript(geckoSession: GeckoSession, scriptFileName: String): GeckoResult<SlowScriptResponse> { + return GeckoResult.fromValue(SlowScriptResponse.STOP) + } + + @AssertCalled(count = 1, order = [2]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat( + "The script did not complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Started"), + ) + } + }) + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.waitForPageStop(mainSession) + } + + /** + * Test that the delegate is called and can continue executing hung scripts + */ + @Test fun stopHungProcessWait() { + setHangReportTestPrefs() + sessionRule.delegateUntilTestEnd(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onSlowScript(geckoSession: GeckoSession, scriptFileName: String): GeckoResult<SlowScriptResponse> { + return GeckoResult.fromValue(SlowScriptResponse.CONTINUE) + } + + @AssertCalled(count = 1, order = [2]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat( + "The script did complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Finished"), + ) + } + }) + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.waitForPageStop(mainSession) + } + + /** + * Test that the delegate is called and paused scripts re-notify after the wait period + */ + @Test fun stopHungProcessWaitThenStop() { + setHangReportTestPrefs(500) + var scriptWaited = false + sessionRule.delegateUntilTestEnd(object : ContentDelegate, ProgressDelegate { + @AssertCalled(count = 2, order = [1, 2]) + override fun onSlowScript(geckoSession: GeckoSession, scriptFileName: String): GeckoResult<SlowScriptResponse> { + return if (!scriptWaited) { + scriptWaited = true + GeckoResult.fromValue(SlowScriptResponse.CONTINUE) + } else { + GeckoResult.fromValue(SlowScriptResponse.STOP) + } + } + + @AssertCalled(count = 1, order = [3]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat( + "The script did not complete.", + mainSession.evaluateJS("document.getElementById(\"content\").innerHTML") as String, + equalTo("Started"), + ) + } + }) + mainSession.loadTestPath(HUNG_SCRIPT) + sessionRule.waitForPageStop(mainSession) + } + + /** + * Test that the display mode is applied to CSS media query + */ + @Test fun displayMode() { + val pwaSession = sessionRule.createOpenSession( + GeckoSessionSettings.Builder(mainSession.settings) + .displayMode(GeckoSessionSettings.DISPLAY_MODE_FULLSCREEN) + .build(), + ) + pwaSession.loadTestPath(HELLO_HTML_PATH) + pwaSession.waitForPageStop() + + val matches = pwaSession.evaluateJS("window.matchMedia('(display-mode: fullscreen)').matches") as Boolean + assertThat( + "display-mode should be fullscreen", + matches, + equalTo(true), + ) + } +} |