summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt')
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt462
1 files changed, 462 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt
new file mode 100644
index 0000000000..82af2c6475
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt
@@ -0,0 +1,462 @@
+package org.mozilla.geckoview.test
+
+import android.content.Context
+import android.graphics.Matrix
+import android.os.Build
+import android.os.Bundle
+import android.os.LocaleList
+import android.util.Pair
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewStructure
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import androidx.core.view.ViewCompat
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.hamcrest.Matchers.equalTo
+import org.junit.* // ktlint-disable no-wildcard-imports
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeThat
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.Autofill
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoView
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate
+import org.mozilla.geckoview.test.util.UiThreadUtils
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class GeckoViewTest : BaseSessionTest() {
+ val activityRule = ActivityScenarioRule(GeckoViewTestActivity::class.java)
+
+ @get:Rule
+ override val rules = RuleChain.outerRule(activityRule).around(sessionRule)
+
+ @Before
+ fun setup() {
+ activityRule.scenario.onActivity {
+ // Attach the default session from the session rule to the GeckoView
+ it.view.setSession(sessionRule.session)
+ }
+ }
+
+ @After
+ fun cleanup() {
+ activityRule.scenario.onActivity {
+ it.view.releaseSession()
+ }
+ }
+
+ @Test
+ fun setSessionOnClosed() {
+ activityRule.scenario.onActivity {
+ it.view.session!!.close()
+ it.view.setSession(GeckoSession())
+ }
+ }
+
+ @Test
+ fun setSessionOnOpenDoesNotThrow() {
+ activityRule.scenario.onActivity {
+ assertThat("Session is open", it.view.session!!.isOpen, equalTo(true))
+ val newSession = GeckoSession()
+ it.view.setSession(newSession)
+ assertThat(
+ "The new session should be correctly set.",
+ it.view.session,
+ equalTo(newSession),
+ )
+ }
+ }
+
+ @Test(expected = java.lang.IllegalStateException::class)
+ fun displayAlreadyAcquired() {
+ activityRule.scenario.onActivity {
+ assertThat(
+ "View should be attached",
+ ViewCompat.isAttachedToWindow(it.view),
+ equalTo(true),
+ )
+ it.view.session!!.acquireDisplay()
+ }
+ }
+
+ @Test
+ fun relaseOnDetach() {
+ activityRule.scenario.onActivity {
+ // The GeckoDisplay should be released when the View is detached from the window...
+ it.view.onDetachedFromWindow()
+ it.view.session!!.releaseDisplay(it.view.session!!.acquireDisplay())
+ }
+ }
+
+ private fun waitUntilContentProcessPriority(high: List<GeckoSession>, low: List<GeckoSession>) {
+ val highPids = high.map { sessionRule.getSessionPid(it) }.toSet()
+ val lowPids = low.map { sessionRule.getSessionPid(it) }.toSet()
+
+ UiThreadUtils.waitForCondition({
+ val shouldBeHighPri = getContentProcessesOomScore(highPids)
+ val shouldBeLowPri = getContentProcessesOomScore(lowPids)
+ // Note that higher oom score means less priority
+ shouldBeHighPri.count { it > 100 } == 0 &&
+ shouldBeLowPri.count { it < 300 } == 0
+ }, env.defaultTimeoutMillis)
+ }
+
+ fun getContentProcessesOomScore(pids: Collection<Int>): List<Int> {
+ return pids.map { pid ->
+ File("/proc/$pid/oom_score").readText(Charsets.UTF_8).trim().toInt()
+ }
+ }
+
+ fun setupPriorityTest(): GeckoSession {
+ // This makes the test a little bit faster
+ sessionRule.setPrefsUntilTestEnd(
+ mapOf(
+ "dom.ipc.processPriorityManager.backgroundGracePeriodMS" to 0,
+ "dom.ipc.processPriorityManager.backgroundPerceivableGracePeriodMS" to 0,
+ ),
+ )
+
+ val otherSession = sessionRule.createOpenSession()
+ // The process manager sets newly created processes to FOREGROUND priority until they
+ // are de-prioritized, so we need to activate and deactivate the session to trigger
+ // a setPriority call.
+ otherSession.setActive(true)
+ otherSession.setActive(false)
+
+ // Need a dummy page to be able to get the PID from the session
+ otherSession.loadUri("https://example.com")
+ otherSession.waitForPageStop()
+
+ mainSession.loadTestPath(HELLO_HTML_PATH)
+ mainSession.waitForPageStop()
+
+ waitUntilContentProcessPriority(
+ high = listOf(mainSession),
+ low = listOf(otherSession),
+ )
+
+ return otherSession
+ }
+
+ @Test
+ @NullDelegate(Autofill.Delegate::class)
+ fun setTabActiveKeepsTabAtHighPriority() {
+ // Bug 1768102 - Doesn't seem to work on Fission
+ assumeThat(env.isFission || env.isIsolatedProcess, equalTo(false))
+ activityRule.scenario.onActivity {
+ val otherSession = setupPriorityTest()
+
+ // A tab with priority hint does not get de-prioritized even when
+ // the surface is destroyed
+ mainSession.setPriorityHint(GeckoSession.PRIORITY_HIGH)
+
+ // This will destroy mainSession's surface and create a surface for otherSession
+ it.view.setSession(otherSession)
+
+ waitUntilContentProcessPriority(high = listOf(mainSession, otherSession), low = listOf())
+
+ // Destroying otherSession's surface should leave mainSession as the sole high priority
+ // tab
+ it.view.releaseSession()
+
+ waitUntilContentProcessPriority(high = listOf(mainSession), low = listOf())
+
+ // Cleanup
+ mainSession.setPriorityHint(GeckoSession.PRIORITY_DEFAULT)
+ }
+ }
+
+ @Test
+ @NullDelegate(Autofill.Delegate::class)
+ fun processPriorityTest() {
+ // Doesn't seem to work on Fission
+ assumeThat(env.isFission || env.isIsolatedProcess, equalTo(false))
+ activityRule.scenario.onActivity {
+ val otherSession = setupPriorityTest()
+
+ // After setting otherSession to the view, otherSession should be high priority
+ // and mainSession should be de-prioritized
+ it.view.setSession(otherSession)
+
+ waitUntilContentProcessPriority(
+ high = listOf(otherSession),
+ low = listOf(mainSession),
+ )
+
+ // After releasing otherSession, both sessions should be low priority
+ it.view.releaseSession()
+
+ waitUntilContentProcessPriority(
+ high = listOf(),
+ low = listOf(mainSession, otherSession),
+ )
+
+ // Test that re-setting mainSession in the view raises the priority again
+ it.view.setSession(mainSession)
+ waitUntilContentProcessPriority(
+ high = listOf(mainSession),
+ low = listOf(otherSession),
+ )
+
+ // Setting the session to active should also raise priority
+ otherSession.setActive(true)
+ waitUntilContentProcessPriority(
+ high = listOf(mainSession, otherSession),
+ low = listOf(),
+ )
+ }
+ }
+
+ @Test
+ @NullDelegate(Autofill.Delegate::class)
+ fun setPriorityHint() {
+ // Bug 1768102 - Doesn't seem to work on Fission
+ assumeThat(env.isFission || env.isIsolatedProcess, equalTo(false))
+
+ val otherSession = setupPriorityTest()
+
+ // Setting priorityHint to PRIORITY_HIGH raises priority
+ otherSession.setPriorityHint(GeckoSession.PRIORITY_HIGH)
+
+ waitUntilContentProcessPriority(
+ high = listOf(mainSession, otherSession),
+ low = listOf(),
+ )
+
+ // Setting priorityHint to PRIORITY_DEFAULT should lower priority
+ otherSession.setPriorityHint(GeckoSession.PRIORITY_DEFAULT)
+
+ waitUntilContentProcessPriority(
+ high = listOf(mainSession),
+ low = listOf(otherSession),
+ )
+ }
+
+ private fun visit(node: MockViewStructure, callback: (MockViewStructure) -> Unit) {
+ callback(node)
+
+ for (child in node.children) {
+ if (child != null) {
+ visit(child, callback)
+ }
+ }
+ }
+
+ @Test
+ @NullDelegate(Autofill.Delegate::class)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun autofillWithNoSession() {
+ mainSession.loadTestPath(FORMS_XORIGIN_HTML_PATH)
+ mainSession.waitForPageStop()
+
+ val autofills = mapOf(
+ "#user1" to "username@example.com",
+ "#user2" to "username@example.com",
+ "#pass1" to "test-password",
+ "#pass2" to "test-password",
+ )
+
+ // Set up promises to monitor the values changing.
+ val promises = autofills.map { entry ->
+ // Repeat each test with both the top document and the iframe document.
+ mainSession.evaluatePromiseJS(
+ """
+ window.getDataForAllFrames('${entry.key}', '${entry.value}')
+ """,
+ )
+ }
+
+ activityRule.scenario.onActivity {
+ val root = MockViewStructure(View.NO_ID)
+ it.view.onProvideAutofillVirtualStructure(root, 0)
+
+ val data = SparseArray<AutofillValue>()
+ visit(root) { node ->
+ if (node.hints?.indexOf(View.AUTOFILL_HINT_USERNAME) != -1) {
+ data.set(node.id, AutofillValue.forText("username@example.com"))
+ } else if (node.hints?.indexOf(View.AUTOFILL_HINT_PASSWORD) != -1) {
+ data.set(node.id, AutofillValue.forText("test-password"))
+ }
+ }
+
+ // Releasing the session will set mSession in GeckoView to null
+ // this test verifies that we can still autofill correctly even in released state
+ val session = it.view.releaseSession()!!
+ it.view.autofill(data)
+
+ // Put back the session and verifies that the autofill went through anyway
+ it.view.setSession(session)
+
+ // Wait on the promises and check for correct values.
+ for (values in promises.map { p -> p.value.asJsonArray() }) {
+ for (i in 0 until values.length()) {
+ val (key, actual, expected, eventInterface) = values.get(i).asJSList<String>()
+
+ assertThat("Auto-filled value must match ($key)", actual, equalTo(expected))
+ assertThat(
+ "input event should be dispatched with InputEvent interface",
+ eventInterface,
+ equalTo("InputEvent"),
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ @NullDelegate(Autofill.Delegate::class)
+ fun activityContextDelegate() {
+ var delegateCalled = false
+ activityRule.scenario.onActivity {
+ class TestActivityDelegate : GeckoView.ActivityContextDelegate {
+ override fun getActivityContext(): Context {
+ delegateCalled = true
+ return it
+ }
+ }
+ // Set view delegate
+ it.view.activityContextDelegate = TestActivityDelegate()
+ val context = it.view.activityContextDelegate?.activityContext
+ assertTrue("The activity context delegate was called.", delegateCalled)
+ assertTrue("The activity context delegate provided the expected context.", context == it)
+ }
+ }
+
+ class MockViewStructure(var id: Int, var parent: MockViewStructure? = null) : ViewStructure() {
+ private var enabled: Boolean = false
+ private var inputType = 0
+ var children = Array<MockViewStructure?>(0, { null })
+ var childIndex = 0
+ var hints: Array<out String>? = null
+
+ override fun setId(p0: Int, p1: String?, p2: String?, p3: String?) {
+ id = p0
+ }
+
+ override fun setEnabled(p0: Boolean) {
+ enabled = p0
+ }
+
+ override fun setChildCount(p0: Int) {
+ children = Array(p0, { null })
+ }
+
+ override fun getChildCount(): Int {
+ return children.size
+ }
+
+ override fun newChild(p0: Int): ViewStructure {
+ val child = MockViewStructure(p0, this)
+ children[childIndex++] = child
+ return child
+ }
+
+ override fun asyncNewChild(p0: Int): ViewStructure {
+ return newChild(p0)
+ }
+
+ override fun setInputType(p0: Int) {
+ inputType = p0
+ }
+
+ fun getInputType(): Int {
+ return inputType
+ }
+
+ override fun setAutofillHints(p0: Array<out String>?) {
+ hints = p0
+ }
+
+ override fun addChildCount(p0: Int): Int {
+ TODO()
+ }
+
+ override fun setDimens(p0: Int, p1: Int, p2: Int, p3: Int, p4: Int, p5: Int) {}
+ override fun setTransformation(p0: Matrix?) {}
+ override fun setElevation(p0: Float) {}
+ override fun setAlpha(p0: Float) {}
+ override fun setVisibility(p0: Int) {}
+ override fun setClickable(p0: Boolean) {}
+ override fun setLongClickable(p0: Boolean) {}
+ override fun setContextClickable(p0: Boolean) {}
+ override fun setFocusable(p0: Boolean) {}
+ override fun setFocused(p0: Boolean) {}
+ override fun setAccessibilityFocused(p0: Boolean) {}
+ override fun setCheckable(p0: Boolean) {}
+ override fun setChecked(p0: Boolean) {}
+ override fun setSelected(p0: Boolean) {}
+ override fun setActivated(p0: Boolean) {}
+ override fun setOpaque(p0: Boolean) {}
+ override fun setClassName(p0: String?) {}
+ override fun setContentDescription(p0: CharSequence?) {}
+ override fun setText(p0: CharSequence?) {}
+ override fun setText(p0: CharSequence?, p1: Int, p2: Int) {}
+ override fun setTextStyle(p0: Float, p1: Int, p2: Int, p3: Int) {}
+ override fun setTextLines(p0: IntArray?, p1: IntArray?) {}
+ override fun setHint(p0: CharSequence?) {}
+ override fun getText(): CharSequence {
+ return ""
+ }
+ override fun getTextSelectionStart(): Int {
+ return 0
+ }
+ override fun getTextSelectionEnd(): Int {
+ return 0
+ }
+ override fun getHint(): CharSequence {
+ return ""
+ }
+ override fun getExtras(): Bundle {
+ return Bundle()
+ }
+ override fun hasExtras(): Boolean {
+ return false
+ }
+
+ override fun getAutofillId(): AutofillId? {
+ return null
+ }
+ override fun setAutofillId(p0: AutofillId) {}
+ override fun setAutofillId(p0: AutofillId, p1: Int) {}
+ override fun setAutofillType(p0: Int) {}
+ override fun setAutofillValue(p0: AutofillValue?) {}
+ override fun setAutofillOptions(p0: Array<out CharSequence>?) {}
+ override fun setDataIsSensitive(p0: Boolean) {}
+ override fun asyncCommit() {}
+ override fun setWebDomain(p0: String?) {}
+ override fun setLocaleList(p0: LocaleList?) {}
+
+ override fun newHtmlInfoBuilder(p0: String): HtmlInfo.Builder {
+ return MockHtmlInfoBuilder()
+ }
+ override fun setHtmlInfo(p0: HtmlInfo) {
+ }
+ }
+
+ class MockHtmlInfoBuilder : ViewStructure.HtmlInfo.Builder() {
+ override fun addAttribute(p0: String, p1: String): ViewStructure.HtmlInfo.Builder {
+ return this
+ }
+
+ override fun build(): ViewStructure.HtmlInfo {
+ return MockHtmlInfo()
+ }
+ }
+
+ class MockHtmlInfo : ViewStructure.HtmlInfo() {
+ override fun getTag(): String {
+ TODO("Not yet implemented")
+ }
+
+ override fun getAttributes(): MutableList<Pair<String, String>>? {
+ TODO("Not yet implemented")
+ }
+ }
+}