chore: simplify types handling in toHaveScreenshot (#29374)

This commit is contained in:
Pavel Feldman 2024-02-05 19:07:30 -08:00 committed by GitHub
parent fb29d90052
commit 20699c36ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 164 additions and 195 deletions

View file

@ -62,14 +62,13 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
path?: string, path?: string,
}; };
type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'screenshotOptions' | 'locator' | 'expected'> & { export type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'locator' | 'expected' | 'mask'> & {
expected?: Buffer, expected?: Buffer,
locator?: Locator, locator?: api.Locator,
isNot: boolean, isNot: boolean,
screenshotOptions: Omit<channels.PageExpectScreenshotOptions['screenshotOptions'], 'mask'> & { mask?: Locator[] } mask?: api.Locator[],
}; };
export class Page extends ChannelOwner<channels.PageChannel> implements api.Page { export class Page extends ChannelOwner<channels.PageChannel> implements api.Page {
private _browserContext: BrowserContext; private _browserContext: BrowserContext;
_ownedContext: BrowserContext | undefined; _ownedContext: BrowserContext | undefined;
@ -547,22 +546,19 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
} }
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> { async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> {
const mask = options.screenshotOptions?.mask ? options.screenshotOptions?.mask.map(locator => ({ const mask = options?.mask ? options?.mask.map(locator => ({
frame: locator._frame._channel, frame: (locator as Locator)._frame._channel,
selector: locator._selector, selector: (locator as Locator)._selector,
})) : undefined; })) : undefined;
const locator = options.locator ? { const locator = options.locator ? {
frame: options.locator._frame._channel, frame: (options.locator as Locator)._frame._channel,
selector: options.locator._selector, selector: (options.locator as Locator)._selector,
} : undefined; } : undefined;
return await this._channel.expectScreenshot({ return await this._channel.expectScreenshot({
...options, ...options,
isNot: !!options.isNot, isNot: !!options.isNot,
locator, locator,
screenshotOptions: { mask,
...options.screenshotOptions,
mask,
}
}); });
} }

View file

@ -1059,26 +1059,22 @@ scheme.PageExpectScreenshotParams = tObject({
frame: tChannel(['Frame']), frame: tChannel(['Frame']),
selector: tString, selector: tString,
})), })),
comparatorOptions: tOptional(tObject({ comparator: tOptional(tString),
comparator: tOptional(tString), maxDiffPixels: tOptional(tNumber),
maxDiffPixels: tOptional(tNumber), maxDiffPixelRatio: tOptional(tNumber),
maxDiffPixelRatio: tOptional(tNumber), threshold: tOptional(tNumber),
threshold: tOptional(tNumber), fullPage: tOptional(tBoolean),
})), clip: tOptional(tType('Rect')),
screenshotOptions: tOptional(tObject({ omitBackground: tOptional(tBoolean),
fullPage: tOptional(tBoolean), caret: tOptional(tEnum(['hide', 'initial'])),
clip: tOptional(tType('Rect')), animations: tOptional(tEnum(['disabled', 'allow'])),
omitBackground: tOptional(tBoolean), scale: tOptional(tEnum(['css', 'device'])),
caret: tOptional(tEnum(['hide', 'initial'])), mask: tOptional(tArray(tObject({
animations: tOptional(tEnum(['disabled', 'allow'])), frame: tChannel(['Frame']),
scale: tOptional(tEnum(['css', 'device'])), selector: tString,
mask: tOptional(tArray(tObject({ }))),
frame: tChannel(['Frame']), maskColor: tOptional(tString),
selector: tString, style: tOptional(tString),
}))),
maskColor: tOptional(tString),
style: tOptional(tString),
})),
}); });
scheme.PageExpectScreenshotResult = tObject({ scheme.PageExpectScreenshotResult = tObject({
diff: tOptional(tBinary), diff: tOptional(tBinary),

View file

@ -179,7 +179,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
} }
async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> { async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> {
const mask: { frame: Frame, selector: string }[] = (params.screenshotOptions?.mask || []).map(({ frame, selector }) => ({ const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
frame: (frame as FrameDispatcher)._object, frame: (frame as FrameDispatcher)._object,
selector, selector,
})); }));
@ -190,14 +190,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
return await this._page.expectScreenshot(metadata, { return await this._page.expectScreenshot(metadata, {
...params, ...params,
locator, locator,
comparatorOptions: { mask,
...params.comparatorOptions,
_comparator: params.comparatorOptions?.comparator,
},
screenshotOptions: {
...params.screenshotOptions,
mask,
},
}); });
} }

