mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: refactor toMatchSnapshot once again (#12313)
Keep massaging code in preparation for `toHaveScreenshot`. References #9938
This commit is contained in:
parent
e5c9d1e39f
commit
5879c7f362
@ -18,10 +18,10 @@ import { Locator, Page, APIResponse } from 'playwright-core';
|
||||
import { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
||||
import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils/utils';
|
||||
import type { Expect } from '../types';
|
||||
import { expectType } from '../util';
|
||||
import { expectType, callLogText } from '../util';
|
||||
import { toBeTruthy } from './toBeTruthy';
|
||||
import { toEqual } from './toEqual';
|
||||
import { callLogText, toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
|
||||
interface LocatorEx extends Locator {
|
||||
_expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { Expect } from '../types';
|
||||
import { expectType } from '../util';
|
||||
import { callLogText, currentExpectTimeout } from './toMatchText';
|
||||
import { expectType, callLogText, currentExpectTimeout } from '../util';
|
||||
|
||||
export async function toBeTruthy(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
import type { Expect } from '../types';
|
||||
import { expectType } from '../util';
|
||||
import { callLogText, currentExpectTimeout } from './toMatchText';
|
||||
import { callLogText, currentExpectTimeout } from '../util';
|
||||
|
||||
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
||||
const EXPECTED_LABEL = 'Expected';
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
|
||||
import type { Expect } from '../types';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import { mimeTypeToComparator, ComparatorResult, ImageComparatorOptions } from 'playwright-core/lib/utils/comparators';
|
||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString } from '../util';
|
||||
import { mimeTypeToComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText } from '../util';
|
||||
import { UpdateSnapshots } from '../types';
|
||||
import colors from 'colors/safe';
|
||||
import fs from 'fs';
|
||||
@ -34,68 +34,163 @@ type SyncExpectationResult = {
|
||||
type NameOrSegments = string | string[];
|
||||
const SNAPSHOT_COUNTER = Symbol('noname-snapshot-counter');
|
||||
|
||||
function parseMatchSnapshotOptions(
|
||||
testInfo: TestInfoImpl,
|
||||
anonymousSnapshotExtension: string,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions,
|
||||
optOptions: ImageComparatorOptions,
|
||||
) {
|
||||
let options: ImageComparatorOptions;
|
||||
let name: NameOrSegments | undefined;
|
||||
if (Array.isArray(nameOrOptions) || typeof nameOrOptions === 'string') {
|
||||
name = nameOrOptions;
|
||||
options = optOptions;
|
||||
} else {
|
||||
name = nameOrOptions.name;
|
||||
options = { ...nameOrOptions };
|
||||
delete (options as any).name;
|
||||
}
|
||||
if (!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(' ');
|
||||
name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension;
|
||||
class SnapshotHelper<T extends ImageComparatorOptions> {
|
||||
readonly testInfo: TestInfoImpl;
|
||||
readonly expectedPath: string;
|
||||
readonly snapshotPath: string;
|
||||
readonly actualPath: string;
|
||||
readonly diffPath: string;
|
||||
readonly mimeType: string;
|
||||
readonly updateSnapshots: UpdateSnapshots;
|
||||
readonly comparatorOptions: T;
|
||||
|
||||
constructor(
|
||||
testInfo: TestInfoImpl,
|
||||
anonymousSnapshotExtension: string,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & T,
|
||||
optOptions: T,
|
||||
) {
|
||||
let options: T;
|
||||
let name: NameOrSegments | undefined;
|
||||
if (Array.isArray(nameOrOptions) || typeof nameOrOptions === 'string') {
|
||||
name = nameOrOptions;
|
||||
options = optOptions;
|
||||
} else {
|
||||
name = nameOrOptions.name;
|
||||
options = { ...nameOrOptions };
|
||||
delete (options as any).name;
|
||||
}
|
||||
if (!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(' ');
|
||||
name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension;
|
||||
}
|
||||
|
||||
options = {
|
||||
...(testInfo.project.expect?.toMatchSnapshot || {}),
|
||||
...options,
|
||||
};
|
||||
|
||||
if (options.pixelCount !== undefined && options.pixelCount < 0)
|
||||
throw new Error('`pixelCount` option value must be non-negative integer');
|
||||
|
||||
if (options.pixelRatio !== undefined && (options.pixelRatio < 0 || options.pixelRatio > 1))
|
||||
throw new Error('`pixelRatio` option value must be between 0 and 1');
|
||||
|
||||
// sanitizes path if string
|
||||
const pathSegments = Array.isArray(name) ? name : [addSuffixToFilePath(name, '', undefined, true)];
|
||||
const snapshotPath = testInfo.snapshotPath(...pathSegments);
|
||||
const outputFile = testInfo.outputPath(...pathSegments);
|
||||
const expectedPath = addSuffixToFilePath(outputFile, '-expected');
|
||||
const actualPath = addSuffixToFilePath(outputFile, '-actual');
|
||||
const diffPath = addSuffixToFilePath(outputFile, '-diff');
|
||||
|
||||
let updateSnapshots = testInfo.config.updateSnapshots;
|
||||
if (updateSnapshots === 'missing' && testInfo.retry < testInfo.project.retries)
|
||||
updateSnapshots = 'none';
|
||||
const mimeType = mime.getType(path.basename(snapshotPath)) ?? 'application/octet-string';
|
||||
const comparator: Comparator = mimeTypeToComparator[mimeType];
|
||||
if (!comparator)
|
||||
throw new Error('Failed to find comparator with type ' + mimeType + ': ' + snapshotPath);
|
||||
|
||||
this.testInfo = testInfo;
|
||||
this.mimeType = mimeType;
|
||||
this.actualPath = actualPath;
|
||||
this.expectedPath = expectedPath;
|
||||
this.diffPath = diffPath;
|
||||
this.snapshotPath = snapshotPath;
|
||||
this.updateSnapshots = updateSnapshots;
|
||||
this.comparatorOptions = options;
|
||||
}
|
||||
|
||||
options = {
|
||||
...(testInfo.project.expect?.toMatchSnapshot || {}),
|
||||
...options,
|
||||
};
|
||||
handleMissingNegated() {
|
||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||
const message = `${this.snapshotPath} is missing in snapshots${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
||||
return {
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
pass: true,
|
||||
message: () => message,
|
||||
};
|
||||
}
|
||||
|
||||
if (options.pixelCount !== undefined && options.pixelCount < 0)
|
||||
throw new Error('`pixelCount` option value must be non-negative integer');
|
||||
handleDifferentNegated() {
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
return { pass: false, message: () => '' };
|
||||
}
|
||||
|
||||
if (options.pixelRatio !== undefined && (options.pixelRatio < 0 || options.pixelRatio > 1))
|
||||
throw new Error('`pixelRatio` option value must be between 0 and 1');
|
||||
handleMatchingNegated() {
|
||||
const message = [
|
||||
colors.red('Snapshot comparison failed:'),
|
||||
'',
|
||||
indent('Expected result should be different from the actual one.', ' '),
|
||||
].join('\n');
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
return { pass: true, message: () => message };
|
||||
}
|
||||
|
||||
// sanitizes path if string
|
||||
const pathSegments = Array.isArray(name) ? name : [addSuffixToFilePath(name, '', undefined, true)];
|
||||
const snapshotPath = testInfo.snapshotPath(...pathSegments);
|
||||
const outputFile = testInfo.outputPath(...pathSegments);
|
||||
const expectedPath = addSuffixToFilePath(outputFile, '-expected');
|
||||
const actualPath = addSuffixToFilePath(outputFile, '-actual');
|
||||
const diffPath = addSuffixToFilePath(outputFile, '-diff');
|
||||
handleMissing(actual: Buffer | string) {
|
||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||
if (isWriteMissingMode) {
|
||||
writeFileSync(this.snapshotPath, actual);
|
||||
writeFileSync(this.actualPath, actual);
|
||||
}
|
||||
const message = `${this.snapshotPath} is missing in snapshots${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||
if (this.updateSnapshots === 'all') {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return { pass: true, message: () => message };
|
||||
}
|
||||
if (this.updateSnapshots === 'missing') {
|
||||
this.testInfo._failWithError(serializeError(new Error(message)), false /* isHardError */);
|
||||
return { pass: true, message: () => '' };
|
||||
}
|
||||
return { pass: false, message: () => message };
|
||||
}
|
||||
|
||||
let updateSnapshots = testInfo.config.updateSnapshots;
|
||||
if (updateSnapshots === 'missing' && testInfo.retry < testInfo.project.retries)
|
||||
updateSnapshots = 'none';
|
||||
const mimeType = mime.getType(path.basename(snapshotPath)) ?? 'application/octet-string';
|
||||
const comparator = mimeTypeToComparator[mimeType];
|
||||
if (!comparator)
|
||||
throw new Error('Failed to find comparator with type ' + mimeType + ': ' + snapshotPath);
|
||||
return {
|
||||
snapshotPath,
|
||||
hasSnapshotFile: fs.existsSync(snapshotPath),
|
||||
expectedPath,
|
||||
actualPath,
|
||||
diffPath,
|
||||
comparator,
|
||||
mimeType,
|
||||
updateSnapshots,
|
||||
options,
|
||||
};
|
||||
handleDifferent(
|
||||
actual: Buffer | string | undefined,
|
||||
expected: Buffer | string | undefined,
|
||||
diff: Buffer | string | undefined,
|
||||
diffError: string | undefined,
|
||||
log: string[] | undefined,
|
||||
title = `Snapshot comparison failed:`) {
|
||||
const output = [
|
||||
colors.red(title),
|
||||
'',
|
||||
];
|
||||
if (diffError) {
|
||||
output.push(...[
|
||||
indent(diffError, ' '),
|
||||
'',
|
||||
]);
|
||||
}
|
||||
if (log)
|
||||
output.push(callLogText(log));
|
||||
|
||||
if (expected !== undefined) {
|
||||
writeFileSync(this.expectedPath, expected);
|
||||
this.testInfo.attachments.push({ name: 'expected', contentType: this.mimeType, path: this.expectedPath });
|
||||
output.push(`Expected: ${colors.yellow(this.expectedPath)}`);
|
||||
}
|
||||
if (actual !== undefined) {
|
||||
writeFileSync(this.actualPath, actual);
|
||||
this.testInfo.attachments.push({ name: 'actual', contentType: this.mimeType, path: this.actualPath });
|
||||
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
||||
}
|
||||
if (diff !== undefined) {
|
||||
writeFileSync(this.diffPath, diff);
|
||||
this.testInfo.attachments.push({ name: 'diff', contentType: this.mimeType, path: this.diffPath });
|
||||
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
||||
}
|
||||
return { pass: false, message: () => output.join('\n'), };
|
||||
}
|
||||
|
||||
handleMatching() {
|
||||
return { pass: true, message: () => '' };
|
||||
}
|
||||
}
|
||||
|
||||
export function toMatchSnapshot(
|
||||
@ -107,124 +202,34 @@ export function toMatchSnapshot(
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`toMatchSnapshot() must be called during the test`);
|
||||
const {
|
||||
options,
|
||||
updateSnapshots,
|
||||
snapshotPath,
|
||||
hasSnapshotFile,
|
||||
expectedPath,
|
||||
actualPath,
|
||||
diffPath,
|
||||
mimeType,
|
||||
comparator,
|
||||
} = parseMatchSnapshotOptions(testInfo, determineFileExtension(received), nameOrOptions, optOptions);
|
||||
if (!hasSnapshotFile)
|
||||
return commitMissingSnapshot(testInfo, received, snapshotPath, actualPath, updateSnapshots, this.isNot);
|
||||
const expected = fs.readFileSync(snapshotPath);
|
||||
const result = comparator(received, expected, options);
|
||||
return commitComparatorResult(
|
||||
testInfo,
|
||||
expected,
|
||||
received,
|
||||
result,
|
||||
mimeType,
|
||||
snapshotPath,
|
||||
expectedPath,
|
||||
actualPath,
|
||||
diffPath,
|
||||
updateSnapshots,
|
||||
this.isNot,
|
||||
);
|
||||
}
|
||||
const helper = new SnapshotHelper(testInfo, determineFileExtension(received), nameOrOptions, optOptions);
|
||||
const comparator: Comparator = mimeTypeToComparator[helper.mimeType];
|
||||
if (!comparator)
|
||||
throw new Error('Failed to find comparator with type ' + helper.mimeType + ': ' + helper.snapshotPath);
|
||||
|
||||
function commitMissingSnapshot(
|
||||
testInfo: TestInfoImpl,
|
||||
actual: Buffer | string,
|
||||
snapshotPath: string,
|
||||
actualPath: string,
|
||||
updateSnapshots: UpdateSnapshots,
|
||||
withNegateComparison: boolean,
|
||||
) {
|
||||
const isWriteMissingMode = updateSnapshots === 'all' || updateSnapshots === 'missing';
|
||||
const commonMissingSnapshotMessage = `${snapshotPath} is missing in snapshots`;
|
||||
if (withNegateComparison) {
|
||||
const message = `${commonMissingSnapshotMessage}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
||||
return { pass: true , message: () => message };
|
||||
if (this.isNot) {
|
||||
if (!fs.existsSync(helper.snapshotPath))
|
||||
return helper.handleMissingNegated();
|
||||
const isDifferent = !!comparator(received, fs.readFileSync(helper.snapshotPath), helper.comparatorOptions);
|
||||
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
|
||||
}
|
||||
if (isWriteMissingMode) {
|
||||
writeFileSync(snapshotPath, actual);
|
||||
writeFileSync(actualPath, actual);
|
||||
}
|
||||
const message = `${commonMissingSnapshotMessage}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||
if (updateSnapshots === 'all') {
|
||||
|
||||
if (!fs.existsSync(helper.snapshotPath))
|
||||
return helper.handleMissing(received);
|
||||
|
||||
const expected = fs.readFileSync(helper.snapshotPath);
|
||||
const result = comparator(received, expected, helper.comparatorOptions);
|
||||
if (!result)
|
||||
return helper.handleMatching();
|
||||
|
||||
if (helper.updateSnapshots === 'all') {
|
||||
writeFileSync(helper.snapshotPath, received);
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return { pass: true, message: () => message };
|
||||
}
|
||||
if (updateSnapshots === 'missing') {
|
||||
testInfo._failWithError(serializeError(new Error(message)), false /* isHardError */);
|
||||
return { pass: true, message: () => '' };
|
||||
}
|
||||
return { pass: false, message: () => message };
|
||||
}
|
||||
|
||||
function commitComparatorResult(
|
||||
testInfo: TestInfoImpl,
|
||||
expected: Buffer | string,
|
||||
actual: Buffer | string,
|
||||
result: ComparatorResult,
|
||||
mimeType: string,
|
||||
snapshotPath: string,
|
||||
expectedPath: string,
|
||||
actualPath: string,
|
||||
diffPath: string,
|
||||
updateSnapshots: UpdateSnapshots,
|
||||
withNegateComparison: boolean,
|
||||
) {
|
||||
if (!result) {
|
||||
const message = withNegateComparison ? [
|
||||
colors.red('Snapshot comparison failed:'),
|
||||
'',
|
||||
indent('Expected result should be different from the actual one.', ' '),
|
||||
].join('\n') : '';
|
||||
return { pass: true, message: () => message };
|
||||
console.log(helper.snapshotPath + ' does not match, writing actual.');
|
||||
return { pass: true, message: () => helper.snapshotPath + ' running with --update-snapshots, writing actual.' };
|
||||
}
|
||||
|
||||
if (withNegateComparison)
|
||||
return { pass: false, message: () => '' };
|
||||
|
||||
if (updateSnapshots === 'all') {
|
||||
writeFileSync(snapshotPath, actual);
|
||||
/* eslint-disable no-console */
|
||||
console.log(snapshotPath + ' does not match, writing actual.');
|
||||
return {
|
||||
pass: true,
|
||||
message: () => snapshotPath + ' running with --update-snapshots, writing actual.'
|
||||
};
|
||||
}
|
||||
|
||||
writeAttachment(testInfo, 'expected', mimeType, expectedPath, expected);
|
||||
writeAttachment(testInfo, 'actual', mimeType, actualPath, actual);
|
||||
if (result.diff)
|
||||
writeAttachment(testInfo, 'diff', mimeType, diffPath, result.diff);
|
||||
|
||||
const output = [
|
||||
colors.red(`Snapshot comparison failed:`),
|
||||
];
|
||||
if (result.errorMessage) {
|
||||
output.push('');
|
||||
output.push(indent(result.errorMessage, ' '));
|
||||
}
|
||||
output.push('');
|
||||
output.push(`Expected: ${colors.yellow(expectedPath)}`);
|
||||
output.push(`Received: ${colors.yellow(actualPath)}`);
|
||||
if (result.diff)
|
||||
output.push(` Diff: ${colors.yellow(diffPath)}`);
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () => output.join('\n'),
|
||||
};
|
||||
return helper.handleDifferent(received, expected, result.diff, result.errorMessage, undefined);
|
||||
}
|
||||
|
||||
function writeFileSync(aPath: string, content: Buffer | string) {
|
||||
@ -232,11 +237,6 @@ function writeFileSync(aPath: string, content: Buffer | string) {
|
||||
fs.writeFileSync(aPath, content);
|
||||
}
|
||||
|
||||
function writeAttachment(testInfo: TestInfoImpl, name: string, contentType: string, aPath: string, body: Buffer | string) {
|
||||
writeFileSync(aPath, body);
|
||||
testInfo.attachments.push({ name, contentType, path: aPath });
|
||||
}
|
||||
|
||||
function indent(lines: string, tab: string) {
|
||||
return lines.replace(/^(?=.+$)/gm, tab);
|
||||
}
|
||||
|
||||
@ -15,12 +15,10 @@
|
||||
*/
|
||||
|
||||
|
||||
import colors from 'colors/safe';
|
||||
import type { ExpectedTextValue } from 'playwright-core/lib/protocol/channels';
|
||||
import { isRegExp, isString } from 'playwright-core/lib/utils/utils';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import type { Expect } from '../types';
|
||||
import { expectType } from '../util';
|
||||
import { expectType, callLogText, currentExpectTimeout } from '../util';
|
||||
import {
|
||||
printReceivedStringContainExpectedResult,
|
||||
printReceivedStringContainExpectedSubstring
|
||||
@ -111,22 +109,3 @@ export function toExpectedTextValues(items: (string | RegExp)[], options: { matc
|
||||
normalizeWhiteSpace: options.normalizeWhiteSpace,
|
||||
}));
|
||||
}
|
||||
|
||||
export function callLogText(log: string[] | undefined): string {
|
||||
if (!log)
|
||||
return '';
|
||||
return `
|
||||
Call log:
|
||||
${colors.dim('- ' + (log || []).join('\n - '))}
|
||||
`;
|
||||
}
|
||||
|
||||
export function currentExpectTimeout(options: { timeout?: number }) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (options.timeout !== undefined)
|
||||
return options.timeout;
|
||||
let defaultExpectTimeout = testInfo?.project.expect?.timeout;
|
||||
if (typeof defaultExpectTimeout === 'undefined')
|
||||
defaultExpectTimeout = 5000;
|
||||
return defaultExpectTimeout;
|
||||
}
|
||||
|
||||
@ -17,11 +17,13 @@
|
||||
import util from 'util';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import colors from 'colors/safe';
|
||||
import type { TestError, Location } from './types';
|
||||
import { default as minimatch } from 'minimatch';
|
||||
import debug from 'debug';
|
||||
import { calculateSha1, isRegExp } from 'playwright-core/lib/utils/utils';
|
||||
import { isInternalFileName } from 'playwright-core/lib/utils/stackTrace';
|
||||
import { currentTestInfo } from './globals';
|
||||
|
||||
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core'));
|
||||
const EXPECT_PATH = path.dirname(require.resolve('expect'));
|
||||
@ -205,3 +207,23 @@ export function getContainedPath(parentPath: string, subPath: string = ''): stri
|
||||
}
|
||||
|
||||
export const debugTest = debug('pw:test');
|
||||
|
||||
export function callLogText(log: string[] | undefined): string {
|
||||
if (!log)
|
||||
return '';
|
||||
return `
|
||||
Call log:
|
||||
${colors.dim('- ' + (log || []).join('\n - '))}
|
||||
`;
|
||||
}
|
||||
|
||||
export function currentExpectTimeout(options: { timeout?: number }) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (options.timeout !== undefined)
|
||||
return options.timeout;
|
||||
let defaultExpectTimeout = testInfo?.project.expect?.timeout;
|
||||
if (typeof defaultExpectTimeout === 'undefined')
|
||||
defaultExpectTimeout = 5000;
|
||||
return defaultExpectTimeout;
|
||||
}
|
||||
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
import colors from 'colors/safe';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PNG } from 'pngjs';
|
||||
import { test, expect, stripAnsi } from './playwright-test-fixtures';
|
||||
import { test, expect, stripAnsi, createWhiteImage, paintBlackPixels } from './playwright-test-fixtures';
|
||||
|
||||
const files = {
|
||||
'helper.ts': `
|
||||
@ -167,21 +166,6 @@ test("doesn\'t create comparison artifacts in an output folder for passed negate
|
||||
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(false);
|
||||
});
|
||||
|
||||
test('should pass on different 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 updated').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should fail on same snapshots with negate matcher', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
@ -443,7 +427,8 @@ test('should throw for invalid pixelRatio values', async ({ runInlineTest }) =>
|
||||
test('should respect pixelCount option', async ({ runInlineTest }) => {
|
||||
const width = 20, height = 20;
|
||||
const BAD_PIXELS = 120;
|
||||
const [image1, image2] = createImagesWithDifferentPixels(width, height, BAD_PIXELS);
|
||||
const image1 = createWhiteImage(width, height);
|
||||
const image2 = paintBlackPixels(image1, BAD_PIXELS);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
@ -488,8 +473,10 @@ test('should respect pixelCount option', async ({ runInlineTest }) => {
|
||||
|
||||
test('should respect pixelRatio option', async ({ runInlineTest }) => {
|
||||
const width = 20, height = 20;
|
||||
const BAD_PERCENT = 0.25;
|
||||
const [image1, image2] = createImagesWithDifferentPixels(width, height, width * height * BAD_PERCENT);
|
||||
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,
|
||||
@ -509,7 +496,7 @@ test('should respect pixelRatio option', async ({ runInlineTest }) => {
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
pixelRatio: ${BAD_PERCENT}
|
||||
pixelRatio: ${BAD_RATIO}
|
||||
});
|
||||
});
|
||||
`
|
||||
@ -519,7 +506,7 @@ test('should respect pixelRatio option', async ({ runInlineTest }) => {
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toMatchSnapshot: { pixelRatio: ${BAD_PERCENT} } } },
|
||||
{ expect: { toMatchSnapshot: { pixelRatio: ${BAD_RATIO} } } },
|
||||
]};
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
@ -534,9 +521,10 @@ test('should respect pixelRatio option', async ({ runInlineTest }) => {
|
||||
|
||||
test('should satisfy both pixelRatio and pixelCount', async ({ runInlineTest }) => {
|
||||
const width = 20, height = 20;
|
||||
const BAD_PERCENT = 0.25;
|
||||
const BAD_COUNT = Math.floor(width * height * BAD_PERCENT);
|
||||
const [image1, image2] = createImagesWithDifferentPixels(width, height, BAD_COUNT);
|
||||
const BAD_RATIO = 0.25;
|
||||
const BAD_COUNT = Math.floor(width * height * BAD_RATIO);
|
||||
const image1 = createWhiteImage(width, height);
|
||||
const image2 = paintBlackPixels(image1, BAD_COUNT);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
@ -557,7 +545,7 @@ test('should satisfy both pixelRatio and pixelCount', async ({ runInlineTest })
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
pixelCount: ${Math.floor(BAD_COUNT / 2)},
|
||||
pixelRatio: ${BAD_PERCENT},
|
||||
pixelRatio: ${BAD_RATIO},
|
||||
});
|
||||
});
|
||||
`
|
||||
@ -571,7 +559,7 @@ test('should satisfy both pixelRatio and pixelCount', async ({ runInlineTest })
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
pixelCount: ${BAD_COUNT},
|
||||
pixelRatio: ${BAD_PERCENT / 2},
|
||||
pixelRatio: ${BAD_RATIO / 2},
|
||||
});
|
||||
});
|
||||
`
|
||||
@ -585,7 +573,7 @@ test('should satisfy both pixelRatio and pixelCount', async ({ runInlineTest })
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
pixelCount: ${BAD_COUNT},
|
||||
pixelRatio: ${BAD_PERCENT},
|
||||
pixelRatio: ${BAD_RATIO},
|
||||
});
|
||||
});
|
||||
`
|
||||
@ -635,7 +623,6 @@ test('should compare different PNG images', async ({ runInlineTest }, testInfo)
|
||||
});
|
||||
|
||||
test('should respect threshold', async ({ runInlineTest }) => {
|
||||
test.skip(!!process.env.PW_USE_BLINK_DIFF);
|
||||
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({
|
||||
@ -656,7 +643,6 @@ test('should respect threshold', async ({ runInlineTest }) => {
|
||||
});
|
||||
|
||||
test('should respect project threshold', async ({ runInlineTest }) => {
|
||||
test.skip(!!process.env.PW_USE_BLINK_DIFF);
|
||||
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({
|
||||
@ -1004,21 +990,3 @@ test('should allow comparing text with text without file extension', async ({ ru
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
function createImagesWithDifferentPixels(width: number, height: number, differentPixels: number): [Buffer, Buffer] {
|
||||
const image1 = new PNG({ width, height });
|
||||
const image2 = new PNG({ width, height });
|
||||
// Make both images red.
|
||||
for (let i = 0; i < width * height; ++i) {
|
||||
image1.data[i * 4] = 255; // red
|
||||
image1.data[i * 4 + 3] = 255; // opacity
|
||||
image2.data[i * 4] = 255; // red
|
||||
image2.data[i * 4 + 3] = 255; // opacity
|
||||
}
|
||||
// Color some pixels blue.
|
||||
for (let i = 0; i < differentPixels; ++i) {
|
||||
image1.data[i * 4] = 0; // red
|
||||
image1.data[i * 4 + 2] = 255; // blue
|
||||
}
|
||||
return [PNG.sync.write(image1), PNG.sync.write(image2)];
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import type { JSONReport, JSONReportSuite } from '@playwright/test/src/reporters
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { PNG } from 'pngjs';
|
||||
import rimraf from 'rimraf';
|
||||
import { promisify } from 'util';
|
||||
import { CommonFixtures, commonFixtures } from '../config/commonFixtures';
|
||||
@ -265,3 +266,28 @@ export function countTimes(s: string, sub: string): number {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createImage(width: number, height: number, r: number = 0, g: number = 0, b: number = 0): Buffer {
|
||||
const image = new PNG({ width, height });
|
||||
// Make both images red.
|
||||
for (let i = 0; i < width * height; ++i) {
|
||||
image.data[i * 4 + 0] = r;
|
||||
image.data[i * 4 + 1] = g;
|
||||
image.data[i * 4 + 2] = b;
|
||||
image.data[i * 4 + 3] = 255;
|
||||
}
|
||||
return PNG.sync.write(image);
|
||||
}
|
||||
|
||||
export function createWhiteImage(width: number, height: number) {
|
||||
return createImage(width, height, 255, 255, 255);
|
||||
}
|
||||
|
||||
export function paintBlackPixels(image: Buffer, blackPixelsCount: number): Buffer {
|
||||
image = PNG.sync.read(image);
|
||||
for (let i = 0; i < blackPixelsCount; ++i) {
|
||||
for (let j = 0; j < 3; ++j)
|
||||
image.data[i * 4 + j] = 0;
|
||||
}
|
||||
return PNG.sync.write(image);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user