mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-13 17:14:02 +03:00
parent
92045b7faf
commit
086333cd60
@ -3,7 +3,7 @@ id: test-snapshots
|
||||
title: "Visual comparisons"
|
||||
---
|
||||
|
||||
Playwright Test includes the ability to produce and visually compare screenshots using `expect(value).toMatchSnapshot(snapshotName)`. On first execution, Playwright test will generate reference screenshots. Subsequent runs will compare against the reference.
|
||||
Playwright Test includes the ability to produce and visually compare screenshots using `expect(value).toMatchSnapshot()`. On first execution, Playwright test will generate reference screenshots. Subsequent runs will compare against the reference.
|
||||
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
@ -11,7 +11,7 @@ const { test, expect } = require('@playwright/test');
|
||||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
expect(await page.screenshot()).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
@ -21,16 +21,16 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
expect(await page.screenshot()).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
When you run above for the first time, test runner will say:
|
||||
```
|
||||
Error: example.spec.ts-snapshots/landing-chromium-darwin.png is missing in snapshots, writing actual.
|
||||
Error: example.spec.ts-snapshots/example-test-1-chromium-darwin.png is missing in snapshots, writing actual.
|
||||
```
|
||||
|
||||
That's because there was no golden file for your `landing.png` snapshot. It is now created and is ready to be added to the repository. The name of the folder with the golden expectations starts with the name of your test file:
|
||||
That's because there was no golden file yet. It is now created and is ready to be added to the repository. The name of the folder with the golden expectations starts with the name of your test file:
|
||||
|
||||
```bash
|
||||
drwxr-xr-x 5 user group 160 Jun 4 11:46 .
|
||||
@ -39,7 +39,16 @@ drwxr-xr-x 6 user group 192 Jun 4 11:45 ..
|
||||
drwxr-xr-x 3 user group 96 Jun 4 11:46 example.spec.ts-snapshots
|
||||
```
|
||||
|
||||
Note the `chromium-darwin` in the generated snapshot file name - it contains the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
|
||||
The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
|
||||
- `example-test-1.png` - an auto-generated name of the snapshot. Alternatively you can specify snapshot name as the first argument of the `toMatchSnapshot()` method:
|
||||
```js js-flavor=js
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
```
|
||||
```js js-flavor=ts
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
```
|
||||
|
||||
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
|
||||
|
||||
If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots:
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
import type { Expect } from '../types';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import { compare } from './golden';
|
||||
import { addSuffixToFilePath } from '../util';
|
||||
import { addSuffixToFilePath, sanitizeForFilePath, trimLongString } from '../util';
|
||||
|
||||
// from expect/build/types
|
||||
type SyncExpectationResult = {
|
||||
@ -26,6 +26,7 @@ type SyncExpectationResult = {
|
||||
};
|
||||
|
||||
type NameOrSegments = string | string[];
|
||||
const SNAPSHOT_COUNTER = Symbol('noname-snapshot-counter');
|
||||
export function toMatchSnapshot(this: ReturnType<Expect['getState']>, received: Buffer | string, nameOrOptions: NameOrSegments | { name: NameOrSegments, threshold?: number }, optOptions: { threshold?: number } = {}): SyncExpectationResult {
|
||||
let options: { name: NameOrSegments, threshold?: number };
|
||||
const testInfo = currentTestInfo();
|
||||
@ -35,8 +36,14 @@ export function toMatchSnapshot(this: ReturnType<Expect['getState']>, received:
|
||||
options = { name: nameOrOptions, ...optOptions };
|
||||
else
|
||||
options = { ...nameOrOptions };
|
||||
if (!options.name)
|
||||
throw new Error(`toMatchSnapshot() requires a "name" parameter`);
|
||||
if (!options.name) {
|
||||
(testInfo as any)[SNAPSHOT_COUNTER] = ((testInfo as any)[SNAPSHOT_COUNTER] || 0) + 1;
|
||||
const fullTitleWithoutSpec = [
|
||||
...testInfo.titlePath.slice(1),
|
||||
(testInfo as any)[SNAPSHOT_COUNTER],
|
||||
].join(' ');
|
||||
options.name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + determineFileExtension(received);
|
||||
}
|
||||
|
||||
const projectThreshold = testInfo.project.expect?.toMatchSnapshot?.threshold;
|
||||
if (options.threshold === undefined && projectThreshold !== undefined)
|
||||
@ -65,3 +72,18 @@ export function toMatchSnapshot(this: ReturnType<Expect['getState']>, received:
|
||||
testInfo.attachments.push({ name: 'diff', contentType, path: diffPath });
|
||||
return { pass, message: () => message || '' };
|
||||
}
|
||||
|
||||
|
||||
function determineFileExtension(file: string | Buffer): string {
|
||||
if (typeof file === 'string')
|
||||
return '.txt';
|
||||
if (compareMagicBytes(file, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
||||
return '.png';
|
||||
if (compareMagicBytes(file, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01]))
|
||||
return '.jpg';
|
||||
return '.bin';
|
||||
}
|
||||
|
||||
function compareMagicBytes(file: Buffer, magicBytes: number[]): boolean {
|
||||
return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0;
|
||||
}
|
||||
|
@ -77,8 +77,8 @@ declare global {
|
||||
/**
|
||||
* Match snapshot
|
||||
*/
|
||||
toMatchSnapshot(options: {
|
||||
name: string | string[],
|
||||
toMatchSnapshot(options?: {
|
||||
name?: string | string[],
|
||||
threshold?: number
|
||||
}): R;
|
||||
/**
|
||||
|
@ -44,6 +44,46 @@ test('should support golden', async ({ runInlineTest }) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
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.bin'))).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.bin'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should compile without name', async ({ runTSC }) => {
|
||||
const result = await runTSC({
|
||||
'a.spec.js': `
|
||||
const { test, expect } = pwt;
|
||||
test('is a test', async ({ page }) => {
|
||||
expect('foo').toMatchSnapshot();
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should fail on wrong golden', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
@ -340,20 +380,6 @@ test('should use provided name', async ({ runInlineTest }) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should throw without a name', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot();
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('toMatchSnapshot() requires a "name" parameter');
|
||||
});
|
||||
|
||||
test('should use provided name via options', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
|
Loading…
Reference in New Issue
Block a user