mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: introduce kind
option in TestInfo.snapshotPath() (#35734)
This commit is contained in:
parent
2de30d694c
commit
508b1ccdcb
@ -387,17 +387,45 @@ Optional description that will be reflected in a test report.
|
||||
* since: v1.10
|
||||
- returns: <[string]>
|
||||
|
||||
Returns a path to a snapshot file with the given `pathSegments`. Learn more about [snapshots](../test-snapshots.md).
|
||||
Returns a path to a snapshot file with the given `name`. Pass [`option: kind`] to obtain a specific path:
|
||||
* `kind: 'screenshot'` for [`method: PageAssertions.toHaveScreenshot#1`];
|
||||
* `kind: 'aria'` for [`method: LocatorAssertions.toMatchAriaSnapshot`];
|
||||
* `kind: 'snapshot'` for [`method: SnapshotAssertions.toMatchSnapshot#1`].
|
||||
|
||||
> Note that `pathSegments` accepts path segments to the snapshot file such as `testInfo.snapshotPath('relative', 'path', 'to', 'snapshot.png')`.
|
||||
> However, this path must stay within the snapshots directory for each test file (i.e. `a.spec.js-snapshots`), otherwise it will throw.
|
||||
**Usage**
|
||||
|
||||
### param: TestInfo.snapshotPath.pathSegments
|
||||
```js
|
||||
await expect(page).toHaveScreenshot('header.png');
|
||||
// Screenshot assertion above expects screenshot at this path:
|
||||
const screenshotPath = test.info().snapshotPath('header.png', { kind: 'screenshot' });
|
||||
|
||||
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.aria.yml' });
|
||||
// Aria snapshot assertion above expects snapshot at this path:
|
||||
const ariaSnapshotPath = test.info().snapshotPath('main.aria.yml', { kind: 'aria' });
|
||||
|
||||
expect('some text').toMatchSnapshot('snapshot.txt');
|
||||
// Snapshot assertion above expects snapshot at this path:
|
||||
const snapshotPath = test.info().snapshotPath('snapshot.txt');
|
||||
|
||||
expect('some text').toMatchSnapshot(['dir', 'subdir', 'snapshot.txt']);
|
||||
// Snapshot assertion above expects snapshot at this path:
|
||||
const nestedPath = test.info().snapshotPath('dir', 'subdir', 'snapshot.txt');
|
||||
```
|
||||
|
||||
### param: TestInfo.snapshotPath.name
|
||||
* since: v1.10
|
||||
- `...pathSegments` <[Array]<[string]>>
|
||||
- `...name` <[Array]<[string]>>
|
||||
|
||||
The name of the snapshot or the path segments to define the snapshot file path. Snapshots with the same name in the same test file are expected to be the same.
|
||||
|
||||
When passing [`option: kind`], multiple name segments are not supported.
|
||||
|
||||
### option: TestInfo.snapshotPath.kind
|
||||
* since: v1.53
|
||||
- `kind` <[SnapshotKind]<"snapshot"|"screenshot"|"aria">>
|
||||
|
||||
The snapshot kind controls which snapshot path template is used. See [`property: TestConfig.snapshotPathTemplate`] for more details. Defaults to `'snapshot'`.
|
||||
|
||||
## property: TestInfo.snapshotSuffix
|
||||
* since: v1.10
|
||||
- type: <[string]>
|
||||
|
@ -54,8 +54,6 @@ export async function toMatchAriaSnapshot(
|
||||
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
|
||||
|
||||
const updateSnapshots = testInfo.config.updateSnapshots;
|
||||
const pathTemplate = testInfo._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate;
|
||||
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
|
||||
|
||||
const matcherOptions = {
|
||||
isNot: this.isNot,
|
||||
@ -70,8 +68,7 @@ export async function toMatchAriaSnapshot(
|
||||
timeout = options.timeout ?? this.timeout;
|
||||
} else {
|
||||
if (expectedParam?.name) {
|
||||
const ext = expectedParam.name.endsWith('.aria.yml') ? '.aria.yml' : undefined;
|
||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [expectedParam.name], true, ext);
|
||||
expectedPath = testInfo._resolveSnapshotPath('aria', [expectedParam.name], true);
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!snapshotNames) {
|
||||
@ -79,11 +76,11 @@ export async function toMatchAriaSnapshot(
|
||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [trimLongString(fullTitleWithoutSpec) + '.aria.yml'], true, '.aria.yml');
|
||||
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(pathTemplate, defaultTemplate, [trimLongString(fullTitleWithoutSpec) + '.yml'], true, '.yml');
|
||||
const legacyPath = testInfo._resolveSnapshotPath('aria', [trimLongString(fullTitleWithoutSpec) + '.yml'], true);
|
||||
if (await fileExistsAsync(legacyPath))
|
||||
expectedPath = legacyPath;
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ class SnapshotHelper {
|
||||
|
||||
constructor(
|
||||
testInfo: TestInfoImpl,
|
||||
matcherName: string,
|
||||
matcherName: 'toMatchSnapshot' | 'toHaveScreenshot',
|
||||
locator: Locator | undefined,
|
||||
anonymousSnapshotExtension: string,
|
||||
configOptions: ToHaveScreenshotConfigOptions,
|
||||
@ -160,8 +160,7 @@ class SnapshotHelper {
|
||||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||
this.attachmentBaseName = sanitizedName;
|
||||
}
|
||||
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments, sanitizeFilePath);
|
||||
this.expectedPath = testInfo._resolveSnapshotPath(matcherName === 'toHaveScreenshot' ? 'screenshot' : 'snapshot', expectedPathSegments, sanitizeFilePath);
|
||||
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
||||
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
||||
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
||||
|
@ -448,20 +448,33 @@ export class TestInfoImpl implements TestInfo {
|
||||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||
}
|
||||
|
||||
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[], sanitizeFilePath: boolean, extension?: string) {
|
||||
_resolveSnapshotPath(kind: 'snapshot' | 'screenshot' | 'aria', pathSegments: string[], sanitizeFilePath: boolean) {
|
||||
let subPath = path.join(...pathSegments);
|
||||
let ext = path.extname(subPath);
|
||||
|
||||
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
let template: string;
|
||||
if (kind === 'screenshot') {
|
||||
template = this._projectInternal.expect?.toHaveScreenshot?.pathTemplate || this._projectInternal.snapshotPathTemplate || legacyTemplate;
|
||||
} 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, extension);
|
||||
subPath = sanitizeFilePathBeforeExtension(subPath, ext);
|
||||
|
||||
const dir = path.dirname(subPath);
|
||||
const ext = extension ?? path.extname(subPath);
|
||||
const name = path.basename(subPath, ext);
|
||||
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||
|
||||
const actualTemplate = (template || this._projectInternal.snapshotPathTemplate || defaultTemplate);
|
||||
const snapshotPath = actualTemplate
|
||||
const snapshotPath = template
|
||||
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
|
||||
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
|
||||
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
|
||||
@ -477,12 +490,25 @@ export class TestInfoImpl implements TestInfo {
|
||||
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
||||
}
|
||||
|
||||
snapshotPath(...pathSegments: string[]) {
|
||||
snapshotPath(...name: string[]): string;
|
||||
snapshotPath(name: string, options: { kind: 'snapshot' | 'screenshot' | 'aria' }): string;
|
||||
snapshotPath(...args: any[]) {
|
||||
let pathSegments: 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);
|
||||
}
|
||||
|
||||
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;
|
||||
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments, sanitizeFilePath);
|
||||
return this._resolveSnapshotPath(kind, pathSegments, sanitizeFilePath);
|
||||
}
|
||||
|
||||
skip(...args: [arg?: any, description?: string]) {
|
||||
|
979
packages/playwright/types/test.d.ts
vendored
979
packages/playwright/types/test.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -201,14 +201,19 @@ test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, tes
|
||||
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
|
||||
};
|
||||
`,
|
||||
'my-snapshots/dir/a.spec.ts/test.aria.yml': `
|
||||
'my-snapshots/dir/a.spec.ts/my-test.aria.yml': `
|
||||
- heading "hello world"
|
||||
`,
|
||||
'dir/a.spec.ts': `
|
||||
import path from 'path';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
const testDir = test.info().project.testDir;
|
||||
const screenshotPath = path.join(testDir, 'my-snapshots/dir/a.spec.ts/my-test.aria.yml');
|
||||
expect(test.info().snapshotPath('my_test.aria.yml', { kind: 'aria' })).toBe(screenshotPath);
|
||||
|
||||
await page.setContent(\`<h1>hello world</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.aria.yml' });
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'my_test.aria.yml' });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
@ -137,3 +137,18 @@ test('arg should receive default arg', async ({ runInlineTest }, testInfo) => {
|
||||
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
|
||||
expect(fs.existsSync(snapshotOutputPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should throw for unknown snapshot kind', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('is a test', async ({}) => {
|
||||
test.info().snapshotPath('foo', { kind: 'bar' });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain(`testInfo.snapshotPath: unknown kind "bar"`);
|
||||
});
|
||||
|
@ -782,8 +782,17 @@ test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runI
|
||||
'__screenshots__/a.spec.js/snapshot.png': blueImage,
|
||||
'actual-screenshots/a.spec.js/snapshot.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;
|
||||
|
||||
const screenshotPath = path.join(testDir, 'actual-screenshots/a.spec.js/snapshot.png');
|
||||
expect(test.info().snapshotPath('snapshot.png', { kind: 'screenshot' })).toBe(screenshotPath);
|
||||
|
||||
const snapshotPath = path.join(testDir, '__screenshots__/a.spec.js/snapshot.png');
|
||||
expect(test.info().snapshotPath('snapshot.png', { kind: 'snapshot' })).toBe(snapshotPath);
|
||||
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
|
@ -29,6 +29,11 @@ test('basics should work', async ({ runTSC }) => {
|
||||
expect(testInfo.title).toBe('my test');
|
||||
testInfo.annotations[0].type;
|
||||
test.setTimeout(123);
|
||||
testInfo.snapshotPath('a', 'b');
|
||||
testInfo.snapshotPath();
|
||||
testInfo.snapshotPath('foo.png', { kind: 'screenshot' });
|
||||
// @ts-expect-error
|
||||
testInfo.snapshotPath('a', 'b', { kind: 'aria' });
|
||||
});
|
||||
test.skip('my test', async () => {});
|
||||
test.fixme('my test', async () => {});
|
||||
|
5
utils/generate_types/overrides-test.d.ts
vendored
5
utils/generate_types/overrides-test.d.ts
vendored
@ -63,6 +63,11 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||
webServer: TestConfigWebServer | null;
|
||||
}
|
||||
|
||||
export interface TestInfo {
|
||||
snapshotPath(...name: ReadonlyArray<string>): string;
|
||||
snapshotPath(name: string, options: { kind: 'snapshot' | 'screenshot' | 'aria' }): string;
|
||||
}
|
||||
|
||||
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
||||
|
||||
export type TestDetailsAnnotation = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user