feat: per-assertion snapshot path template in config (#34537)

This commit is contained in:
Dmitry Gozman 2025-01-29 18:47:20 +00:00 committed by GitHub
parent b552637ee0
commit ba650161a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 239 additions and 110 deletions

View File

@ -2245,27 +2245,13 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
**Usage**
```js
await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
```
```python async
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```python sync
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
```
```java
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
```
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
@ -2273,7 +2259,7 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match
* langs: js
- `name` <[string]>
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
Name of the snapshot to store in the snapshot folder corresponding to this test.
Generates sequential names if not specified.
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%

View File

@ -1758,7 +1758,9 @@ await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
- `type` ?<[string]>
* langs: js
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`], [`method: LocatorAssertions.toMatchAriaSnapshot#2`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
You can configure templates for each assertion separately in [`property: TestConfig.expect`].
**Usage**
@ -1767,7 +1769,19 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Single template for all assertions
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
// Assertion-specific templates
expect: {
toHaveScreenshot: {
pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
},
toMatchAriaSnapshot: {
pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
},
},
});
```
@ -1798,22 +1812,22 @@ test.describe('suite', () => {
The list of supported tokens:
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
* `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be an auto-generated snapshot name.
* Value: `foo/bar/baz`
* `{ext}` - snapshot extension (with dots)
* `{ext}` - Snapshot extension (with the leading dot).
* Value: `.png`
* `{platform}` - The value of `process.platform`.
* `{projectName}` - Project's file-system-sanitized name, if any.
* Value: `''` (empty string).
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
* `{snapshotDir}` - Project's [`property: TestProject.snapshotDir`].
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{testDir}` - Project's [`property: TestConfig.testDir`].
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
* `{testDir}` - Project's [`property: TestProject.testDir`].
* Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with config)
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* Value: `page`
* `{testFileName}` - Test file name with extension.
* Value: `page-click.spec.ts`
* `{testFilePath}` - Relative path from `testDir` to **test file**
* `{testFilePath}` - Relative path from `testDir` to **test file**.
* Value: `page/page-click.spec.ts`
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* Value: `suite-test-should-work`

View File

@ -21,7 +21,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
```
* New method [`method: Test.step.skip`] to disable execution of a test step.
```js
test('some test', async ({ page }) => {
await test.step('before running step', async () => {
@ -49,6 +49,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
* Option [`property: TestConfig.webServer`] added a `gracefulShutdown` field for specifying a process kill signal other than the default `SIGKILL`.
* Exposed [`property: TestStep.attachments`] from the reporter API to allow retrieval of all attachments created by that step.
* New option `pathTemplate` for `toHaveScreenshot` and `toMatchAriaSnapshot` assertions in the [`property: TestConfig.expect`] configuration.
### UI updates

View File

@ -48,6 +48,9 @@ export default defineConfig({
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
- `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.

View File

