/* -*- 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, 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 and an element which covers up the 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) } } }