diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index f8446d0e51..d858b9db2a 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import { browserTest as it, expect } from '../config/browserTest'; +import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import type { Page } from 'playwright-core'; -import { spawnSync } from 'child_process'; import { PNG, jpegjs } from 'playwright-core/lib/utilsBundle'; import { registry } from '../../packages/playwright-core/lib/server'; -import { rewriteErrorMessage } from '../../packages/playwright-core/lib/utils/stackTrace'; +import { expect, browserTest as it } from '../config/browserTest'; import { parseTraceRaw } from '../config/utils'; const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath('javascript'); @@ -56,18 +55,11 @@ export class VideoPlayer { this.videoHeight = parseInt(resolutionMatch![2], 10); } - seekFirstNonEmptyFrame(offset?: { x: number, y: number }): any | undefined { + findFrame(framePredicate: (pixels: Buffer) => boolean, offset?: { x: number, y: number }): any |undefined { for (let f = 1; f <= this.frames; ++f) { const frame = this.frame(f, offset); - let hasColor = false; - for (let i = 0; i < frame.data.length; i += 4) { - if (frame.data[i + 0] < 230 || frame.data[i + 1] < 230 || frame.data[i + 2] < 230) { - hasColor = true; - break; - } - } - if (hasColor) - return this.frame(f, offset); + if (framePredicate(frame.data)) + return frame; } } @@ -88,46 +80,52 @@ export class VideoPlayer { } } -function almostRed(r, g, b, alpha) { - expect(r).toBeGreaterThan(185); - expect(g).toBeLessThan(70); - expect(b).toBeLessThan(70); - expect(alpha).toBe(255); +type Pixel = { r: number, g: number, b: number, alpha: number }; +type PixelPredicate = (pixel: Pixel) => boolean; + +function isAlmostRed({ r, g, b, alpha }: Pixel): boolean { + return r > 185 && g < 70 && b < 70 && alpha === 255; } -function almostBlack(r, g, b, alpha) { - expect(r).toBeLessThan(70); - expect(g).toBeLessThan(70); - expect(b).toBeLessThan(70); - expect(alpha).toBe(255); +function isAlmostBlack({ r, g, b, alpha }: Pixel): boolean { + return r < 70 && g < 70 && b < 70 && alpha === 255; } -function almostGray(r, g, b, alpha) { - expect(r).toBeGreaterThan(70); - expect(g).toBeGreaterThan(70); - expect(b).toBeGreaterThan(70); - expect(r).toBeLessThan(185); - expect(g).toBeLessThan(185); - expect(b).toBeLessThan(185); - expect(alpha).toBe(255); +function isAlmostGray({ r, g, b, alpha }: Pixel): boolean { + return r > 70 && r < 185 && + g > 70 && g < 185 && + b > 70 && b < 185 && + alpha === 255; } -function expectAll(pixels: Buffer, rgbaPredicate) { - const checkPixel = i => { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const alpha = pixels[i + 3]; - rgbaPredicate(r, g, b, alpha); - }; - try { - for (let i = 0, n = pixels.length; i < n; i += 4) - checkPixel(i); - } catch (e) { - // Log pixel values on failure. - rewriteErrorMessage(e, e.message + `\n\nActual pixels=[${pixels.join(',')}]`); - throw e; +function findPixel(pixels: Buffer, pixelPredicate: PixelPredicate): Pixel|undefined { + for (let i = 0, n = pixels.length; i < n; i += 4) { + const pixel = { + r: pixels[i], + g: pixels[i + 1], + b: pixels[i + 2], + alpha: pixels[i + 3], + }; + if (pixelPredicate(pixel)) + return pixel; } + return undefined; +} + +function everyPixel(pixels: Buffer, pixelPredicate: PixelPredicate) { + const badPixel = findPixel(pixels, pixel => !pixelPredicate(pixel)); + return !badPixel; +} + +function expectAll(pixels: Buffer, pixelPredicate: PixelPredicate) { + const badPixel = findPixel(pixels, pixel => !pixelPredicate(pixel)); + if (!badPixel) + return; + const rgba = [badPixel.r, badPixel.g, badPixel.b, badPixel.alpha].join(', '); + throw new Error([ + `Expected all pixels to satisfy ${pixelPredicate.name}, found bad pixel (${rgba})`, + `Actual pixels=[${pixels.join(',')}]`, + ].join('\n')); } function findVideos(videoDir: string) { @@ -145,11 +143,11 @@ function expectRedFrames(videoFile: string, size: { width: number, height: numbe { const pixels = videoPlayer.seekLastFrame().data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } { const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: 0 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } } @@ -399,13 +397,14 @@ it.describe('screencast', () => { expect(duration).toBeGreaterThan(0); { - const pixels = videoPlayer.seekFirstNonEmptyFrame().data; - expectAll(pixels, almostBlack); + // Find a frame with all almost-black pixels. + const frame = videoPlayer.findFrame(pixels => everyPixel(pixels, isAlmostBlack)); + expect(frame).not.toBeUndefined(); } { const pixels = videoPlayer.seekLastFrame().data; - expectAll(pixels, almostGray); + expectAll(pixels, isAlmostGray); } }); @@ -435,7 +434,7 @@ it.describe('screencast', () => { { const pixels = videoPlayer.seekLastFrame({ x: 95, y: 45 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } }); @@ -506,19 +505,19 @@ it.describe('screencast', () => { { const pixels = videoPlayer.seekLastFrame({ x: 0, y: 0 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } { const pixels = videoPlayer.seekLastFrame({ x: 300, y: 0 }).data; - expectAll(pixels, almostGray); + expectAll(pixels, isAlmostGray); } { const pixels = videoPlayer.seekLastFrame({ x: 0, y: 200 }).data; - expectAll(pixels, almostGray); + expectAll(pixels, isAlmostGray); } { const pixels = videoPlayer.seekLastFrame({ x: 300, y: 200 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } }); @@ -603,7 +602,7 @@ it.describe('screencast', () => { { const pixels = videoPlayer.seekLastFrame().data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); } }); @@ -754,7 +753,7 @@ it.describe('screencast', () => { // Bottom right corner should be part of the red border. // However, headed browsers on mac have rounded corners, so offset by 10. const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: size.height - 20 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); }); it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell }, testInfo) => { @@ -791,7 +790,7 @@ it.describe('screencast', () => { // Bottom right corner should be part of the red border. // However, headed browsers on mac have rounded corners, so offset by 10. const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: size.height - 20 }).data; - expectAll(pixels, almostRed); + expectAll(pixels, isAlmostRed); }); it('should work with video+trace', async ({ browser, trace, headless, browserName, isHeadlessShell }, testInfo) => { @@ -827,7 +826,13 @@ it.describe('screencast', () => { expect(image.width).toBe(size.width); expect(image.height).toBe(size.height); const offset = size.width * size.height / 2 * 4 + size.width * 4 / 2; // Center should be red. - almostRed(image.data.readUInt8(offset), image.data.readUInt8(offset + 1), image.data.readUInt8(offset + 2), image.data.readUInt8(offset + 3)); + const pixel: Pixel = { + r: image.data.readUInt8(offset), + g: image.data.readUInt8(offset + 1), + b: image.data.readUInt8(offset + 2), + alpha: image.data.readUInt8(offset + 3), + }; + expect(isAlmostRed(pixel)).toBe(true); }); });