@ -98,6 +98,9 @@ export default defineConfig({
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: Page.screenshot.caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestProject.snapshotPathTemplate`] for details.
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestProject.snapshotPathTemplate`] for details.
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.

View File

@ -48,7 +48,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
The snapshot name and path can be configured with [`snapshotPathTemplate`](./api/class-testproject#test-project-snapshot-path-template) in the playwright config.
The snapshot name and path can be configured with [`property: TestConfig.snapshotPathTemplate`] in the playwright config.
## Updating screenshots

View File

@ -170,7 +170,7 @@ export class FullProjectInternal {
readonly fullyParallel: boolean;
readonly expect: Project['expect'];
readonly respectGitIgnore: boolean;
readonly snapshotPathTemplate: string;
readonly snapshotPathTemplate: string | undefined;
readonly ignoreSnapshots: boolean;
id = '';
deps: FullProjectInternal[] = [];
@ -179,8 +179,7 @@ export class FullProjectInternal {
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
this.fullConfig = fullConfig;
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
this.project = {
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),

View File

@ -49,6 +49,8 @@ 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,
@ -63,7 +65,7 @@ export async function toMatchAriaSnapshot(
timeout = options.timeout ?? this.timeout;
} else {
if (expectedParam?.name) {
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
} else {
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
if (!snapshotNames) {
@ -71,7 +73,7 @@ export async function toMatchAriaSnapshot(
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
}
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml']);
}
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
timeout = expectedParam?.timeout ?? this.timeout;

View File

@ -148,7 +148,8 @@ class SnapshotHelper {
outputBasePath = testInfo._getOutputPath(sanitizedName);
this.attachmentBaseName = sanitizedName;
}
this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');

View File

@ -454,14 +454,15 @@ export class TestInfoImpl implements TestInfo {
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
}
snapshotPath(...pathSegments: string[]) {
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
const subPath = path.join(...pathSegments);
const parsedSubPath = path.parse(subPath);
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
const snapshotPath = (this._projectInternal.snapshotPathTemplate || '')
const actualTemplate = (template || this._projectInternal.snapshotPathTemplate || defaultTemplate);
const snapshotPath = actualTemplate
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
@ -477,6 +478,11 @@ export class TestInfoImpl implements TestInfo {
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
}
snapshotPath(...pathSegments: string[]) {
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
}
skip(...args: [arg?: any, description?: string]) {
this._modifier('skip', args);
}

View File

@ -214,6 +214,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
*/
stylePath?: string|Array<string>;
/**
* A template controlling location of the screenshots. See
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
* Configuration for the
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* method.
*/
toMatchAriaSnapshot?: {
/**
* A template controlling location of the aria snapshots. See
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
@ -404,10 +425,14 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
/**
* This option configures a template controlling location of snapshots generated by
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
* You can configure templates for each assertion separately in
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
*
* **Usage**
*
* ```js
@ -416,7 +441,19 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
*
* export default defineConfig({
* testDir: './tests',
*
* // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
*
* // Assertion-specific templates
* expect: {
* toHaveScreenshot: {
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
* },
* toMatchAriaSnapshot: {
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
* },
* },
* });
* ```
*
@ -447,27 +484,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
* ```
*
* The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
* an auto-generated snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
* - `{testFilePath}` - Relative path from `testDir` to **test file**
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@ -991,6 +1028,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
*/
threshold?: number;
/**
* A template controlling location of the screenshots. See
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
* Configuration for the
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* method.
*/
toMatchAriaSnapshot?: {
/**
* A template controlling location of the aria snapshots. See
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
* for details.
*/
pathTemplate?: string;
};
/**
@ -1494,10 +1552,14 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* This option configures a template controlling location of snapshots generated by
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* and
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
*
* You can configure templates for each assertion separately in
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
*
* **Usage**
*
* ```js
@ -1506,7 +1568,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
*
* export default defineConfig({
* testDir: './tests',
*
* // Single template for all assertions
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
*
* // Assertion-specific templates
* expect: {
* toHaveScreenshot: {
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
* },
* toMatchAriaSnapshot: {
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
* },
* },
* });
* ```
*
@ -1537,27 +1611,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
* ```
*
* The list of supported tokens:
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
* snapshot name.
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
* an auto-generated snapshot name.
* - Value: `foo/bar/baz`
* - `{ext}` - snapshot extension (with dots)
* - `{ext}` - Snapshot extension (with the leading dot).
* - Value: `.png`
* - `{platform}` - The value of `process.platform`.
* - `{projectName}` - Project's file-system-sanitized name, if any.
* - Value: `''` (empty string).
* - `{snapshotDir}` - Project's
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* - `{testDir}` - Project's
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
* config)
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* - Value: `page`
* - `{testFileName}` - Test file name with extension.
* - Value: `page-click.spec.ts`
* - `{testFilePath}` - Relative path from `testDir` to **test file**
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
* - Value: `page/page-click.spec.ts`
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
* - Value: `suite-test-should-work`
@ -8711,19 +8785,22 @@ interface LocatorAssertions {
/**
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
*
* Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate`
* and/or `snapshotPathTemplate` properties in the configuration file.
*
* **Usage**
*
* ```js
* await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
* ```
*
* @param options
*/
toMatchAriaSnapshot(options?: {
/**
* Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
* names if not specified.
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates sequential names if not
* specified.
*/
name?: string;

View File

@ -22,12 +22,7 @@ test.describe.configure({ mode: 'parallel' });
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test.yml': `
'a.spec.ts-snapshots/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
@ -44,11 +39,6 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
@ -61,25 +51,20 @@ test('should generate multiple missing', async ({ runInlineTest }, testInfo) =>
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test-1.yml': `
'a.spec.ts-snapshots/test-1.yml': `
- heading "foo"
`,
'__snapshots__/a.spec.ts/test-2.yml': `
'a.spec.ts-snapshots/test-2.yml': `
- heading "bar"
`,
'a.spec.ts': `
@ -94,22 +79,17 @@ test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml`);
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.yml`);
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-2.yml`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test.yml': `
'a.spec.ts-snapshots/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
@ -122,17 +102,12 @@ test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
}, { 'update-snapshots': 'changed' });
expect(result.exitCode).toBe(0);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test.yml'), 'utf8');
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test.yml'), 'utf8');
expect(snapshot1.trim()).toBe('- heading "hello world"');
});
test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test name', async ({ page }) => {
@ -145,11 +120,11 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-1.yml'), 'utf8');
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
@ -158,7 +133,6 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
updateSnapshots: '${updateSnapshots}',
};
`,
@ -169,13 +143,13 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
'__snapshots__/a.spec.ts/test-1.yml': '- heading "Old content" [level=1]',
'a.spec.ts-snapshots/test-1.yml': '- heading "Old content" [level=1]',
});
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
expect(result.exitCode).toBe(rebase ? 0 : 1);
if (rebase) {
const snapshotOutputPath = testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml');
const snapshotOutputPath = testInfo.outputPath('a.spec.ts-snapshots/test-1.yml');
expect(result.output).toContain(`A snapshot is generated at`);
const data = fs.readFileSync(snapshotOutputPath);
expect(data.toString()).toBe('- heading "New content" [level=1]');
@ -187,14 +161,6 @@ for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
test('should respect timeout', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import path from 'path';
@ -203,9 +169,61 @@ test('should respect timeout', async ({ runInlineTest }, testInfo) => {
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
});
`,
'__snapshots__/a.spec.ts/test-1.yml': '- heading "new world" [level=1]',
'a.spec.ts-snapshots/test-1.yml': '- heading "new world" [level=1]',
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Timed out 1ms waiting for`);
});
test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
};
`,
'my-snapshots/dir/a.spec.ts/test.yml': `
- heading "hello world"
`,
'dir/a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should respect config.expect.toMatchAriaSnapshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
expect: {
toMatchAriaSnapshot: {
pathTemplate: 'actual-snapshots/{testFilePath}/{arg}{ext}',
},
},
};
`,
'my-snapshots/dir/a.spec.ts/test.yml': `
- heading "wrong one"
`,
'actual-snapshots/dir/a.spec.ts/test.yml': `
- heading "hello world"
`,
'dir/a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View File

@ -740,6 +740,25 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
});
test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
...playwrightConfig({
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
expect: { toHaveScreenshot: { pathTemplate: 'actual-screenshots/{testFilePath}/{arg}{ext}' } },
}),
'__screenshots__/a.spec.js/snapshot.png': blueImage,
'actual-screenshots/a.spec.js/snapshot.png': whiteImage,
'a.spec.js': `
const { test, expect } = require('@playwright/test');
test('is a test', async ({ page }) => {
await expect(page).toHaveScreenshot('snapshot.png');
});
`
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
const EXPECTED_SNAPSHOT = blueImage;
const result = await runInlineTest({