diff --git a/examples/common/ExampleSettings.ts b/examples/common/ExampleSettings.ts index 5bf7c62..08b210a 100644 --- a/examples/common/ExampleSettings.ts +++ b/examples/common/ExampleSettings.ts @@ -19,8 +19,8 @@ export interface SnapshotOptions { clip?: { x: number; y: number; - w: number; - h: number; + width: number; + height: number; }; } diff --git a/examples/index.ts b/examples/index.ts index da88944..710424c 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -404,8 +404,8 @@ async function runAutomation( const clipRect = options?.clip || { x: testRoot.x, y: testRoot.y, - w: testRoot.w, - h: testRoot.h, + width: testRoot.w, + height: testRoot.h, }; const adjustedOptions = { @@ -413,8 +413,8 @@ async function runAutomation( clip: { x: Math.round(clipRect.x * logicalPixelRatio), y: Math.round(clipRect.y * logicalPixelRatio), - w: Math.round(clipRect.w * logicalPixelRatio), - h: Math.round(clipRect.h * logicalPixelRatio), + width: Math.round(clipRect.width * logicalPixelRatio), + height: Math.round(clipRect.height * logicalPixelRatio), }, }; diff --git a/visual-regression/src/index.ts b/visual-regression/src/index.ts index b8a5407..244d218 100644 --- a/visual-regression/src/index.ts +++ b/visual-regression/src/index.ts @@ -280,20 +280,13 @@ async function runTest(browserType: 'chromium') { async (test: string, options: SnapshotOptions) => { snapshotsTested++; - // Ensure clip dimensions are integers + // Ensure clip dimensions are integers (matches Playwright's clip shape + // exactly — caller is expected to send {x, y, width, height}). if (options.clip) { - for (const key of ['x', 'y', 'w', 'h']) { - // remap 'w' and 'h' to 'width' and 'height' - if (key === 'w' || key === 'h') { - options.clip[key === 'w' ? 'width' : 'height'] = Math.round( - options.clip[key as keyof typeof options.clip], - ); - } else { - options.clip[key as keyof typeof options.clip] = Math.round( - options.clip[key as keyof typeof options.clip], - ); - } - } + options.clip.x = Math.round(options.clip.x); + options.clip.y = Math.round(options.clip.y); + options.clip.width = Math.round(options.clip.width); + options.clip.height = Math.round(options.clip.height); } const subtestName = options.name ? `${test}_${options.name}` : test; @@ -303,39 +296,54 @@ async function runTest(browserType: 'chromium') { `${subtestName}-${snapshotIndex}${postfix ? `-${postfix}` : ''}.png`; const snapshotPath = path.join(snapshotSubDir, makeFilename()); - if (argv.capture) { - // Handle snapshot capturing - const captureResponse = await saveSnapshot( + // Wrap the capture/compare so an unexpected throw (e.g. Playwright + // rejecting a malformed clip) is counted as a failure instead of + // silently leaving the counters at zero. Pre-fix, exceptions here + // bypassed both snapshotsPassed and snapshotsFailed, so a runner full + // of thrown comparisons exited with `snapshotsFailed === 0` and CI + // reported success even though nothing actually ran. + try { + if (argv.capture) { + const captureResponse = await saveSnapshot( + page, + snapshotPath, + options, + subtestName, + snapshotIndex, + argv.overwrite, + ); + if (captureResponse === false) { + snapshotsSkipped++; + return; + } + + if (argv.overwrite) { + snapshotsPassed++; + return; + } + } + + const resp = await compareSnapshot( page, snapshotPath, options, subtestName, snapshotIndex, - argv.overwrite, ); - if (captureResponse === false) { - snapshotsSkipped++; - return; - } - - if (argv.overwrite) { + if (resp) { snapshotsPassed++; - return; + } else { + snapshotsFailed++; } - } - - // Handle snapshot comparison - const resp = await compareSnapshot( - page, - snapshotPath, - options, - subtestName, - snapshotIndex, - ); - if (resp) { - snapshotsPassed++; - } else { + } catch (err) { snapshotsFailed++; + console.log( + chalk.red.bold( + `FAILED! (${subtestName}-${snapshotIndex} threw: ${ + err instanceof Error ? err.message : String(err) + })`, + ), + ); } }, ); diff --git a/visual-regression/src/snapshot.ts b/visual-regression/src/snapshot.ts index e78baa1..e4b5084 100644 --- a/visual-regression/src/snapshot.ts +++ b/visual-regression/src/snapshot.ts @@ -9,7 +9,9 @@ import { PNG } from 'pngjs'; import pixelmatch from 'pixelmatch'; /** - * Keep in sync with `examples/common/ExampleSettings.ts` + * Keep in sync with `examples/common/ExampleSettings.ts`. + * `width`/`height` (not `w`/`h`) so the object can be handed directly to + * Playwright's `page.screenshot({ clip })`, which expects this exact shape. */ export interface SnapshotOptions { name?: string; @@ -93,9 +95,7 @@ export async function compareSnapshot( } const expectedPng = await fs.promises.readFile(snapshotPath); - const width = options.clip?.width || (1080 as number); - const height = options.clip?.height || (1920 as number); - const result = compareBuffers(actualPng, expectedPng, width, height); + const result = compareBuffers(actualPng, expectedPng); if (result.doesMatch) { console.log(chalk.green.bold('PASS!')); @@ -168,10 +168,7 @@ export async function saveFailedSnapshot( export function compareBuffers( actualImageBuffer: Buffer, expectedImageBuffer: Buffer, - width: number, - height: number, ): CompareResult { - const diff = new PNG({ width: width as number, height: height as number }); const actualImage = PNG.sync.read(actualImageBuffer); const expectedImage = PNG.sync.read(expectedImageBuffer); @@ -182,25 +179,31 @@ export function compareBuffers( return { doesMatch: false, diffImageBuffer: undefined, - reason: 'Image dimensions do not match', + reason: `Image dimensions do not match (actual ${actualImage.width}x${actualImage.height}, expected ${expectedImage.width}x${expectedImage.height})`, }; } + const width = actualImage.width; + const height = actualImage.height; + const diff = new PNG({ width, height }); + + // pixelmatch threshold is normalized YIQ color distance (0..1). 0.1 is the + // library default and the recommended value for catching meaningful UI + // changes while tolerating sub-pixel AA jitter. The previous 0.8 was so + // permissive that missing text on a solid background still passed. const count = pixelmatch( actualImage.data, expectedImage.data, - diff.data, width, height, - { threshold: 0.8 }, // Adjust threshold for sensitivity + { threshold: 0.1 }, ); const doesMatch = count === 0; return { doesMatch, - diffImageBuffer: doesMatch ? undefined : diff, reason: doesMatch ? undefined : `${count} pixels differ`, };