Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/common/ExampleSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export interface SnapshotOptions {
clip?: {
x: number;
y: number;
w: number;
h: number;
width: number;
height: number;
};
}

Expand Down
8 changes: 4 additions & 4 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,17 +404,17 @@ 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 = {
...options,
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),
},
};

Expand Down
82 changes: 45 additions & 37 deletions visual-regression/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
})`,
),
);
}
},
);
Expand Down
25 changes: 14 additions & 11 deletions visual-regression/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!'));
Expand Down Expand Up @@ -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);

Expand All @@ -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`,
};
Expand Down
Loading