test: fix "should capture navigation" flakiness on firefox-headed (#34291)

This commit is contained in:
Andrey Lushnikov 2025-01-10 16:51:28 -05:00 committed by GitHub
parent 423005a7ab
commit 2cd5003062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,14 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { browserTest as it, expect } from '../config/browserTest'; import { spawnSync } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { Page } from 'playwright-core'; import type { Page } from 'playwright-core';
import { spawnSync } from 'child_process';
import { PNG, jpegjs } from 'playwright-core/lib/utilsBundle'; import { PNG, jpegjs } from 'playwright-core/lib/utilsBundle';
import { registry } from '../../packages/playwright-core/lib/server'; 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'; import { parseTraceRaw } from '../config/utils';
const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath('javascript'); const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath('javascript');
@ -56,18 +55,11 @@ export class VideoPlayer {
this.videoHeight = parseInt(resolutionMatch![2], 10); 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) { for (let f = 1; f <= this.frames; ++f) {
const frame = this.frame(f, offset); const frame = this.frame(f, offset);
let hasColor = false; if (framePredicate(frame.data))
for (let i = 0; i < frame.data.length; i += 4) { return frame;
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);
} }
} }
@ -88,46 +80,52 @@ export class VideoPlayer {
} }
} }
function almostRed(r, g, b, alpha) { type Pixel = { r: number, g: number, b: number, alpha: number };
expect(r).toBeGreaterThan(185); type PixelPredicate = (pixel: Pixel) => boolean;
expect(g).toBeLessThan(70);
expect(b).toBeLessThan(70); function isAlmostRed({ r, g, b, alpha }: Pixel): boolean {
expect(alpha).toBe(255); return r > 185 && g < 70 && b < 70 && alpha === 255;
} }
function almostBlack(r, g, b, alpha) { function isAlmostBlack({ r, g, b, alpha }: Pixel): boolean {
expect(r).toBeLessThan(70); return r < 70 && g < 70 && b < 70 && alpha === 255;
expect(g).toBeLessThan(70);
expect(b).toBeLessThan(70);
expect(alpha).toBe(255);
} }
function almostGray(r, g, b, alpha) { function isAlmostGray({ r, g, b, alpha }: Pixel): boolean {
expect(r).toBeGreaterThan(70); return r > 70 && r < 185 &&
expect(g).toBeGreaterThan(70); g > 70 && g < 185 &&
expect(b).toBeGreaterThan(70); b > 70 && b < 185 &&
expect(r).toBeLessThan(185); alpha === 255;
expect(g).toBeLessThan(185);
expect(b).toBeLessThan(185);
expect(alpha).toBe(255);
} }
function expectAll(pixels: Buffer, rgbaPredicate) { function findPixel(pixels: Buffer, pixelPredicate: PixelPredicate): Pixel|undefined {
const checkPixel = i => { for (let i = 0, n = pixels.length; i < n; i += 4) {
const r = pixels[i]; const pixel = {
const g = pixels[i + 1]; r: pixels[i],
const b = pixels[i + 2]; g: pixels[i + 1],
const alpha = pixels[i + 3]; b: pixels[i + 2],
rgbaPredicate(r, g, b, alpha); alpha: pixels[i + 3],
}; };
try { if (pixelPredicate(pixel))
for (let i = 0, n = pixels.length; i < n; i += 4) return pixel;
checkPixel(i);
} catch (e) {
// Log pixel values on failure.
rewriteErrorMessage(e, e.message + `\n\nActual pixels=[${pixels.join(',')}]`);
throw e;
} }
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) { function findVideos(videoDir: string) {
@ -145,11 +143,11 @@ function expectRedFrames(videoFile: string, size: { width: number, height: numbe
{ {
const pixels = videoPlayer.seekLastFrame().data; const pixels = videoPlayer.seekLastFrame().data;
expectAll(pixels, almostRed); expectAll(pixels, isAlmostRed);
} }
{ {
const pixels = videoPlayer.seekLastFrame({ x: size.width - 20, y: 0 }).data; 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); expect(duration).toBeGreaterThan(0);
{ {
const pixels = videoPlayer.seekFirstNonEmptyFrame().data; // Find a frame with all almost-black pixels.
expectAll(pixels, almostBlack); const frame = videoPlayer.findFrame(pixels => everyPixel(pixels, isAlmostBlack));
expect(frame).not.toBeUndefined();
} }
{ {
const pixels = videoPlayer.seekLastFrame().data; 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; 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; const pixels = videoPlayer.seekLastFrame({ x: 0, y: 0 }).data;
expectAll(pixels, almostRed); expectAll(pixels, isAlmostRed);
} }
{ {
const pixels = videoPlayer.seekLastFrame({ x: 300, y: 0 }).data; const pixels = videoPlayer.seekLastFrame({ x: 300, y: 0 }).data;
expectAll(pixels, almostGray); expectAll(pixels, isAlmostGray);
} }
{ {
const pixels = videoPlayer.seekLastFrame({ x: 0, y: 200 }).data; const pixels = videoPlayer.seekLastFrame({ x: 0, y: 200 }).data;
expectAll(pixels, almostGray); expectAll(pixels, isAlmostGray);
} }
{ {
const pixels = videoPlayer.seekLastFrame({ x: 300, y: 200 }).data; 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; 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. // Bottom right corner should be part of the red border.
// However, headed browsers on mac have rounded corners, so offset by 10. // 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; 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) => { 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. // Bottom right corner should be part of the red border.
// However, headed browsers on mac have rounded corners, so offset by 10. // 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; 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) => { 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.width).toBe(size.width);
expect(image.height).toBe(size.height); expect(image.height).toBe(size.height);
const offset = size.width * size.height / 2 * 4 + size.width * 4 / 2; // Center should be red. 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);
}); });
}); });