summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/DynamicToolbarTest.kt
blob: 6a79df617389214dc0d79ebbc5bf7eabb507b640 (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
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
/* -*- 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.* // ktlint-disable no-wildcard-imports
import android.graphics.Bitmap
import android.os.SystemClock
import android.util.Base64
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
import org.hamcrest.Matchers.closeTo
import org.hamcrest.Matchers.equalTo
import org.junit.Assume.assumeThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.ContentDelegate
import org.mozilla.geckoview.GeckoSession.ScrollDelegate
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import java.io.ByteArrayOutputStream

private const val SCREEN_WIDTH = 100
private const val SCREEN_HEIGHT = 200

@RunWith(AndroidJUnit4::class)
@MediumTest
class DynamicToolbarTest : BaseSessionTest() {
    // Makes sure we can load a page when the dynamic toolbar is bigger than the whole content
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun outOfRangeValue() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT + 1
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(HELLO_HTML_PATH)
        mainSession.waitForPageStop()
    }

    private fun assertScreenshotResult(result: GeckoResult<Bitmap>, comparisonImage: Bitmap) {
        sessionRule.waitForResult(result).let {
            assertThat(
                "Screenshot is not null",
                it,
                notNullValue(),
            )
            assertThat("Widths are the same", comparisonImage.width, equalTo(it.width))
            assertThat("Heights are the same", comparisonImage.height, equalTo(it.height))
            assertThat("Byte counts are the same", comparisonImage.byteCount, equalTo(it.byteCount))
            assertThat("Configs are the same", comparisonImage.config, equalTo(it.config))

            if (!comparisonImage.sameAs(it)) {
                val outputForComparison = ByteArrayOutputStream()
                comparisonImage.compress(Bitmap.CompressFormat.PNG, 100, outputForComparison)

                val outputForActual = ByteArrayOutputStream()
                it.compress(Bitmap.CompressFormat.PNG, 100, outputForActual)
                val actualString: String = Base64.encodeToString(outputForActual.toByteArray(), Base64.DEFAULT)
                val comparisonString: String = Base64.encodeToString(outputForComparison.toByteArray(), Base64.DEFAULT)

                assertThat("Encoded strings are the same", comparisonString, equalTo(actualString))
            }

            assertThat("Bytes are the same", comparisonImage.sameAs(it), equalTo(true))
        }
    }

    /**
     * Returns a whole green Bitmap.
     * This Bitmap would be a reference image of tests in this file.
     */
    private fun getComparisonScreenshot(width: Int, height: Int): Bitmap {
        val screenshotFile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(screenshotFile)
        val paint = Paint()
        paint.color = Color.rgb(0, 128, 0)
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
        return screenshotFile
    }

    // With the dynamic toolbar max height vh units values exceed
    // the top most window height. This is a test case that exceeded area
    // is rendered properly (on the compositor).
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun positionFixedElementClipping() {
        sessionRule.display?.run { setDynamicToolbarMaxHeight(SCREEN_HEIGHT / 2) }

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        // FIXED_VH is an HTML file which has a position:fixed element whose
        // style is "width: 100%; height: 200vh" and the document is scaled by
        // minimum-scale 0.5, so that the height of the element exceeds the
        // window height.
        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        // Scroll down bit, if we correctly render the document, the position
        // fixed element still covers whole the document area.
        mainSession.evaluateJS("window.scrollTo({ top: 100, behavior: 'instant' })")

        // Wait a while to make sure the scrolling result is composited on the compositor
        // since capturePixels() takes a snapshot directly from the compositor without
        // waiting for a corresponding MozAfterPaint on the main-thread so it's possible
        // to take a stale snapshot even if it's a result of syncronous scrolling.
        mainSession.evaluateJS("new Promise(resolve => window.setTimeout(resolve, 1000))")

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    // Asynchronous scrolling with the dynamic toolbar max height causes
    // situations where the visual viewport size gets bigger than the layout
    // viewport on the compositor thread because of 200vh position:fixed
    // elements.  This is a test case that a 200vh position element is
    // properly rendered its positions.
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun layoutViewportExpansion() {
        sessionRule.display?.run { setDynamicToolbarMaxHeight(SCREEN_HEIGHT / 2) }

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        mainSession.evaluateJS("window.scrollTo(0, 100)")

        // Scroll back to the original position by asynchronous scrolling.
        mainSession.evaluateJS("window.scrollTo({ top: 0, behavior: 'smooth' })")

        mainSession.evaluateJS("new Promise(resolve => window.setTimeout(resolve, 1000))")

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun visualViewportEvents() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double
        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double

        for (i in 1..dynamicToolbarMaxHeight) {
            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }

            val expectedViewportHeight = (SCREEN_HEIGHT - dynamicToolbarMaxHeight + i) / scale / pixelRatio
            val promise = mainSession.evaluatePromiseJS(
                """
             new Promise(resolve => {
               window.visualViewport.addEventListener('resize', resolve(window.visualViewport.height));
             });
                """.trimIndent(),
            )

            assertThat(
                "The visual viewport height should be changed in response to the dynamc toolbar transition",
                promise.value as Double,
                closeTo(expectedViewportHeight, .01),
            )
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun percentBaseValueOnPositionFixedElement() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_PERCENT)
        mainSession.waitForPageStop()

        val originalHeight = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        // Set the vertical clipping value to the middle of toolbar transition.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight / 2) }

        var height = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        assertThat(
            "The %-based height should be the static in the middle of toolbar tansition",
            height,
            equalTo(originalHeight),
        )

        // Set the vertical clipping value to hide the toolbar completely.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        height = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double
        val expectedHeight = (SCREEN_HEIGHT / scale).toInt()
        assertThat(
            "The %-based height should be now recomputed based on the screen height",
            height,
            equalTo(expectedHeight.toString() + "px"),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun resizeEvents() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        for (i in 1..dynamicToolbarMaxHeight - 1) {
            val promise = mainSession.evaluatePromiseJS(
                """
                new Promise(resolve => {
                    let fired = false;
                    window.addEventListener('resize', () => { fired = true; }, { once: true });
                    // Note that `resize` event is fired just before rAF callbacks, so under ideal
                    // circumstances waiting for a rAF should be sufficient, even if it's not sufficient
                    // unexpected resize event(s) will be caught in the next loop.
                    requestAnimationFrame(() => { resolve(fired); });
                });
                """.trimIndent(),
            )

            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }
            assertThat(
                "'resize' event on window should not be fired in response to the dynamc toolbar transition",
                promise.value as Boolean,
                equalTo(false),
            )
        }

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                window.addEventListener('resize', () => { resolve(true); }, { once: true });
            });
            """.trimIndent(),
        )

        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        assertThat(
            "'resize' event on window should be fired when the dynamc toolbar is completely hidden",
            promise.value as Boolean,
            equalTo(true),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun windowInnerHeight() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        // We intentionally use FIXED_BOTTOM instead of FIXED_VH in this test since
        // FIXED_VH has `minimum-scale=0.5` thus we can't properly test window.innerHeight
        // with FXIED_VH for now due to bug 1598487.
        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double

        for (i in 1..dynamicToolbarMaxHeight - 1) {
            val promise = mainSession.evaluatePromiseJS(
                """
               new Promise(resolve => {
                 window.visualViewport.addEventListener('resize', resolve(window.innerHeight));
               });
                """.trimIndent(),
            )

            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }
            assertThat(
                "window.innerHeight should not be changed in response to the dynamc toolbar transition",
                promise.value as Double,
                closeTo(SCREEN_HEIGHT / 2 / pixelRatio, .01),
            )
        }

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                window.addEventListener('resize', () => { resolve(window.innerHeight); }, { once: true });
            });
            """.trimIndent(),
        )

        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        assertThat(
            "window.innerHeight should be changed when the dynamc toolbar is completely hidden",
            promise.value as Double,
            closeTo(SCREEN_HEIGHT / pixelRatio, .01),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun notCrashOnResizeEvent() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => window.addEventListener('resize', () => resolve(true)));
            """.trimIndent(),
        )

        // Do some setVerticalClipping calls that we might try to queue two window resize events.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight + 1) }
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        assertThat("Got a rezie event", promise.value as Boolean, equalTo(true))
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun showDynamicToolbar() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(SHOW_DYNAMIC_TOOLBAR_HTML_PATH)
        mainSession.waitForPageStop()
        mainSession.evaluateJS("window.scrollTo(0, " + dynamicToolbarMaxHeight + ")")
        mainSession.waitUntilCalled(object : ScrollDelegate {
            @AssertCalled(count = 1)
            override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            }
        })

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.synthesizeTap(5, 25)

        mainSession.waitUntilCalled(object : ContentDelegate {
            @AssertCalled(count = 1)
            override fun onShowDynamicToolbar(session: GeckoSession) {
            }
        })
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun showDynamicToolbarOnOverflowHidden() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(SHOW_DYNAMIC_TOOLBAR_HTML_PATH)
        mainSession.waitForPageStop()
        mainSession.evaluateJS("window.scrollTo(0, " + dynamicToolbarMaxHeight + ")")
        mainSession.waitUntilCalled(object : ScrollDelegate {
            @AssertCalled(count = 1)
            override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            }
        })

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.evaluateJS("document.documentElement.style.overflow = 'hidden'")

        mainSession.waitUntilCalled(object : ContentDelegate {
            @AssertCalled(count = 1)
            override fun onShowDynamicToolbar(session: GeckoSession) {
            }
        })
    }

    private fun getComputedViewportHeight(style: String): Double {
        val viewportHeight = mainSession.evaluateJS(
            """
            const target = document.createElement('div');
            target.style.height = '$style';
            document.body.appendChild(target);
            parseFloat(getComputedStyle(target).height);
            """.trimIndent(),
        ) as Double

        return viewportHeight
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun viewportVariants() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.VIEWPORT_PATH)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double
        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double

        var smallViewportHeight = getComputedViewportHeight("100svh")
        assertThat(
            "svh value at the initial state",
            smallViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        var largeViewportHeight = getComputedViewportHeight("100lvh")
        assertThat(
            "lvh value at the initial state",
            largeViewportHeight,
            closeTo(SCREEN_HEIGHT / scale / pixelRatio, 0.1),
        )

        var dynamicViewportHeight = getComputedViewportHeight("100dvh")
        assertThat(
            "dvh value at the initial state",
            dynamicViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        // Move down the toolbar at a fourth of its position.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight / 4) }

        smallViewportHeight = getComputedViewportHeight("100svh")
        assertThat(
            "svh value during toolbar transition",
            smallViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        largeViewportHeight = getComputedViewportHeight("100lvh")
        assertThat(
            "lvh value during toolbar transition",
            largeViewportHeight,
            closeTo(SCREEN_HEIGHT / scale / pixelRatio, 0.1),
        )

        dynamicViewportHeight = getComputedViewportHeight("100dvh")
        assertThat(
            "dvh value during toolbar transition",
            dynamicViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight + dynamicToolbarMaxHeight / 4) / scale / pixelRatio, 0.1),
        )
    }

    // With dynamic toolbar, there was a floating point rounding error in Gecko layout side.
    // The error was appeared by user interactive async scrolling, not by programatic async
    // scrolling, e.g. scrollTo() method. If the error happens there will appear 1px gap
    // between <body> and an element which covers up the <body> element.
    // This test simulates the situation.
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun noGapAppearsBetweenBodyAndElementFullyCoveringBody() {
        // Bug 1764219 - disable the test to reduce intermittent failure rate
        assumeThat(sessionRule.env.isDebugBuild, equalTo(false))
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        mainSession.loadTestPath(BaseSessionTest.BODY_FULLY_COVERED_BY_GREEN_ELEMENT)
        mainSession.waitForPageStop()
        mainSession.flushApzRepaints()

        // Scrolling down by touch events.
        var downTime = SystemClock.uptimeMillis()
        var down = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            50f,
            70f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(down)
        var move = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_MOVE,
            50f,
            30f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(move)
        var up = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_UP,
            50f,
            10f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(up)
        mainSession.flushApzRepaints()

        // Scrolling up by touch events to restore the original position.
        downTime = SystemClock.uptimeMillis()
        down = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            50f,
            10f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(down)
        move = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_MOVE,
            50f,
            30f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(move)
        up = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_UP,
            50f,
            70f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(up)
        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun zoomedOverflowHidden() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for foreground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        // Change the body background color to match the reference image's background color.
        mainSession.evaluateJS("document.body.style.background = 'rgb(0, 128, 0)'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        // Zoom in the content so that the content's visual viewport can be scrollable.
        mainSession.setResolutionAndScaleTo(10.0f)

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun zoomedPositionFixedRoot() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for foreground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        // Change the body background color to match the reference image's background color.
        mainSession.evaluateJS("document.body.style.background = 'rgb(0, 128, 0)'")

        // Change the root `overlow` style to make it scrollable and change the position style
        // to `fixed` so that the root container is not scrollable.
        mainSession.evaluateJS("document.body.style.overflow = 'scroll'")
        mainSession.evaluateJS("document.documentElement.style.position = 'fixed'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        // Zoom in the content so that the content's visual viewport can be scrollable.
        mainSession.setResolutionAndScaleTo(10.0f)

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun backgroundImageFixed() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.TOUCH_ACTION_HTML_PATH)
        mainSession.waitForPageStop()

        // Specify the root background-color to match the reference image color and specify
        // `background-attachment: fixed`.
        mainSession.evaluateJS("document.documentElement.style.background = 'linear-gradient(green, green) fixed'")

        // Make the root element scrollable.
        mainSession.evaluateJS("document.documentElement.style.height = '100vh'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        mainSession.flushApzRepaints()

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun backgroundAttachmentFixed() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.TOUCH_ACTION_HTML_PATH)
        mainSession.waitForPageStop()

        // Specify the root background-color to match the reference image color and specify
        // `background-attachment: fixed`.
        mainSession.evaluateJS("document.documentElement.style.background = 'rgb(0, 128, 0) fixed'")

        // Make the root element scrollable.
        mainSession.evaluateJS("document.documentElement.style.height = '100vh'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        mainSession.flushApzRepaints()

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }
}