playwright/tests/playwright-test/golden.spec.ts
Andrey Lushnikov 6d82460a02
feat: implement a new image comparison function (#19166)
This patch implements a new image comparison function, codenamed
"ssim-cie94". The goal of the new comparison function is to cancel out
browser non-determenistic rendering.

To use the new comparison function:

```ts
await expect(page).toHaveScreenshot({
  comparator: 'ssim-cie94',
});
```

As of Nov 30, 2022, we identified the following sources of
non-determenistic rendering for Chromium:
- Anti-aliasing for certain shapes might be different due to the
  way skia rasterizes certain shapes.
- Color blending might be different on `x86` and `aarch64`
architectures.

The new function employs a few heuristics to fight these
differences.

Consider two non-equal image pixels `(r1, g1, b1)` and `(r2, g2, b2)`:
1. If the [CIE94] metric is less then 1.0, then we consider these pixels
   **EQUAL**. (The value `1.0` is the [just-noticeable difference] for
   [CIE94].). Otherwise, proceed to next step.
1. If all the 8 neighbors of the first pixel match its color, or
   if the 8 neighbors of the second pixel match its color, then these
   pixels are **DIFFERENT**. (In case of anti-aliasing, some of the
   direct neighbors have to be blended up or down.) Otherwise, proceed
   to next step.
1. If SSIM in some locality around the different pixels is more than
   0.99, then consider this pixels to be **EQUAL**. Otherwise, mark them
   as **DIFFERENT**. (Local SSIM for anti-aliased pixels turns out to be
   very close to 1.0).

[CIE94]: https://en.wikipedia.org/wiki/Color_difference#CIE94
[just-noticeable difference]:
https://en.wikipedia.org/wiki/Just-noticeable_difference
2022-12-02 15:22:05 -08:00

1036 lines
38 KiB
TypeScript

/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import colors from 'colors/safe';
import * as fs from 'fs';
import * as path from 'path';
import { test, expect, stripAnsi, createWhiteImage, paintBlackPixels } from './playwright-test-fixtures';
const files = {
'helper.ts': `
export const test = pwt.test.extend({
auto: [ async ({}, run, testInfo) => {
testInfo.snapshotSuffix = '';
await run();
}, { auto: true } ]
});
`
};
test('should support golden', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should work with non-txt extensions', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.csv': `1,2,3`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('1,2,4').toMatchSnapshot('snapshot.csv');
});
`
});
expect(result.exitCode).toBe(1);
expect(stripAnsi(result.output)).toContain(`1,2,34`);
});
test('should generate default name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', async ({ page }) => {
expect.soft('foo').toMatchSnapshot();
expect.soft('bar').toMatchSnapshot();
expect.soft(await page.screenshot({type: 'png'})).toMatchSnapshot();
expect.soft(await page.screenshot({type: 'jpeg'})).toMatchSnapshot();
expect.soft(Buffer.from([1,2,3,4])).toMatchSnapshot();
});
`
});
expect(result.exitCode).toBe(1);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.txt'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-2-actual.txt'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-3-actual.png'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-4-actual.jpg'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-5-actual.dat'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.txt'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-2.txt'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-3.png'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-4.jpg'))).toBe(true);
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-5.dat'))).toBe(true);
});
test('should compile with different option combinations', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
const { test } = pwt;
test('is a test', async ({ page }) => {
expect('foo').toMatchSnapshot();
expect('foo').toMatchSnapshot({ threshold: 0.2 });
expect('foo').toMatchSnapshot({ maxDiffPixelRatio: 0.2 });
expect('foo').toMatchSnapshot({ maxDiffPixels: 0.2 });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should fail on wrong golden', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Line1
Line2
Line3
Hello world line1
Line5
Line6
Line7`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
const data = [];
data.push('Line1');
data.push('Line22');
data.push('Line3');
data.push('Hi world line2');
data.push('Line5');
data.push('Line6');
data.push('Line7');
expect(data.join('\\n')).toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Line1');
expect(result.output).toContain('Line2' + colors.green('2'));
expect(result.output).toContain('line' + colors.reset(colors.strikethrough(colors.red('1'))) + colors.green('2'));
expect(result.output).toContain('Line3');
expect(result.output).toContain('Line5');
expect(result.output).toContain('Line7');
});
test('should write detailed failure result to an output folder', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world updated').toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(1);
const outputText = stripAnsi(result.output);
expect(outputText).toContain('Snapshot comparison failed:');
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.txt');
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.txt');
expect(outputText).toContain(`Expected: ${expectedSnapshotArtifactPath}`);
expect(outputText).toContain(`Received: ${actualSnapshotArtifactPath}`);
expect(fs.existsSync(expectedSnapshotArtifactPath)).toBe(true);
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(true);
});
test("doesn\'t create comparison artifacts in an output folder for passed negated snapshot matcher", async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world updated').not.toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(0);
const outputText = stripAnsi(result.output);
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.txt');
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.txt');
expect(outputText).not.toContain(`Expected: ${expectedSnapshotArtifactPath}`);
expect(outputText).not.toContain(`Received: ${actualSnapshotArtifactPath}`);
expect(fs.existsSync(expectedSnapshotArtifactPath)).toBe(false);
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(false);
});
test('should fail on same snapshots with negate matcher', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Snapshot comparison failed:');
expect(result.output).toContain('Expected result should be different from the actual one.');
});
test('should write missing expectations locally twice and continue', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt');
expect('Hello world2').toMatchSnapshot('snapshot2.txt');
console.log('Here we are!');
});
`
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const snapshot1OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`Error: A snapshot doesn't exist at ${snapshot1OutputPath}, writing actual`);
expect(fs.readFileSync(snapshot1OutputPath, 'utf-8')).toBe('Hello world');
const snapshot2OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot2.txt');
expect(result.output).toContain(`Error: A snapshot doesn't exist at ${snapshot2OutputPath}, writing actual`);
expect(fs.readFileSync(snapshot2OutputPath, 'utf-8')).toBe('Hello world2');
expect(result.output).toContain('Here we are!');
const stackLines = stripAnsi(result.output).split('\n').filter(line => line.includes(' at ')).filter(line => !line.includes(testInfo.outputPath()));
expect(result.output).toContain('a.spec.js:8');
expect(stackLines.length).toBe(0);
});
test('should not write missing expectations for negated matcher', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(1);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, matchers using ".not" won\'t write them automatically.`);
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
});
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`${snapshotOutputPath} does not match, writing actual.`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
});
test('should ignore text snapshot with the ignore-snapshots flag', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
});
`
}, { 'ignore-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(``);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe(EXPECTED_SNAPSHOT);
});
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').not.toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe(EXPECTED_SNAPSHOT);
});
test('should silently write missing expectations locally with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
const ACTUAL_SNAPSHOT = 'Hello world new';
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
});
test('should silently write missing expectations locally with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(1);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, matchers using ".not" won\'t write them automatically.`);
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
});
test('should match multiple snapshots', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot1.txt': `Snapshot1`,
'a.spec.js-snapshots/snapshot2.txt': `Snapshot2`,
'a.spec.js-snapshots/snapshot3.txt': `Snapshot3`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Snapshot1').toMatchSnapshot('snapshot1.txt');
expect('Snapshot2').toMatchSnapshot('snapshot2.txt');
expect('Snapshot3').toMatchSnapshot('snapshot3.txt');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should match snapshots from multiple projects', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
import * as path from 'path';
module.exports = { projects: [
{ testDir: path.join(__dirname, 'p1') },
{ testDir: path.join(__dirname, 'p2') },
]};
`,
'p1/a.spec.js': `
const { test } = require('../helper');
test('is a test', ({}) => {
expect('Snapshot1').toMatchSnapshot('snapshot.txt');
});
`,
'p1/a.spec.js-snapshots/snapshot.txt': `Snapshot1`,
'p2/a.spec.js': `
const { test } = require('../helper');
test('is a test', ({}) => {
expect('Snapshot2').toMatchSnapshot('snapshot.txt');
});
`,
'p2/a.spec.js-snapshots/snapshot.txt': `Snapshot2`,
});
expect(result.exitCode).toBe(0);
});
test('should use provided name', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/provided.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('provided.txt');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should use provided name via options', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/provided.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot({ name: 'provided.txt' });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should compare binary', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.dat': Buffer.from([1, 2, 3, 4]),
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from([1,2,3,4])).toMatchSnapshot('snapshot.dat');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should respect maxDiffPixels option', async ({ runInlineTest }) => {
const width = 20, height = 20;
const BAD_PIXELS = 120;
const image1 = createWhiteImage(width, height);
const image2 = paintBlackPixels(image1, BAD_PIXELS);
await test.step('make sure default comparison fails', async () => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
});
`
});
expect(stripAnsi(result.output)).toContain('120 pixels');
expect(stripAnsi(result.output)).toContain('ratio 0.30');
expect(result.exitCode).toBe(1);
});
expect((await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
maxDiffPixels: ${BAD_PIXELS}
});
});
`
})).exitCode, 'make sure maxDiffPixels option is respected').toBe(0);
expect((await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = { projects: [
{ expect: { toMatchSnapshot: { maxDiffPixels: ${BAD_PIXELS} } } },
]};
`,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
});
`
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
});
test('should respect maxDiffPixelRatio option', async ({ runInlineTest }) => {
const width = 20, height = 20;
const BAD_RATIO = 0.25;
const BAD_PIXELS = Math.floor(width * height * BAD_RATIO);
const image1 = createWhiteImage(width, height);
const image2 = paintBlackPixels(image1, BAD_PIXELS);
expect((await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
});
`
})).exitCode, 'make sure default comparison fails').toBe(1);
expect((await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
maxDiffPixelRatio: ${BAD_RATIO}
});
});
`
})).exitCode, 'make sure maxDiffPixelRatio option is respected').toBe(0);
expect((await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = { projects: [
{ expect: { toMatchSnapshot: { maxDiffPixelRatio: ${BAD_RATIO} } } },
]};
`,
'a.spec.js-snapshots/snapshot.png': image1,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
});
`
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
});
test('should compare PNG images', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64')).toMatchSnapshot('snapshot.png');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should compare different PNG images', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png');
});
`
});
const outputText = stripAnsi(result.output);
expect(result.exitCode).toBe(1);
expect(outputText).toContain('Screenshot comparison failed:');
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.png');
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.png');
const diffSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-diff.png');
expect(outputText).toContain(`Expected: ${expectedSnapshotArtifactPath}`);
expect(outputText).toContain(`Received: ${actualSnapshotArtifactPath}`);
expect(outputText).toContain(`Diff: ${diffSnapshotArtifactPath}`);
expect(fs.existsSync(expectedSnapshotArtifactPath)).toBe(true);
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(true);
expect(fs.existsSync(diffSnapshotArtifactPath)).toBe(true);
});
test('should respect threshold', async ({ runInlineTest }) => {
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': expected,
'a.spec.js-snapshots/snapshot2.png': expected,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png', { threshold: 0.2 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot2.png', { threshold: 0.3 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot({ name: 'snapshot2.png', threshold: 0.3 });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should respect project threshold', async ({ runInlineTest }) => {
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = { projects: [
{ expect: { toMatchSnapshot: { threshold: 0.2 } } },
]};
`,
'a.spec.js-snapshots/snapshot.png': expected,
'a.spec.js-snapshots/snapshot2.png': expected,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png');
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot2.png', { threshold: 0.3 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot({ name: 'snapshot2.png', threshold: 0.3 });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should respect comparator name', async ({ runInlineTest }) => {
const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png'));
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': expected,
'a.spec.js': `
const { test } = require('./helper');
test('should pass', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
threshold: 0,
comparator: 'ssim-cie94',
});
});
test('should fail', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
threshold: 0,
comparator: 'pixelmatch',
});
});
`
});
expect(result.exitCode).toBe(1);
expect(result.report.suites[0].specs[0].title).toBe('should pass');
expect(result.report.suites[0].specs[0].ok).toBe(true);
expect(result.report.suites[0].specs[1].title).toBe('should fail');
expect(result.report.suites[0].specs[1].ok).toBe(false);
});
test('should respect comparator in config', async ({ runInlineTest }) => {
const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png'));
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = {
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
projects: [
{
name: 'should-pass',
expect: {
toMatchSnapshot: {
comparator: 'ssim-cie94',
}
},
},
{
name: 'should-fail',
expect: {
toMatchSnapshot: {
comparator: 'pixelmatch',
}
},
},
],
};
`,
'__screenshots__/a.spec.js/snapshot.png': expected,
'a.spec.js': `
const { test } = require('./helper');
test('test', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
threshold: 0,
});
});
`
});
expect(result.exitCode).toBe(1);
expect(result.report.suites[0].specs[0].tests[0].projectName).toBe('should-pass');
expect(result.report.suites[0].specs[0].tests[0].status).toBe('expected');
expect(result.report.suites[0].specs[0].tests[1].projectName).toBe('should-fail');
expect(result.report.suites[0].specs[0].tests[1].status).toBe('unexpected');
});
test('should sanitize snapshot name when passed as string', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/-snapshot-.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');;
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('../../snapshot!.txt');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should write missing expectations with sanitized snapshot name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');;
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('../../snapshot!.txt');
});
`
});
expect(result.exitCode).toBe(1);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/-snapshot-.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('Hello world');
});
test('should join array of snapshot path segments without sanitizing', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/test/path/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(['test', 'path', 'snapshot.txt']);
});
`
});
expect(result.exitCode).toBe(0);
});
test('should use snapshotDir as snapshot base directory', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = {
snapshotDir: 'snaps',
};
`,
'snaps/a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should use snapshotDir with path segments as snapshot directory', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = {
snapshotDir: 'snaps',
};
`,
'snaps/tests/a.spec.js-snapshots/test/path/snapshot.txt': `Hello world`,
'tests/a.spec.js': `
const { test } = require('../helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(['test', 'path', 'snapshot.txt']);
});
`
});
expect(result.exitCode).toBe(0);
});
test('should use snapshotDir with nested test suite and path segments', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = {
snapshotDir: 'snaps',
};
`,
'snaps/path/to/tests/a.spec.js-snapshots/path/to/snapshot.txt': `Hello world`,
'path/to/tests/a.spec.js': `
const { test } = require('../../../helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(['path', 'to', 'snapshot.txt']);
});
`
});
expect(result.exitCode).toBe(0);
});
test('should use project snapshotDir over base snapshotDir', async ({ runInlineTest }) => {
const result = await runInlineTest({
'helper.ts': `
export const test = pwt.test.extend({
auto: [ async ({}, run, testInfo) => {
testInfo.snapshotSuffix = 'suffix';
await run();
}, { auto: true } ]
});
`,
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'foo',
snapshotDir: 'project_snaps',
},
],
snapshotDir: 'snaps',
};
`,
'project_snaps/a.spec.js-snapshots/test/path/snapshot-foo-suffix.txt': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(['test', 'path', 'snapshot.txt']);
});
`
});
expect(result.exitCode).toBe(0);
});
test('should update snapshot with array of path segments', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(['test', 'path', 'snapshot.txt']);
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/test/path/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('Hello world');
});
test('should attach expected/actual/diff with snapshot path', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/test/path/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': `
const { test } = require('./helper');
test.afterEach(async ({}, testInfo) => {
console.log('## ' + JSON.stringify(testInfo.attachments));
});
test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot(['test', 'path', 'snapshot.png']);
});
`
});
const outputText = stripAnsi(result.output);
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
for (const attachment of attachments) {
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
attachment.name = attachment.name.replace(/\\/g, '/');
}
expect(attachments).toEqual([
{
name: 'test/path/snapshot-expected.png',
contentType: 'image/png',
path: 'a-is-a-test/test/path/snapshot-expected.png'
},
{
name: 'test/path/snapshot-actual.png',
contentType: 'image/png',
path: 'a-is-a-test/test/path/snapshot-actual.png'
},
{
name: 'test/path/snapshot-diff.png',
contentType: 'image/png',
path: 'a-is-a-test/test/path/snapshot-diff.png'
}
]);
});
test('should attach expected/actual/diff', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': `
const { test } = require('./helper');
test.afterEach(async ({}, testInfo) => {
console.log('## ' + JSON.stringify(testInfo.attachments));
});
test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png');
});
`
});
const outputText = stripAnsi(result.output);
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
for (const attachment of attachments)
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
expect(attachments).toEqual([
{
name: 'snapshot-expected.png',
contentType: 'image/png',
path: 'a-is-a-test/snapshot-expected.png'
},
{
name: 'snapshot-actual.png',
contentType: 'image/png',
path: 'a-is-a-test/snapshot-actual.png'
},
{
name: 'snapshot-diff.png',
contentType: 'image/png',
path: 'a-is-a-test/snapshot-diff.png'
}
]);
});
test('should attach expected/actual and no diff', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mP8z8AARAwMjDAGACwBA/9IB8FMAAAAAElFTkSuQmCC', 'base64'),
'a.spec.js': `
const { test } = require('./helper');
test.afterEach(async ({}, testInfo) => {
console.log('## ' + JSON.stringify(testInfo.attachments));
});
test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png');
});
`
});
const outputText = stripAnsi(result.output);
expect(outputText).toContain('Expected an image 2px by 2px, received 1px by 1px.');
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
for (const attachment of attachments)
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
expect(attachments).toEqual([
{
name: 'snapshot-expected.png',
contentType: 'image/png',
path: 'a-is-a-test/snapshot-expected.png'
},
{
name: 'snapshot-actual.png',
contentType: 'image/png',
path: 'a-is-a-test/snapshot-actual.png'
},
]);
});
test('should fail with missing expectations and retries', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = { retries: 1 };
`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt');
});
`
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('Hello world');
});
test('should update expectations with retries', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...files,
'playwright.config.ts': `
module.exports = { retries: 1 };
`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt');
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('Hello world');
});
test('should allow comparing text with text without file extension', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot-no-extension': `Hello world`,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot-no-extension');
});
`
});
expect(result.exitCode).toBe(0);
});
test('should throw if a Promise was passed to toMatchSnapshot', async ({ runInlineTest }) => {
const result = await runInlineTest({
...files,
'a.spec.js': `
const { test } = require('./helper');
test('is a test', ({}) => {
expect(() => expect(new Promise(() => {})).toMatchSnapshot('foobar')).toThrow(/An unresolved Promise was passed to toMatchSnapshot\\(\\), make sure to resolve it by adding await to it./);
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});