<lambda>null1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * Any copyright is dedicated to the Public Domain.
3 http://creativecommons.org/publicdomain/zero/1.0/ */
4
5 package org.mozilla.geckoview.test
6
7
8 import android.graphics.*
9 import androidx.test.filters.MediumTest
10 import androidx.test.ext.junit.runners.AndroidJUnit4
11 import android.view.Surface
12 import org.hamcrest.Matchers.*
13 import org.junit.Assert
14 import org.junit.Rule
15 import org.junit.Test
16 import org.junit.rules.ExpectedException
17 import org.junit.runner.RunWith
18 import org.mozilla.geckoview.GeckoResult
19 import org.mozilla.geckoview.GeckoResult.OnExceptionListener
20 import org.mozilla.geckoview.GeckoResult.fromException
21 import org.mozilla.geckoview.GeckoSession
22 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
23 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
24 import org.mozilla.geckoview.test.util.Callbacks
25 import kotlin.math.absoluteValue
26 import kotlin.math.max
27 import android.graphics.BitmapFactory
28 import android.graphics.Bitmap
29 import androidx.test.platform.app.InstrumentationRegistry
30 import org.junit.Assume.assumeThat
31 import java.lang.IllegalStateException
32 import java.lang.NullPointerException
33
34
35 private const val SCREEN_HEIGHT = 800
36 private const val SCREEN_WIDTH = 800
37 private const val BIG_SCREEN_HEIGHT = 999999
38 private const val BIG_SCREEN_WIDTH = 999999
39
40 @RunWith(AndroidJUnit4::class)
41 @MediumTest
42 class ScreenshotTest : BaseSessionTest() {
43
44 @get:Rule
45 val expectedEx: ExpectedException = ExpectedException.none()
46
47 private fun getComparisonScreenshot(width: Int, height: Int): Bitmap {
48 val screenshotFile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
49 val canvas = Canvas(screenshotFile)
50 val paint = Paint()
51 paint.shader = LinearGradient(0f, 0f, width.toFloat(), height.toFloat(), Color.RED, Color.WHITE, Shader.TileMode.MIRROR)
52 canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
53 return screenshotFile
54 }
55
56 companion object {
57 /**
58 * Compares two Bitmaps and returns the largest color element difference (red, green or blue)
59 */
60 public fun imageElementDifference(b1: Bitmap, b2: Bitmap): Int {
61 return if (b1.width == b2.width && b1.height == b2.height) {
62 val pixels1 = IntArray(b1.width * b1.height)
63 val pixels2 = IntArray(b2.width * b2.height)
64 b1.getPixels(pixels1, 0, b1.width, 0, 0, b1.width, b1.height)
65 b2.getPixels(pixels2, 0, b2.width, 0, 0, b2.width, b2.height)
66 var maxDiff = 0
67 for (i in 0 until pixels1.size) {
68 val redDiff = (Color.red(pixels1[i]) - Color.red(pixels2[i])).absoluteValue
69 val greenDiff = (Color.green(pixels1[i]) - Color.green(pixels2[i])).absoluteValue
70 val blueDiff = (Color.blue(pixels1[i]) - Color.blue(pixels2[i])).absoluteValue
71 maxDiff = max(maxDiff, max(redDiff, max(greenDiff, blueDiff)))
72 }
73 maxDiff
74 } else {
75 256
76 }
77 }
78 }
79
80 private fun assertScreenshotResult(result: GeckoResult<Bitmap>, comparisonImage: Bitmap) {
81 sessionRule.waitForResult(result).let {
82 assertThat("Screenshot is not null",
83 it, notNullValue())
84 assertThat("Widths are the same", comparisonImage.width, equalTo(it.width))
85 assertThat("Heights are the same", comparisonImage.height, equalTo(it.height))
86 assertThat("Byte counts are the same", comparisonImage.byteCount, equalTo(it.byteCount))
87 assertThat("Configs are the same", comparisonImage.config, equalTo(it.config))
88 assertThat("Images are almost identical",
89 imageElementDifference(comparisonImage, it), lessThanOrEqualTo(1))
90 }
91 }
92
93 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
94 @Test
95 fun capturePixelsSucceeds() {
96 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
97
98 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
99 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
100 @AssertCalled(count = 1)
101 override fun onFirstContentfulPaint(session: GeckoSession) {
102 }
103 })
104
105 sessionRule.display?.let {
106 assertScreenshotResult(it.capturePixels(), screenshotFile)
107 }
108 }
109
110 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
111 @Test
112 fun capturePixelsCanBeCalledMultipleTimes() {
113 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
114
115 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
116 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
117 @AssertCalled(count = 1)
118 override fun onFirstContentfulPaint(session: GeckoSession) {
119 }
120 })
121
122 sessionRule.display?.let {
123 val call1 = it.capturePixels()
124 val call2 = it.capturePixels()
125 val call3 = it.capturePixels()
126 assertScreenshotResult(call1, screenshotFile)
127 assertScreenshotResult(call2, screenshotFile)
128 assertScreenshotResult(call3, screenshotFile)
129 }
130 }
131
132 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
133 @Test
134 fun capturePixelsCompletesCompositorPausedRestarted() {
135 sessionRule.display?.let {
136 it.surfaceDestroyed()
137 val result = it.capturePixels()
138 val texture = SurfaceTexture(0)
139 texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
140 val surface = Surface(texture)
141 it.surfaceChanged(surface, SCREEN_WIDTH, SCREEN_HEIGHT)
142 sessionRule.waitForResult(result)
143 }
144 }
145
146 // This tests tries to catch problems like Bug 1644561.
147 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
148 @Test
149 fun capturePixelsStressTest() {
150 val screenshots = mutableListOf<GeckoResult<Bitmap>>()
151 sessionRule.display?.let {
152 for (i in 0..100) {
153 screenshots.add(it.capturePixels())
154 }
155
156 for (i in 0..50) {
157 sessionRule.waitForResult(screenshots[i])
158 }
159
160 it.surfaceDestroyed()
161 screenshots.add(it.capturePixels())
162 it.surfaceDestroyed()
163
164 val texture = SurfaceTexture(0)
165 texture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
166 val surface = Surface(texture)
167 it.surfaceChanged(surface, SCREEN_WIDTH, SCREEN_HEIGHT)
168
169 for (i in 0..100) {
170 screenshots.add(it.capturePixels())
171 }
172
173 for (i in 0..100) {
174 it.surfaceDestroyed()
175 screenshots.add(it.capturePixels())
176 val newTexture = SurfaceTexture(0)
177 newTexture.setDefaultBufferSize(SCREEN_WIDTH, SCREEN_HEIGHT)
178 val newSurface = Surface(newTexture)
179 it.surfaceChanged(newSurface, SCREEN_WIDTH, SCREEN_HEIGHT)
180 }
181
182 try {
183 for (result in screenshots) {
184 sessionRule.waitForResult(result)
185 }
186 } catch (ex: RuntimeException) {
187 // Rejecting the screenshot is fine
188 }
189 }
190 }
191
192 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
193 @Test(expected = IllegalStateException::class)
194 fun capturePixelsFailsCompositorPaused() {
195 sessionRule.display?.let {
196 it.surfaceDestroyed()
197 val result = it.capturePixels()
198 it.surfaceDestroyed()
199
200 sessionRule.waitForResult(result)
201 }
202 }
203
204 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
205 @Test
206 fun capturePixelsWhileSessionDeactivated() {
207 // TODO: Bug 1673955
208 assumeThat(sessionRule.env.isFission, equalTo(false))
209 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
210
211 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
212 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
213 @AssertCalled(count = 1)
214 override fun onFirstContentfulPaint(session: GeckoSession) {
215 }
216 })
217
218 sessionRule.session.setActive(false)
219
220 // Deactivating the session should trigger a flush state change
221 sessionRule.waitUntilCalled(object : Callbacks.ProgressDelegate {
222 @AssertCalled(count = 1)
223 override fun onSessionStateChange(session: GeckoSession,
224 sessionState: GeckoSession.SessionState) {}
225 })
226
227 sessionRule.display?.let {
228 assertScreenshotResult(it.capturePixels(), screenshotFile)
229 }
230 }
231
232 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
233 @Test
234 fun screenshotToBitmap() {
235 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
236
237 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
238 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
239 @AssertCalled(count = 1)
240 override fun onFirstContentfulPaint(session: GeckoSession) {
241 }
242 })
243
244 sessionRule.display?.let {
245 assertScreenshotResult(it.screenshot().capture(), screenshotFile)
246 }
247 }
248
249 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
250 @Test
251 fun screenshotScaledToSize() {
252 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
253
254 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
255 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
256 @AssertCalled(count = 1)
257 override fun onFirstContentfulPaint(session: GeckoSession) {
258 }
259 })
260
261 sessionRule.display?.let {
262 assertScreenshotResult(it.screenshot().size(SCREEN_WIDTH/2, SCREEN_HEIGHT/2).capture(), screenshotFile)
263 }
264 }
265
266 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
267 @Test
268 fun screenShotScaledWithScale() {
269 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
270
271 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
272 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
273 @AssertCalled(count = 1)
274 override fun onFirstContentfulPaint(session: GeckoSession) {
275 }
276 })
277
278 sessionRule.display?.let {
279 assertScreenshotResult(it.screenshot().scale(0.5f).capture(), screenshotFile)
280 }
281 }
282
283 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
284 @Test
285 fun screenShotScaledWithAspectPreservingSize() {
286 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
287
288 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
289 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
290 @AssertCalled(count = 1)
291 override fun onFirstContentfulPaint(session: GeckoSession) {
292 }
293 })
294
295 sessionRule.display?.let {
296 assertScreenshotResult(it.screenshot().aspectPreservingSize(SCREEN_WIDTH/2).capture(), screenshotFile)
297 }
298 }
299
300 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
301 @Test
302 fun recycleBitmap() {
303 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
304
305 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
306 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
307 @AssertCalled(count = 1)
308 override fun onFirstContentfulPaint(session: GeckoSession) {
309 }
310 })
311
312 sessionRule.display?.let {
313 val call1 = it.screenshot().capture()
314 assertScreenshotResult(call1, screenshotFile)
315 val call2 = it.screenshot().bitmap(call1.poll(1000)).capture()
316 assertScreenshotResult(call2, screenshotFile)
317 val call3 = it.screenshot().bitmap(call2.poll(1000)).capture()
318 assertScreenshotResult(call3, screenshotFile)
319 }
320 }
321
322 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
323 @Test
324 fun screenshotWholeRegion() {
325 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)
326
327 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
328 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
329 @AssertCalled(count = 1)
330 override fun onFirstContentfulPaint(session: GeckoSession) {
331 }
332 })
333
334 sessionRule.display?.let {
335 assertScreenshotResult(it.screenshot().source(0,0,SCREEN_WIDTH, SCREEN_HEIGHT).capture(), screenshotFile)
336 }
337 }
338
339 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
340 @Test
341 fun screenshotWholeRegionScaled() {
342 val screenshotFile = getComparisonScreenshot(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
343
344 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
345 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
346 @AssertCalled(count = 1)
347 override fun onFirstContentfulPaint(session: GeckoSession) {
348 }
349 })
350
351 sessionRule.display?.let {
352 assertScreenshotResult(it.screenshot()
353 .source(0,0,SCREEN_WIDTH, SCREEN_HEIGHT)
354 .size(SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
355 .capture(), screenshotFile)
356 }
357 }
358
359 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
360 @Test
361 fun screenshotQuarters() {
362 val res = InstrumentationRegistry.getInstrumentation().targetContext.resources
363 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
364 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
365 @AssertCalled(count = 1)
366 override fun onFirstContentfulPaint(session: GeckoSession) {
367 }
368 })
369
370 sessionRule.display?.let {
371 assertScreenshotResult(
372 it.screenshot()
373 .source(0,0,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
374 .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl))
375 assertScreenshotResult(
376 it.screenshot()
377 .source(SCREEN_WIDTH/2,SCREEN_HEIGHT/2,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
378 .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br))
379 }
380 }
381
382 @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
383 @Test
384 fun screenshotQuartersScaled() {
385 val res = InstrumentationRegistry.getInstrumentation().targetContext.resources
386 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
387 sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
388 @AssertCalled(count = 1)
389 override fun onFirstContentfulPaint(session: GeckoSession) {
390 }
391 })
392
393 sessionRule.display?.let {
394 assertScreenshotResult(
395 it.screenshot()
396 .source(0,0,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
397 .size(SCREEN_WIDTH/4, SCREEN_WIDTH/4)
398 .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_tl_scaled))
399 assertScreenshotResult(
400 it.screenshot()
401 .source(SCREEN_WIDTH/2,SCREEN_HEIGHT/2,SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
402 .size(SCREEN_WIDTH/4, SCREEN_WIDTH/4)
403 .capture(), BitmapFactory.decodeResource(res, R.drawable.colors_br_scaled))
404 }
405 }
406
407 @WithDisplay(height = BIG_SCREEN_HEIGHT, width = BIG_SCREEN_WIDTH)
408 @Test
409 fun giantScreenshot() {
410 sessionRule.session.loadTestPath(COLORS_HTML_PATH)
411 sessionRule.display?.screenshot()!!.source(0,0, BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT)
412 .size(BIG_SCREEN_WIDTH, BIG_SCREEN_HEIGHT)
413 .capture()
414 .exceptionally(OnExceptionListener<Throwable> { error: Throwable ->
415 Assert.assertTrue(error is OutOfMemoryError)
416 fromException(error)
417 })
418 }
419 }
420