mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(test runner): TestInfo.snapshotPath() should sanitize (#35709)
This commit is contained in:
parent
a858681da4
commit
79fd15b530
@ -18,11 +18,11 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
import { escapeTemplateString, isString } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
||||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||||
import { callLogText, fileExistsAsync, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
import { callLogText, fileExistsAsync, trimLongString } from '../util';
|
||||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo } from '../common/globals';
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ export async function toMatchAriaSnapshot(
|
|||||||
timeout = options.timeout ?? this.timeout;
|
timeout = options.timeout ?? this.timeout;
|
||||||
} else {
|
} else {
|
||||||
if (expectedParam?.name) {
|
if (expectedParam?.name) {
|
||||||
const ext = expectedParam.name!.endsWith('.aria.yml') ? '.aria.yml' : undefined;
|
const ext = expectedParam.name.endsWith('.aria.yml') ? '.aria.yml' : undefined;
|
||||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name, ext)]);
|
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [expectedParam.name], true, ext);
|
||||||
} else {
|
} else {
|
||||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||||
if (!snapshotNames) {
|
if (!snapshotNames) {
|
||||||
@ -79,11 +79,11 @@ export async function toMatchAriaSnapshot(
|
|||||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||||
}
|
}
|
||||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec))], '.aria.yml');
|
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [trimLongString(fullTitleWithoutSpec) + '.aria.yml'], true, '.aria.yml');
|
||||||
// in 1.51, we changed the default template to use .aria.yml extension
|
// in 1.51, we changed the default template to use .aria.yml extension
|
||||||
// for backwards compatibility, we check for the legacy .yml extension
|
// for backwards compatibility, we check for the legacy .yml extension
|
||||||
if (!(await fileExistsAsync(expectedPath))) {
|
if (!(await fileExistsAsync(expectedPath))) {
|
||||||
const legacyPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec))], '.yml');
|
const legacyPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [trimLongString(fullTitleWithoutSpec) + '.yml'], true, '.yml');
|
||||||
if (await fileExistsAsync(legacyPath))
|
if (await fileExistsAsync(legacyPath))
|
||||||
expectedPath = legacyPath;
|
expectedPath = legacyPath;
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,7 @@ class SnapshotHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let expectedPathSegments: string[];
|
let expectedPathSegments: string[];
|
||||||
|
let sanitizeFilePath = true;
|
||||||
let outputBasePath: string;
|
let outputBasePath: string;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
// Consider the use case below. We should save actual to different paths.
|
// Consider the use case below. We should save actual to different paths.
|
||||||
@ -135,17 +136,24 @@ class SnapshotHelper {
|
|||||||
++snapshotNames.anonymousSnapshotIndex,
|
++snapshotNames.anonymousSnapshotIndex,
|
||||||
].join(' ');
|
].join(' ');
|
||||||
// Note: expected path must not ever change for backwards compatibility.
|
// Note: expected path must not ever change for backwards compatibility.
|
||||||
expectedPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension];
|
expectedPathSegments = [trimLongString(fullTitleWithoutSpec) + '.' + anonymousSnapshotExtension];
|
||||||
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
|
// Trim the output file paths more aggressively to avoid hitting Windows filesystem limits.
|
||||||
const sanitizedName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
|
const sanitizedName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
|
||||||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||||
this.attachmentBaseName = sanitizedName;
|
this.attachmentBaseName = sanitizedName;
|
||||||
} else {
|
} 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
|
// 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.
|
// it is a file system path. See https://github.com/microsoft/playwright/pull/9156.
|
||||||
// Note: expected path must not ever change for backwards compatibility.
|
sanitizeFilePath = false;
|
||||||
expectedPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)];
|
expectedPathSegments = name;
|
||||||
const joinedName = Array.isArray(name) ? name.join(path.sep) : sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength));
|
joinedName = name.join(path.sep);
|
||||||
|
} else {
|
||||||
|
expectedPathSegments = [name];
|
||||||
|
joinedName = sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength));
|
||||||
|
}
|
||||||
snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1;
|
snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1;
|
||||||
const index = snapshotNames.namedSnapshotIndex[joinedName];
|
const index = snapshotNames.namedSnapshotIndex[joinedName];
|
||||||
const sanitizedName = index > 1 ? addSuffixToFilePath(joinedName, `-${index - 1}`) : joinedName;
|
const sanitizedName = index > 1 ? addSuffixToFilePath(joinedName, `-${index - 1}`) : joinedName;
|
||||||
@ -153,7 +161,7 @@ class SnapshotHelper {
|
|||||||
this.attachmentBaseName = sanitizedName;
|
this.attachmentBaseName = sanitizedName;
|
||||||
}
|
}
|
||||||
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||||
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
|
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments, sanitizeFilePath);
|
||||||
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
||||||
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
||||||
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
||||||
|
@ -20,7 +20,7 @@ import path from 'path';
|
|||||||
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||||
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||||
import { TestTracing } from './testTracing';
|
import { TestTracing } from './testTracing';
|
||||||
import { testInfoError } from './util';
|
import { testInfoError } from './util';
|
||||||
|
|
||||||
@ -448,8 +448,11 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[], extension?: string) {
|
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[], sanitizeFilePath: boolean, extension?: string) {
|
||||||
const subPath = path.join(...pathSegments);
|
let subPath = path.join(...pathSegments);
|
||||||
|
if (sanitizeFilePath)
|
||||||
|
subPath = sanitizeFilePathBeforeExtension(subPath, extension);
|
||||||
|
|
||||||
const dir = path.dirname(subPath);
|
const dir = path.dirname(subPath);
|
||||||
const ext = extension ?? path.extname(subPath);
|
const ext = extension ?? path.extname(subPath);
|
||||||
const name = path.basename(subPath, ext);
|
const name = path.basename(subPath, ext);
|
||||||
@ -475,8 +478,11 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
snapshotPath(...pathSegments: string[]) {
|
snapshotPath(...pathSegments: string[]) {
|
||||||
|
// 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;
|
||||||
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||||
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
|
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments, sanitizeFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
skip(...args: [arg?: any, description?: string]) {
|
skip(...args: [arg?: any, description?: string]) {
|
||||||
|
@ -742,6 +742,37 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
|
|||||||
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect config.snapshotPathTemplate and sanitize the name', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/35669' },
|
||||||
|
}, async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...playwrightConfig({
|
||||||
|
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
|
||||||
|
}),
|
||||||
|
'__screenshots__/a.spec.js/my-name.png': whiteImage,
|
||||||
|
'__screenshots__/a.spec.js/my_name/bar.png': whiteImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const path = require('path');
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
const testDir = test.info().project.testDir;
|
||||||
|
|
||||||
|
// A single name is sanitized to a valid file name.
|
||||||
|
const expectedPath1 = path.join(testDir, '__screenshots__/a.spec.js/my-name.png');
|
||||||
|
expect(test.info().snapshotPath('my_name.png')).toBe(expectedPath1);
|
||||||
|
await expect(page).toHaveScreenshot('my_name.png');
|
||||||
|
|
||||||
|
// An array is not sanitized - see https://github.com/microsoft/playwright/pull/9156.
|
||||||
|
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']);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
|
test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
...playwrightConfig({
|
...playwrightConfig({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user