View file

@ -110,7 +110,7 @@ type EmulatedMedia = {
forcedColors: types.ForcedColors; forcedColors: types.ForcedColors;
}; };
type ExpectScreenshotOptions = { type ExpectScreenshotOptions = ImageComparatorOptions & ScreenshotOptions & {
timeout?: number, timeout?: number,
expected?: Buffer, expected?: Buffer,
isNot?: boolean, isNot?: boolean,
@ -118,8 +118,6 @@ type ExpectScreenshotOptions = {
frame: frames.Frame, frame: frames.Frame,
selector: string, selector: string,
}, },
comparatorOptions?: ImageComparatorOptions,
screenshotOptions?: ScreenshotOptions,
}; };
export class Page extends SdkObject { export class Page extends SdkObject {
@ -537,11 +535,11 @@ export class Page extends SdkObject {
async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions = {}): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[] }> { async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions = {}): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[] }> {
const locator = options.locator; const locator = options.locator;
const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => { const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => {
return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options.screenshotOptions || {}); return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {});
} : async (progress: Progress, timeout: number) => { } : async (progress: Progress, timeout: number) => {
await this.performLocatorHandlersCheckpoint(progress); await this.performLocatorHandlersCheckpoint(progress);
await this.mainFrame().rafrafTimeout(timeout); await this.mainFrame().rafrafTimeout(timeout);
return await this._screenshotter.screenshotPage(progress, options.screenshotOptions || {}); return await this._screenshotter.screenshotPage(progress, options || {});
}; };
const comparator = getComparator('image/png'); const comparator = getComparator('image/png');
@ -549,7 +547,7 @@ export class Page extends SdkObject {
if (!options.expected && options.isNot) if (!options.expected && options.isNot)
return { errorMessage: '"not" matcher requires expected result' }; return { errorMessage: '"not" matcher requires expected result' };
try { try {
const format = validateScreenshotOptions(options.screenshotOptions || {}); const format = validateScreenshotOptions(options || {});
if (format !== 'png') if (format !== 'png')
throw new Error('Only PNG screenshots are supported'); throw new Error('Only PNG screenshots are supported');
} catch (error) { } catch (error) {
@ -562,7 +560,7 @@ export class Page extends SdkObject {
diff?: Buffer, diff?: Buffer,
} | undefined = undefined; } | undefined = undefined;
const areEqualScreenshots = (actual: Buffer | undefined, expected: Buffer | undefined, previous: Buffer | undefined) => { const areEqualScreenshots = (actual: Buffer | undefined, expected: Buffer | undefined, previous: Buffer | undefined) => {
const comparatorResult = actual && expected ? comparator(actual, expected, options.comparatorOptions) : undefined; const comparatorResult = actual && expected ? comparator(actual, expected, options) : undefined;
if (comparatorResult !== undefined && !!comparatorResult === !!options.isNot) if (comparatorResult !== undefined && !!comparatorResult === !!options.isNot)
return true; return true;
if (comparatorResult) if (comparatorResult)

View file

@ -21,7 +21,7 @@ import { compare } from '../image_tools/compare';
const { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } = require('../third_party/diff_match_patch'); const { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } = require('../third_party/diff_match_patch');
import { PNG } from '../utilsBundle'; import { PNG } from '../utilsBundle';
export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, _comparator?: string }; export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, comparator?: string };
export type ComparatorResult = { diff?: Buffer; errorMessage: string; } | null; export type ComparatorResult = { diff?: Buffer; errorMessage: string; } | null;
export type Comparator = (actualBuffer: Buffer | string, expectedBuffer: Buffer, options?: any) => ComparatorResult; export type Comparator = (actualBuffer: Buffer | string, expectedBuffer: Buffer, options?: any) => ComparatorResult;
@ -65,18 +65,18 @@ function compareImages(mimeType: string, actualBuffer: Buffer | string, expected
} }
const diff = new PNG({ width: size.width, height: size.height }); const diff = new PNG({ width: size.width, height: size.height });
let count; let count;
if (options._comparator === 'ssim-cie94') { if (options.comparator === 'ssim-cie94') {
count = compare(expected.data, actual.data, diff.data, size.width, size.height, { count = compare(expected.data, actual.data, diff.data, size.width, size.height, {
// All ΔE* formulae are originally designed to have the difference of 1.0 stand for a "just noticeable difference" (JND). // All ΔE* formulae are originally designed to have the difference of 1.0 stand for a "just noticeable difference" (JND).
// See https://en.wikipedia.org/wiki/Color_difference#CIELAB_%CE%94E* // See https://en.wikipedia.org/wiki/Color_difference#CIELAB_%CE%94E*
maxColorDeltaE94: 1.0, maxColorDeltaE94: 1.0,
}); });
} else if ((options._comparator ?? 'pixelmatch') === 'pixelmatch') { } else if ((options.comparator ?? 'pixelmatch') === 'pixelmatch') {
count = pixelmatch(expected.data, actual.data, diff.data, size.width, size.height, { count = pixelmatch(expected.data, actual.data, diff.data, size.width, size.height, {
threshold: options.threshold ?? 0.2, threshold: options.threshold ?? 0.2,
}); });
} else { } else {
throw new Error(`Configuration specifies unknown comparator "${options._comparator}"`); throw new Error(`Configuration specifies unknown comparator "${options.comparator}"`);
} }
const maxDiffPixels1 = options.maxDiffPixels; const maxDiffPixels1 = options.maxDiffPixels;

View file

@ -15,12 +15,10 @@
*/ */
import type { Locator, Page } from 'playwright-core'; import type { Locator, Page } from 'playwright-core';
import type { Page as PageEx } from 'playwright-core/lib/client/page'; import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator';
import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import { currentTestInfo, currentExpectTimeout } from '../common/globals';
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
import { getComparator, sanitizeForFilePath, zones } from 'playwright-core/lib/utils'; import { getComparator, sanitizeForFilePath, zones } from 'playwright-core/lib/utils';
import type { PageScreenshotOptions } from 'playwright-core/types/types';
import { import {
addSuffixToFilePath, addSuffixToFilePath,
trimLongString, callLogText, trimLongString, callLogText,
@ -32,6 +30,7 @@ import { mime } from 'playwright-core/lib/utilsBundle';
import type { TestInfoImpl } from '../worker/testInfo'; import type { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherContext } from './expect'; import type { ExpectMatcherContext } from './expect';
import type { MatcherResult } from './matcherHint'; import type { MatcherResult } from './matcherHint';
import type { FullProjectInternal } from '../common/config';
type NameOrSegments = string | string[]; type NameOrSegments = string | string[];
const snapshotNamesSymbol = Symbol('snapshotNames'); const snapshotNamesSymbol = Symbol('snapshotNames');
@ -43,7 +42,36 @@ type SnapshotNames = {
type ImageMatcherResult = MatcherResult<string, string> & { diff?: string }; type ImageMatcherResult = MatcherResult<string, string> & { diff?: string };
class SnapshotHelper<T extends ImageComparatorOptions> { type ToHaveScreenshotConfigOptions = NonNullable<NonNullable<FullProjectInternal['expect']>['toHaveScreenshot']> & {
_comparator?: string;
};
type ToHaveScreenshotOptions = ToHaveScreenshotConfigOptions & {
clip?: {
x: number;
y: number;
width: number;
height: number;
};
fullPage?: boolean;
mask?: Array<Locator>;
maskColor?: string;
omitBackground?: boolean;
timeout?: number;
};
// Keep in sync with above (begin).
const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [
'clip',
'fullPage',
'mask',
'maskColor',
'omitBackground',
'timeout',
];
// Keep in sync with above (end).
class SnapshotHelper {
readonly testInfo: TestInfoImpl; readonly testInfo: TestInfoImpl;
readonly snapshotName: string; readonly snapshotName: string;
readonly legacyExpectedPath: string; readonly legacyExpectedPath: string;
@ -54,9 +82,8 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
readonly mimeType: string; readonly mimeType: string;
readonly kind: 'Screenshot'|'Snapshot'; readonly kind: 'Screenshot'|'Snapshot';
readonly updateSnapshots: 'all' | 'none' | 'missing'; readonly updateSnapshots: 'all' | 'none' | 'missing';
readonly comparatorOptions: ImageComparatorOptions;
readonly comparator: Comparator; readonly comparator: Comparator;
readonly allOptions: T; readonly options: Omit<ToHaveScreenshotOptions, '_comparator'> & { comparator?: string };
readonly matcherName: string; readonly matcherName: string;
readonly locator: Locator | undefined; readonly locator: Locator | undefined;
@ -66,19 +93,18 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
locator: Locator | undefined, locator: Locator | undefined,
snapshotPathResolver: (...pathSegments: string[]) => string, snapshotPathResolver: (...pathSegments: string[]) => string,
anonymousSnapshotExtension: string, anonymousSnapshotExtension: string,
configOptions: ImageComparatorOptions, configOptions: ToHaveScreenshotConfigOptions,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & T, nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions,
optOptions: T, optOptions: ToHaveScreenshotOptions,
) { ) {
let options: T;
let name: NameOrSegments | undefined; let name: NameOrSegments | undefined;
if (Array.isArray(nameOrOptions) || typeof nameOrOptions === 'string') { if (Array.isArray(nameOrOptions) || typeof nameOrOptions === 'string') {
name = nameOrOptions; name = nameOrOptions;
options = optOptions; this.options = { ...optOptions };
} else { } else {
name = nameOrOptions.name; name = nameOrOptions.name;
options = { ...nameOrOptions }; this.options = { ...nameOrOptions };
delete (options as any).name; delete (this.options as any).name;
} }
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames; let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
@ -116,15 +142,24 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
} }
} }
options = { const filteredConfigOptions = { ...configOptions };
...configOptions, for (const prop of NonConfigProperties)
...options, delete (filteredConfigOptions as any)[prop];
this.options = {
...filteredConfigOptions,
...this.options,
}; };
if (options.maxDiffPixels !== undefined && options.maxDiffPixels < 0) // While comparator is not a part of the public API, it is translated here.
if ((this.options as any)._comparator) {
this.options.comparator = (this.options as any)._comparator;
delete (this.options as any)._comparator;
}
if (this.options.maxDiffPixels !== undefined && this.options.maxDiffPixels < 0)
throw new Error('`maxDiffPixels` option value must be non-negative integer'); throw new Error('`maxDiffPixels` option value must be non-negative integer');
if (options.maxDiffPixelRatio !== undefined && (options.maxDiffPixelRatio < 0 || options.maxDiffPixelRatio > 1)) if (this.options.maxDiffPixelRatio !== undefined && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1))
throw new Error('`maxDiffPixelRatio` option value must be between 0 and 1'); throw new Error('`maxDiffPixelRatio` option value must be between 0 and 1');
// sanitizes path if string // sanitizes path if string
@ -145,13 +180,6 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
this.comparator = getComparator(this.mimeType); this.comparator = getComparator(this.mimeType);
this.testInfo = testInfo; this.testInfo = testInfo;
this.allOptions = options;
this.comparatorOptions = {
maxDiffPixels: options.maxDiffPixels,
maxDiffPixelRatio: options.maxDiffPixelRatio,
threshold: options.threshold,
_comparator: options._comparator,
};
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot'; this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
} }
@ -284,7 +312,7 @@ export function toMatchSnapshot(
if (this.isNot) { if (this.isNot) {
if (!fs.existsSync(helper.snapshotPath)) if (!fs.existsSync(helper.snapshotPath))
return helper.handleMissingNegated(); return helper.handleMissingNegated();
const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.snapshotPath), helper.comparatorOptions); const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.snapshotPath), helper.options);
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
} }
@ -292,7 +320,7 @@ export function toMatchSnapshot(
return helper.handleMissing(received); return helper.handleMissing(received);
const expected = fs.readFileSync(helper.snapshotPath); const expected = fs.readFileSync(helper.snapshotPath);
const result = helper.comparator(received, expected, helper.comparatorOptions); const result = helper.comparator(received, expected, helper.options);
if (!result) if (!result)
return helper.handleMatching(); return helper.handleMatching();
@ -306,11 +334,9 @@ export function toMatchSnapshot(
return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined); return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined);
} }
type HaveScreenshotOptions = ImageComparatorOptions & Omit<PageScreenshotOptions, 'type' | 'quality' | 'path' | 'style'> & { stylePath?: string | string[] };
export function toHaveScreenshotStepTitle( export function toHaveScreenshotStepTitle(
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & HaveScreenshotOptions = {}, nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
optOptions: HaveScreenshotOptions = {} optOptions: ToHaveScreenshotOptions = {}
): string { ): string {
let name: NameOrSegments | undefined; let name: NameOrSegments | undefined;
if (typeof nameOrOptions === 'object' && !Array.isArray(nameOrOptions)) if (typeof nameOrOptions === 'object' && !Array.isArray(nameOrOptions))
@ -323,8 +349,8 @@ export function toHaveScreenshotStepTitle(
export async function toHaveScreenshot( export async function toHaveScreenshot(
this: ExpectMatcherContext, this: ExpectMatcherContext,
pageOrLocator: Page | Locator, pageOrLocator: Page | Locator,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & HaveScreenshotOptions = {}, nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
optOptions: HaveScreenshotOptions = {} optOptions: ToHaveScreenshotOptions = {}
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> { ): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
@ -334,47 +360,45 @@ export async function toHaveScreenshot(
return { pass: !this.isNot, message: () => '', name: 'toHaveScreenshot', expected: nameOrOptions }; return { pass: !this.isNot, message: () => '', name: 'toHaveScreenshot', expected: nameOrOptions };
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot'); expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx]; const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
const config = (testInfo._projectInternal.expect as any)?.toHaveScreenshot; const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
const snapshotPathResolver = testInfo.snapshotPath.bind(testInfo); const snapshotPathResolver = testInfo.snapshotPath.bind(testInfo);
const helper = new SnapshotHelper( const helper = new SnapshotHelper(
testInfo, 'toHaveScreenshot', locator, snapshotPathResolver, 'png', testInfo, 'toHaveScreenshot', locator, snapshotPathResolver, 'png',
{ configOptions, nameOrOptions, optOptions);
_comparator: config?._comparator,
maxDiffPixels: config?.maxDiffPixels,
maxDiffPixelRatio: config?.maxDiffPixelRatio,
threshold: config?.threshold,
},
nameOrOptions, optOptions);
if (!helper.snapshotPath.toLowerCase().endsWith('.png')) if (!helper.snapshotPath.toLowerCase().endsWith('.png'))
throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`); throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`);
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot'); expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
return await zones.preserve(async () => { return await zones.preserve(async () => {
// Loading from filesystem resets zones. // Loading from filesystem resets zones.
const style = await loadScreenshotStyles(helper.allOptions.stylePath || config?.stylePath); const style = await loadScreenshotStyles(helper.options.stylePath);
return toHaveScreenshotContinuation.call(this, helper, page, locator, config, style); return toHaveScreenshotContinuation.call(this, helper, page, locator, style);
}); });
} }
async function toHaveScreenshotContinuation( async function toHaveScreenshotContinuation(
this: ExpectMatcherContext, this: ExpectMatcherContext,
helper: SnapshotHelper<HaveScreenshotOptions>, helper: SnapshotHelper,
page: PageEx, page: PageEx,
locator: LocatorEx | undefined, locator: Locator | undefined,
config?: HaveScreenshotOptions,
style?: string) { style?: string) {
const screenshotOptions: any = { const expectScreenshotOptions: ExpectScreenshotOptions = {
animations: config?.animations ?? 'disabled', locator,
scale: config?.scale ?? 'css', animations: helper.options.animations ?? 'disabled',
caret: config?.caret ?? 'hide', caret: helper.options.caret ?? 'hide',
clip: helper.options.clip,
fullPage: helper.options.fullPage,
mask: helper.options.mask,
maskColor: helper.options.maskColor,
omitBackground: helper.options.omitBackground,
scale: helper.options.scale ?? 'css',
style, style,
...helper.allOptions, isNot: !!this.isNot,
mask: (helper.allOptions.mask || []) as LocatorEx[], timeout: currentExpectTimeout(helper.options),
maskColor: helper.allOptions.maskColor, comparator: helper.options.comparator,
name: undefined, maxDiffPixels: helper.options.maxDiffPixels,
threshold: undefined, maxDiffPixelRatio: helper.options.maxDiffPixelRatio,
maxDiffPixels: undefined, threshold: helper.options.threshold,
maxDiffPixelRatio: undefined,
}; };
const hasSnapshot = fs.existsSync(helper.snapshotPath); const hasSnapshot = fs.existsSync(helper.snapshotPath);
@ -385,17 +409,8 @@ async function toHaveScreenshotContinuation(
// Having `errorMessage` means we timed out while waiting // Having `errorMessage` means we timed out while waiting
// for screenshots not to match, so screenshots // for screenshots not to match, so screenshots
// are actually the same in the end. // are actually the same in the end.
const isDifferent = !(await page._expectScreenshot({ expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath);
expected: await fs.promises.readFile(helper.snapshotPath), const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage;
isNot: true,
locator,
comparatorOptions: {
...helper.comparatorOptions,
comparator: helper.comparatorOptions._comparator,
},
screenshotOptions,
timeout: currentExpectTimeout(helper.allOptions),
})).errorMessage;
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
} }
@ -405,15 +420,7 @@ async function toHaveScreenshotContinuation(
if (!hasSnapshot) { if (!hasSnapshot) {
// Regenerate a new screenshot by waiting until two screenshots are the same. // Regenerate a new screenshot by waiting until two screenshots are the same.
const timeout = currentExpectTimeout(helper.allOptions); const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot({
expected: undefined,
isNot: false,
locator,
comparatorOptions: { ...helper.comparatorOptions, comparator: helper.comparatorOptions._comparator },
screenshotOptions,
timeout,
});
// We tried re-generating new snapshot but failed. // We tried re-generating new snapshot but failed.
// This can be due to e.g. spinning animation, so we want to show it as a diff. // This can be due to e.g. spinning animation, so we want to show it as a diff.
if (errorMessage) if (errorMessage)
@ -427,15 +434,8 @@ async function toHaveScreenshotContinuation(
// - snapshot exists // - snapshot exists
// - regular matcher (i.e. not a `.not`) // - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots // - perhaps an 'all' flag to update non-matching screenshots
const expected = await fs.promises.readFile(helper.snapshotPath); expectScreenshotOptions.expected = await fs.promises.readFile(helper.snapshotPath);
const { actual, diff, errorMessage, log } = await page._expectScreenshot({ const { actual, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
expected,
isNot: false,
locator,
comparatorOptions: { ...helper.comparatorOptions, comparator: helper.comparatorOptions._comparator },
screenshotOptions,
timeout: currentExpectTimeout(helper.allOptions),
});
if (!errorMessage) if (!errorMessage)
return helper.handleMatching(); return helper.handleMatching();
@ -448,7 +448,7 @@ async function toHaveScreenshotContinuation(
return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true); return helper.createMatcherResult(helper.snapshotPath + ' running with --update-snapshots, writing actual.', true);
} }
return helper.handleDifferent(actual, expected, undefined, diff, errorMessage, log); return helper.handleDifferent(actual, expectScreenshotOptions.expected, undefined, diff, errorMessage, log);
} }
function writeFileSync(aPath: string, content: Buffer | string) { function writeFileSync(aPath: string, content: Buffer | string) {

View file

@ -1939,26 +1939,22 @@ export type PageExpectScreenshotParams = {
frame: FrameChannel, frame: FrameChannel,
selector: string, selector: string,
}, },
comparatorOptions?: { comparator?: string,
comparator?: string, maxDiffPixels?: number,
maxDiffPixels?: number, maxDiffPixelRatio?: number,
maxDiffPixelRatio?: number, threshold?: number,
threshold?: number, fullPage?: boolean,
}, clip?: Rect,
screenshotOptions?: { omitBackground?: boolean,
fullPage?: boolean, caret?: 'hide' | 'initial',
clip?: Rect, animations?: 'disabled' | 'allow',
omitBackground?: boolean, scale?: 'css' | 'device',
caret?: 'hide' | 'initial', mask?: {
animations?: 'disabled' | 'allow', frame: FrameChannel,
scale?: 'css' | 'device', selector: string,
mask?: { }[],
frame: FrameChannel, maskColor?: string,
selector: string, style?: string,
}[],
maskColor?: string,
style?: string,
},
}; };
export type PageExpectScreenshotOptions = { export type PageExpectScreenshotOptions = {
expected?: Binary, expected?: Binary,
@ -1967,26 +1963,22 @@ export type PageExpectScreenshotOptions = {
frame: FrameChannel, frame: FrameChannel,
selector: string, selector: string,
}, },
comparatorOptions?: { comparator?: string,
comparator?: string, maxDiffPixels?: number,
maxDiffPixels?: number, maxDiffPixelRatio?: number,
maxDiffPixelRatio?: number, threshold?: number,
threshold?: number, fullPage?: boolean,
}, clip?: Rect,
screenshotOptions?: { omitBackground?: boolean,
fullPage?: boolean, caret?: 'hide' | 'initial',
clip?: Rect, animations?: 'disabled' | 'allow',
omitBackground?: boolean, scale?: 'css' | 'device',
caret?: 'hide' | 'initial', mask?: {
animations?: 'disabled' | 'allow', frame: FrameChannel,
scale?: 'css' | 'device', selector: string,
mask?: { }[],
frame: FrameChannel, maskColor?: string,
selector: string, style?: string,
}[],
maskColor?: string,
style?: string,
},
}; };
export type PageExpectScreenshotResult = { export type PageExpectScreenshotResult = {
diff?: Binary, diff?: Binary,

View file

@ -1367,19 +1367,13 @@ Page:
properties: properties:
frame: Frame frame: Frame
selector: string selector: string
comparatorOptions: comparator: string?
type: object? maxDiffPixels: number?
properties: maxDiffPixelRatio: number?
comparator: string? threshold: number?
maxDiffPixels: number? fullPage: boolean?
maxDiffPixelRatio: number? clip: Rect?
threshold: number? $mixin: CommonScreenshotOptions
screenshotOptions:
type: object?
properties:
fullPage: boolean?
clip: Rect?
$mixin: CommonScreenshotOptions
returns: returns:
diff: binary? diff: binary?
errorMessage: string? errorMessage: string?

View file

@ -22,5 +22,5 @@ type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxD
export function comparePNGs(actual: Buffer, expected: Buffer, options: ImageComparatorOptions = {}): ComparatorResult { export function comparePNGs(actual: Buffer, expected: Buffer, options: ImageComparatorOptions = {}): ComparatorResult {
// Strict threshold by default in our tests. // Strict threshold by default in our tests.
return pngComparator(actual, expected, { _comparator: 'ssim-cie94', threshold: 0, ...options }); return pngComparator(actual, expected, { comparator: 'ssim-cie94', threshold: 0, ...options });
} }