summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoViewTest.kt
blob: 82af2c6475abeecae568358660fd1079fd3a38dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
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")
        }
    }
}