From 5879c7f3621ecf4eaf976775727f6f41aaf70dac Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Wed, 23 Feb 2022 14:17:37 -0700 Subject: [PATCH] chore: refactor toMatchSnapshot once again (#12313) Keep massaging code in preparation for `toHaveScreenshot`. References #9938 --- .../playwright-test/src/matchers/matchers.ts | 4 +- .../src/matchers/toBeTruthy.ts | 3 +- .../playwright-test/src/matchers/toEqual.ts | 2 +- .../src/matchers/toMatchSnapshot.ts | 352 +++++++++--------- .../src/matchers/toMatchText.ts | 23 +- packages/playwright-test/src/util.ts | 22 ++ tests/playwright-test/golden.spec.ts | 64 +--- .../playwright-test-fixtures.ts | 26 ++ 8 files changed, 245 insertions(+), 251 deletions(-) diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index 5392a2773d..53b48eee64 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -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 & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>; diff --git a/packages/playwright-test/src/matchers/toBeTruthy.ts b/packages/playwright-test/src/matchers/toBeTruthy.ts index a189da7ae7..2a048141f2 100644 --- a/packages/playwright-test/src/matchers/toBeTruthy.ts +++ b/packages/playwright-test/src/matchers/toBeTruthy.ts @@ -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, diff --git a/packages/playwright-test/src/matchers/toEqual.ts b/packages/playwright-test/src/matchers/toEqual.ts index 28c704be48..35b36ad4e2 100644 --- a/packages/playwright-test/src/matchers/toEqual.ts +++ b/packages/playwright-test/src/matchers/toEqual.ts @@ -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'; diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index fc24c41498..1c2019b1f7 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -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 { + 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); } diff --git a/packages/playwright-test/src/matchers/toMatchText.ts b/packages/playwright-test/src/matchers/toMatchText.ts index 42cdb09730..778ad37d69 100644 --- a/packages/playwright-test/src/matchers/toMatchText.ts +++ b/packages/playwright-test/src/matchers/toMatchText.ts @@ -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; -} diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index a2177853b7..42bb520678 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -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; +} + diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts index 47e19dc3a6..3f9aba99cc 100644 --- a/tests/playwright-test/golden.spec.ts +++ b/tests/playwright-test/golden.spec.ts @@ -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)]; -} diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 05fb19feca..6a02d0aae8 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -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); +}