mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: consolidate snapshot path logic into TestInfo (#35752)
This commit is contained in:
parent
eaedc6fda3
commit
2c2da8a0aa
@ -22,7 +22,7 @@ import { escapeTemplateString, isString } from 'playwright-core/lib/utils';
|
||||
|
||||
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
import { callLogText, fileExistsAsync, trimLongString } from '../util';
|
||||
import { callLogText, fileExistsAsync } from '../util';
|
||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
|
||||
@ -67,24 +67,12 @@ export async function toMatchAriaSnapshot(
|
||||
expected = expectedParam;
|
||||
timeout = options.timeout ?? this.timeout;
|
||||
} else {
|
||||
if (expectedParam?.name) {
|
||||
expectedPath = testInfo._resolveSnapshotPath('aria', [expectedParam.name], true);
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!snapshotNames) {
|
||||
snapshotNames = { anonymousSnapshotIndex: 0 };
|
||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||
expectedPath = testInfo._resolveSnapshotPath('aria', [trimLongString(fullTitleWithoutSpec) + '.aria.yml'], true);
|
||||
// in 1.51, we changed the default template to use .aria.yml extension
|
||||
// for backwards compatibility, we check for the legacy .yml extension
|
||||
if (!(await fileExistsAsync(expectedPath))) {
|
||||
const legacyPath = testInfo._resolveSnapshotPath('aria', [trimLongString(fullTitleWithoutSpec) + '.yml'], true);
|
||||
if (await fileExistsAsync(legacyPath))
|
||||
expectedPath = legacyPath;
|
||||
}
|
||||
}
|
||||
const legacyPath = testInfo._resolveSnapshotPaths('aria', expectedParam?.name, 'dontUpdateSnapshotIndex', '.yml').absoluteSnapshotPath;
|
||||
expectedPath = testInfo._resolveSnapshotPaths('aria', expectedParam?.name, 'updateSnapshotIndex').absoluteSnapshotPath;
|
||||
// in 1.51, we changed the default template to use .aria.yml extension
|
||||
// for backwards compatibility, we check for the legacy .yml extension
|
||||
if (!(await fileExistsAsync(expectedPath)) && await fileExistsAsync(legacyPath))
|
||||
expectedPath = legacyPath;
|
||||
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||
timeout = expectedParam?.timeout ?? this.timeout;
|
||||
}
|
||||
@ -190,9 +178,3 @@ function unshift(snapshot: string): string {
|
||||
function indent(snapshot: string, indent: string): string {
|
||||
return snapshot.split('\n').map(line => indent + line).join('\n');
|
||||
}
|
||||
|
||||
const snapshotNamesSymbol = Symbol('snapshotNames');
|
||||
|
||||
type SnapshotNames = {
|
||||
anonymousSnapshotIndex: number;
|
||||
};
|
||||
|
||||
@ -17,16 +17,11 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { compareBuffersOrStrings, getComparator, isString } from 'playwright-core/lib/utils';
|
||||
import { colors } from 'playwright-core/lib/utils';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import {
|
||||
addSuffixToFilePath, callLogText,
|
||||
expectTypes,
|
||||
sanitizeFilePathBeforeExtension,
|
||||
trimLongString,
|
||||
windowsFilesystemFriendlyLength } from '../util';
|
||||
import { addSuffixToFilePath, callLogText, expectTypes } from '../util';
|
||||
import { matcherHint } from './matcherHint';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
|
||||
@ -39,12 +34,6 @@ import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/li
|
||||
import type { Comparator, ImageComparatorOptions } from 'playwright-core/lib/utils';
|
||||
|
||||
type NameOrSegments = string | string[];
|
||||
const snapshotNamesSymbol = Symbol('snapshotNames');
|
||||
|
||||
type SnapshotNames = {
|
||||
anonymousSnapshotIndex: number;
|
||||
namedSnapshotIndex: { [key: string]: number };
|
||||
};
|
||||
|
||||
type ImageMatcherResult = MatcherResult<string, string> & { diff?: string };
|
||||
|
||||
@ -97,7 +86,7 @@ class SnapshotHelper {
|
||||
testInfo: TestInfoImpl,
|
||||
matcherName: 'toMatchSnapshot' | 'toHaveScreenshot',
|
||||
locator: Locator | undefined,
|
||||
anonymousSnapshotExtension: string,
|
||||
anonymousSnapshotExtension: string | undefined,
|
||||
configOptions: ToHaveScreenshotConfigOptions,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions,
|
||||
optOptions: ToHaveScreenshotOptions,
|
||||
@ -112,55 +101,11 @@ class SnapshotHelper {
|
||||
name = nameFromOptions;
|
||||
}
|
||||
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!(testInfo as any)[snapshotNamesSymbol]) {
|
||||
snapshotNames = {
|
||||
anonymousSnapshotIndex: 0,
|
||||
namedSnapshotIndex: {},
|
||||
};
|
||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === 'toHaveScreenshot' ? 'screenshot' : 'snapshot', name, 'updateSnapshotIndex', anonymousSnapshotExtension);
|
||||
this.expectedPath = resolvedPaths.absoluteSnapshotPath;
|
||||
this.attachmentBaseName = resolvedPaths.relativeOutputPath;
|
||||
|
||||
let expectedPathSegments: string[];
|
||||
let sanitizeFilePath = true;
|
||||
let outputBasePath: string;
|
||||
if (!name) {
|
||||
// Consider the use case below. We should save actual to different paths.
|
||||
// Therefore we auto-increment |anonymousSnapshotIndex|.
|
||||
//
|
||||
// expect.toMatchSnapshot('a.png')
|
||||
// // noop
|
||||
// expect.toMatchSnapshot('a.png')
|
||||
const fullTitleWithoutSpec = [
|
||||
...testInfo.titlePath.slice(1),
|
||||
++snapshotNames.anonymousSnapshotIndex,
|
||||
].join(' ');
|
||||
// Note: expected path must not ever change for backwards compatibility.
|
||||
expectedPathSegments = [trimLongString(fullTitleWithoutSpec) + '.' + anonymousSnapshotExtension];
|
||||
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
|
||||
const sanitizedName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
|
||||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||
this.attachmentBaseName = sanitizedName;
|
||||
} else {
|
||||
// Note: expected path must not ever change for backwards compatibility.
|
||||
let joinedName: string;
|
||||
if (Array.isArray(name)) {
|
||||
// We intentionally do not sanitize user-provided array of segments, assuming
|
||||
// it is a file system path. See https://github.com/microsoft/playwright/pull/9156.
|
||||
sanitizeFilePath = false;
|
||||
expectedPathSegments = name;
|
||||
joinedName = name.join(path.sep);
|
||||
} else {
|
||||
expectedPathSegments = [name];
|
||||
joinedName = sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength));
|
||||
}
|
||||
snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1;
|
||||
const index = snapshotNames.namedSnapshotIndex[joinedName];
|
||||
const sanitizedName = index > 1 ? addSuffixToFilePath(joinedName, `-${index - 1}`) : joinedName;
|
||||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||
this.attachmentBaseName = sanitizedName;
|
||||
}
|
||||
this.expectedPath = testInfo._resolveSnapshotPath(matcherName === 'toHaveScreenshot' ? 'screenshot' : 'snapshot', expectedPathSegments, sanitizeFilePath);
|
||||
const outputBasePath = testInfo._getOutputPath(resolvedPaths.relativeOutputPath);
|
||||
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
||||
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
||||
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
||||
@ -316,7 +261,7 @@ export function toMatchSnapshot(
|
||||
|
||||
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
|
||||
const helper = new SnapshotHelper(
|
||||
testInfo, 'toMatchSnapshot', undefined, determineFileExtension(received),
|
||||
testInfo, 'toMatchSnapshot', undefined, '.' + determineFileExtension(received),
|
||||
configOptions, nameOrOptions, optOptions);
|
||||
|
||||
if (this.isNot) {
|
||||
@ -387,7 +332,7 @@ export async function 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 Locator];
|
||||
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
|
||||
const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
|
||||
const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, undefined, configOptions, nameOrOptions, optOptions);
|
||||
if (!helper.expectedPath.toLowerCase().endsWith('.png'))
|
||||
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
|
||||
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
||||
|
||||
@ -20,7 +20,7 @@ import path from 'path';
|
||||
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
||||
|
||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||
import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||
import { TestTracing } from './testTracing';
|
||||
import { testInfoError } from './util';
|
||||
|
||||
@ -51,10 +51,17 @@ export interface TestStepInternal {
|
||||
box?: boolean;
|
||||
}
|
||||
|
||||
type SnapshotNames = {
|
||||
lastAnonymousSnapshotIndex: number;
|
||||
lastNamedSnapshotIndex: { [key: string]: number };
|
||||
};
|
||||
|
||||
export class TestInfoImpl implements TestInfo {
|
||||
private _onStepBegin: (payload: StepBeginPayload) => void;
|
||||
private _onStepEnd: (payload: StepEndPayload) => void;
|
||||
private _onAttach: (payload: AttachmentPayload) => void;
|
||||
private _snapshotNames: SnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
||||
private _ariaSnapshotNames: SnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
||||
readonly _timeoutManager: TimeoutManager;
|
||||
readonly _startTime: number;
|
||||
readonly _startWallTime: number;
|
||||
@ -448,10 +455,57 @@ export class TestInfoImpl implements TestInfo {
|
||||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||
}
|
||||
|
||||
_resolveSnapshotPath(kind: 'snapshot' | 'screenshot' | 'aria', pathSegments: string[], sanitizeFilePath: boolean) {
|
||||
let subPath = path.join(...pathSegments);
|
||||
let ext = path.extname(subPath);
|
||||
_resolveSnapshotPaths(kind: 'snapshot' | 'screenshot' | 'aria', name: string | string[] | undefined, updateSnapshotIndex: 'updateSnapshotIndex' | 'dontUpdateSnapshotIndex', anonymousExtension?: string) {
|
||||
// NOTE: snapshot path must not ever change for backwards compatibility!
|
||||
|
||||
const snapshotNames = kind === 'aria' ? this._ariaSnapshotNames : this._snapshotNames;
|
||||
const defaultExtensions = { 'aria': '.aria.yml', 'screenshot': '.png', 'snapshot': '.txt' };
|
||||
const ariaAwareExtname = (filePath: string) => kind === 'aria' && filePath.endsWith('.aria.yml') ? '.aria.yml' : path.extname(filePath);
|
||||
|
||||
let subPath: string;
|
||||
let ext: string;
|
||||
let relativeOutputPath: string;
|
||||
|
||||
if (!name) {
|
||||
// Consider the use case below. We should save actual to different paths, so we use |nextAnonymousSnapshotIndex|.
|
||||
//
|
||||
// expect.toMatchSnapshot('a.png')
|
||||
// // noop
|
||||
// expect.toMatchSnapshot('a.png')
|
||||
const index = snapshotNames.lastAnonymousSnapshotIndex + 1;
|
||||
if (updateSnapshotIndex === 'updateSnapshotIndex')
|
||||
snapshotNames.lastAnonymousSnapshotIndex = index;
|
||||
const fullTitleWithoutSpec = [...this.titlePath.slice(1), index].join(' ');
|
||||
ext = anonymousExtension ?? defaultExtensions[kind];
|
||||
subPath = sanitizeFilePathBeforeExtension(trimLongString(fullTitleWithoutSpec) + ext, ext);
|
||||
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
|
||||
relativeOutputPath = sanitizeFilePathBeforeExtension(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength) + ext, ext);
|
||||
} else {
|
||||
if (Array.isArray(name)) {
|
||||
// We intentionally do not sanitize user-provided array of segments,
|
||||
// assuming it is a file system path.
|
||||
// See https://github.com/microsoft/playwright/pull/9156.
|
||||
subPath = path.join(...name);
|
||||
relativeOutputPath = path.join(...name);
|
||||
ext = ariaAwareExtname(subPath);
|
||||
} else {
|
||||
ext = ariaAwareExtname(name);
|
||||
subPath = sanitizeFilePathBeforeExtension(name, ext);
|
||||
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
|
||||
relativeOutputPath = sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength), ext);
|
||||
}
|
||||
const index = (snapshotNames.lastNamedSnapshotIndex[relativeOutputPath] || 0) + 1;
|
||||
if (updateSnapshotIndex === 'updateSnapshotIndex')
|
||||
snapshotNames.lastNamedSnapshotIndex[relativeOutputPath] = index;
|
||||
if (index > 1)
|
||||
relativeOutputPath = addSuffixToFilePath(relativeOutputPath, `-${index - 1}`);
|
||||
}
|
||||
|
||||
const absoluteSnapshotPath = this._applyPathTemplate(kind, subPath, ext);
|
||||
return { absoluteSnapshotPath, relativeOutputPath };
|
||||
}
|
||||
|
||||
private _applyPathTemplate(kind: 'snapshot' | 'screenshot' | 'aria', relativePath: string, ext: string) {
|
||||
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
let template: string;
|
||||
if (kind === 'screenshot') {
|
||||
@ -459,17 +513,12 @@ export class TestInfoImpl implements TestInfo {
|
||||
} else if (kind === 'aria') {
|
||||
const ariaDefaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
|
||||
template = this._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate || this._projectInternal.snapshotPathTemplate || ariaDefaultTemplate;
|
||||
if (subPath.endsWith('.aria.yml'))
|
||||
ext = '.aria.yml';
|
||||
} else {
|
||||
template = this._projectInternal.snapshotPathTemplate || legacyTemplate;
|
||||
}
|
||||
|
||||
if (sanitizeFilePath)
|
||||
subPath = sanitizeFilePathBeforeExtension(subPath, ext);
|
||||
|
||||
const dir = path.dirname(subPath);
|
||||
const name = path.basename(subPath, ext);
|
||||
const dir = path.dirname(relativePath);
|
||||
const name = path.basename(relativePath, ext);
|
||||
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||
@ -493,22 +542,21 @@ export class TestInfoImpl implements TestInfo {
|
||||
snapshotPath(...name: string[]): string;
|
||||
snapshotPath(name: string, options: { kind: 'snapshot' | 'screenshot' | 'aria' }): string;
|
||||
snapshotPath(...args: any[]) {
|
||||
let pathSegments: string[] = args;
|
||||
let name: string[] = args;
|
||||
let kind: 'snapshot' | 'screenshot' | 'aria' = 'snapshot';
|
||||
|
||||
const options = args[args.length - 1];
|
||||
if (options && typeof options === 'object') {
|
||||
kind = options.kind ?? kind;
|
||||
pathSegments = args.slice(0, -1);
|
||||
name = args.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!['snapshot', 'screenshot', 'aria'].includes(kind))
|
||||
throw new Error(`testInfo.snapshotPath: unknown kind "${kind}", must be one of "snapshot", "screenshot" or "aria"`);
|
||||
|
||||
// Assume a single path segment corresponds to `toHaveScreenshot(name)` and sanitize it,
|
||||
// like we do in SnapshotHelper. See https://github.com/microsoft/playwright/pull/9156 for history.
|
||||
const sanitizeFilePath = pathSegments.length === 1;
|
||||
return this._resolveSnapshotPath(kind, pathSegments, sanitizeFilePath);
|
||||
// Assume a zero/single path segment corresponds to `toHaveScreenshot(name)`,
|
||||
// while multiple path segments correspond to `toHaveScreenshot([...name])`.
|
||||
return this._resolveSnapshotPaths(kind, name.length <= 1 ? name[0] : name, 'dontUpdateSnapshotIndex').absoluteSnapshotPath;
|
||||
}
|
||||
|
||||
skip(...args: [arg?: any, description?: string]) {
|
||||
|
||||
@ -751,6 +751,8 @@ test('should respect config.snapshotPathTemplate and sanitize the name', {
|
||||
}),
|
||||
'__screenshots__/a.spec.js/my-name.png': whiteImage,
|
||||
'__screenshots__/a.spec.js/my_name/bar.png': whiteImage,
|
||||
'__screenshots__/a.spec.js/is-a-test-1.png': whiteImage,
|
||||
'__screenshots__/a.spec.js/is-a-test-2.png': whiteImage,
|
||||
'a.spec.js': `
|
||||
const path = require('path');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
@ -766,6 +768,16 @@ test('should respect config.snapshotPathTemplate and sanitize the name', {
|
||||
const expectedPath2 = path.join(testDir, '__screenshots__/a.spec.js/my_name/bar.png');
|
||||
expect(test.info().snapshotPath('my_name', 'bar.png')).toBe(expectedPath2);
|
||||
await expect(page).toHaveScreenshot(['my_name', 'bar.png']);
|
||||
|
||||
// Auto-generated name is sanitized.
|
||||
const expectedPath3 = path.join(testDir, '__screenshots__/a.spec.js/is-a-test-1.png');
|
||||
expect(test.info().snapshotPath('', { kind: 'screenshot' })).toBe(expectedPath3);
|
||||
await expect(page).toHaveScreenshot();
|
||||
|
||||
// Auto-generated name is incremented.
|
||||
const expectedPath4 = path.join(testDir, '__screenshots__/a.spec.js/is-a-test-2.png');
|
||||
expect(test.info().snapshotPath('', { kind: 'screenshot' })).toBe(expectedPath4);
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user