/* -*- 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.graphics.BitmapFactory import android.view.Surface import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports import org.junit.Assert import org.junit.Assume.assumeThat import org.junit.Test import org.junit.runner.RunWith import org.mozilla.geckoview.GeckoDisplay.SurfaceInfo import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoResult.OnExceptionListener import org.mozilla.geckoview.GeckoResult.fromException import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoSession.ContentDelegate import org.mozilla.geckoview.GeckoSession.ProgressDelegate import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay import java.lang.IllegalStateException import kotlin.math.absoluteValue import kotlin.math.max private const val SCREEN_HEIGHT = 800 private const val SCREEN_WIDTH = 800 private const val BIG_SCREEN_HEIGHT = 999999 private const val BIG_SCREEN_WIDTH = 999999 @RunWith(AndroidJUnit4::class) @MediumTest class ScreenshotTest : BaseSessionTest() { 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.shader = LinearGradient(0f, 0f, width.toFloat(), height.toFloat(), Color.RED, Color.WHITE, Shader.TileMode.MIRROR) canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) return screenshotFile } companion object { /** * Compares two Bitmaps and returns the largest color element difference (red, green or blue) */ public fun imageElementDifference(b1: Bitmap, b2: Bitmap): Int { return if (b1.width == b2.width && b1.height == b2.height) { val pixels1 = IntArray(b1.width * b1.height) val pixels2 = IntArray(b2.width * b2.height) b1.getPixels(pixels1, 0, b1.width, 0, 0, b1.width, b1.height) b2.getPixels(pixels2, 0, b2.width, 0, 0, b2.width, b2.height) var maxDiff = 0 for (i in 0 until pixels1.size) { val redDiff = (Color.red(pixels1[i]) - Color.red(pixels2[i])).absoluteValue val greenDiff = (Color.green(pixels1[i]) - Color.green(pixels2[i])).absoluteValue val blueDiff = (Color.blue(pixels1[i]) - Color.blue(pixels2[i])).absoluteValue maxDiff = max(maxDiff, max(redDiff, max(greenDiff, blueDiff))) } maxDiff } else { 256 } } } 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)) assertThat( "Images are almost identical", imageElementDifference(comparisonImage, it), lessThanOrEqualTo(1), ) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun capturePixelsSucceeds() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.capturePixels(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun capturePixelsCanBeCalledMultipleTimes() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { val call1 = it.capturePixels() val call2 = it.capturePixels() val call3 = it.capturePixels() assertScreenshotResult(call1, screenshotFile) assertScreenshotResult(call2, screenshotFile) assertScreenshotResult(call3, screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun capturePixelsCompletesCompositorPausedRestarted() { sessionRule.display?.let { it.surfaceDestroyed() val result = it.capturePixels() val texture = SurfaceTexture(0) texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT) val surface = Surface(texture) it.surfaceChanged(SurfaceInfo.Builder(surface).size(SCREEN_WIDTH, SCREEN_HEIGHT).build()) sessionRule.waitForResult(result) } } // This tests tries to catch problems like Bug 1644561. @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun capturePixelsStressTest() { val screenshots = mutableListOf>() sessionRule.display?.let { for (i in 0..100) { screenshots.add(it.capturePixels()) } for (i in 0..50) { sessionRule.waitForResult(screenshots[i]) } it.surfaceDestroyed() screenshots.add(it.capturePixels()) it.surfaceDestroyed() val texture = SurfaceTexture(0) texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT) val surface = Surface(texture) it.surfaceChanged(SurfaceInfo.Builder(surface).size(SCREEN_WIDTH, SCREEN_HEIGHT).build()) for (i in 0..100) { screenshots.add(it.capturePixels()) } for (i in 0..100) { it.surfaceDestroyed() screenshots.add(it.capturePixels()) val newTexture = SurfaceTexture(0) newTexture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT) val newSurface = Surface(newTexture) it.surfaceChanged(SurfaceInfo.Builder(newSurface).size(SCREEN_WIDTH, SCREEN_HEIGHT).build()) } try { for (result in screenshots) { sessionRule.waitForResult(result) } } catch (ex: RuntimeException) { // Rejecting the screenshot is fine } } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test(expected = IllegalStateException::class) fun capturePixelsFailsCompositorPaused() { sessionRule.display?.let { it.surfaceDestroyed() val result = it.capturePixels() it.surfaceDestroyed() sessionRule.waitForResult(result) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun capturePixelsWhileSessionDeactivated() { // TODO: Bug 1673955 assumeThat(sessionRule.env.isFission, equalTo(false)) val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) mainSession.setActive(false) // Deactivating the session should trigger a flush state change sessionRule.waitUntilCalled(object : ProgressDelegate { @AssertCalled(count = 1) override fun onSessionStateChange( session: GeckoSession, sessionState: GeckoSession.SessionState, ) {} }) sessionRule.display?.let { assertScreenshotResult(it.capturePixels(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotToBitmap() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.screenshot().capture(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotScaledToSize() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.screenshot().size(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2).capture(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenShotScaledWithScale() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.screenshot().scale(0.5f).capture(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenShotScaledWithAspectPreservingSize() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.screenshot().aspectPreservingSize(SCREEN_WIDTH / 2).capture(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun recycleBitmap() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { val call1 = it.screenshot().capture() assertScreenshotResult(call1, screenshotFile) val call2 = it.screenshot().bitmap(call1.poll(1000)).capture() assertScreenshotResult(call2, screenshotFile) val call3 = it.screenshot().bitmap(call2.poll(1000)).capture() assertScreenshotResult(call3, screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotWholeRegion() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult(it.screenshot().source(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT).capture(), screenshotFile) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotWholeRegionScaled() { val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult( it.screenshot() .source(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) .size(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) .capture(), screenshotFile, ) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotQuarters() { val res = InstrumentationRegistry.getInstrumentation().targetContext.resources mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult( it.screenshot() .source(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl), ) assertScreenshotResult( it.screenshot() .source(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br), ) } } @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH) @Test fun screenshotQuartersScaled() { val res = InstrumentationRegistry.getInstrumentation().targetContext.resources mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.waitUntilCalled(object : ContentDelegate { @AssertCalled(count = 1) override fun onFirstContentfulPaint(session: GeckoSession) { } }) sessionRule.display?.let { assertScreenshotResult( it.screenshot() .source(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) .size(SCREEN_WIDTH / 4, SCREEN_WIDTH / 4) .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl_scaled), ) assertScreenshotResult( it.screenshot() .source(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2) .size(SCREEN_WIDTH / 4, SCREEN_WIDTH / 4) .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br_scaled), ) } } @WithDisplay(height = BIG_SCREEN_HEIGHT, width = BIG_SCREEN_WIDTH) @Test fun giantScreenshot() { mainSession.loadTestPath(COLORS_HTML_PATH) sessionRule.display?.screenshot()!!.source(0, 0, BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT) .size(BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT) .capture() .exceptionally( OnExceptionListener { error: Throwable -> Assert.assertTrue(error is OutOfMemoryError) fromException(error) }, ) } }