Skip to content

Commit ba65016

Browse files
authored
feat: per-assertion snapshot path template in config (#34537)
1 parent b552637 commit ba65016

13 files changed

+239
-110
lines changed

docs/src/api/class-locatorassertions.md

+4-18
Original file line numberDiff line numberDiff line change
@@ -2245,35 +2245,21 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
22452245

22462246
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
22472247

2248+
Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
2249+
22482250
**Usage**
22492251

22502252
```js
22512253
await expect(page.locator('body')).toMatchAriaSnapshot();
2252-
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
2253-
```
2254-
2255-
```python async
2256-
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
2257-
```
2258-
2259-
```python sync
2260-
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
2261-
```
2262-
2263-
```csharp
2264-
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
2265-
```
2266-
2267-
```java
2268-
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
2254+
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
22692255
```
22702256

22712257
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
22722258
* since: v1.50
22732259
* langs: js
22742260
- `name` <[string]>
22752261

2276-
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
2262+
Name of the snapshot to store in the snapshot folder corresponding to this test.
22772263
Generates sequential names if not specified.
22782264

22792265
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%

docs/src/api/params.md

+21-7
Original file line numberDiff line numberDiff line change
@@ -1758,7 +1758,9 @@ await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
17581758
- `type` ?<[string]>
17591759
* langs: js
17601760

1761-
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
1761+
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`], [`method: LocatorAssertions.toMatchAriaSnapshot#2`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
1762+
1763+
You can configure templates for each assertion separately in [`property: TestConfig.expect`].
17621764

17631765
**Usage**
17641766

@@ -1767,7 +1769,19 @@ import { defineConfig } from '@playwright/test';
17671769

17681770
export default defineConfig({
17691771
testDir: './tests',
1772+
1773+
// Single template for all assertions
17701774
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
1775+
1776+
// Assertion-specific templates
1777+
expect: {
1778+
toHaveScreenshot: {
1779+
pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
1780+
},
1781+
toMatchAriaSnapshot: {
1782+
pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
1783+
},
1784+
},
17711785
});
17721786
```
17731787

@@ -1798,22 +1812,22 @@ test.describe('suite', () => {
17981812

17991813
The list of supported tokens:
18001814

1801-
* `{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.
1815+
* `{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.
18021816
* Value: `foo/bar/baz`
1803-
* `{ext}` - snapshot extension (with dots)
1817+
* `{ext}` - Snapshot extension (with the leading dot).
18041818
* Value: `.png`
18051819
* `{platform}` - The value of `process.platform`.
18061820
* `{projectName}` - Project's file-system-sanitized name, if any.
18071821
* Value: `''` (empty string).
1808-
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
1822+
* `{snapshotDir}` - Project's [`property: TestProject.snapshotDir`].
18091823
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
1810-
* `{testDir}` - Project's [`property: TestConfig.testDir`].
1811-
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
1824+
* `{testDir}` - Project's [`property: TestProject.testDir`].
1825+
* Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with config)
18121826
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
18131827
* Value: `page`
18141828
* `{testFileName}` - Test file name with extension.
18151829
* Value: `page-click.spec.ts`
1816-
* `{testFilePath}` - Relative path from `testDir` to **test file**
1830+
* `{testFilePath}` - Relative path from `testDir` to **test file**.
18171831
* Value: `page/page-click.spec.ts`
18181832
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
18191833
* Value: `suite-test-should-work`

docs/src/release-notes-js.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
2121
```
2222

2323
* New method [`method: Test.step.skip`] to disable execution of a test step.
24-
24+
2525
```js
2626
test('some test', async ({ page }) => {
2727
await test.step('before running step', async () => {
@@ -49,6 +49,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
4949

5050
* Option [`property: TestConfig.webServer`] added a `gracefulShutdown` field for specifying a process kill signal other than the default `SIGKILL`.
5151
* Exposed [`property: TestStep.attachments`] from the reporter API to allow retrieval of all attachments created by that step.
52+
* New option `pathTemplate` for `toHaveScreenshot` and `toMatchAriaSnapshot` assertions in the [`property: TestConfig.expect`] configuration.
5253

5354
### UI updates
5455

docs/src/test-api/class-testconfig.md

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export default defineConfig({
4848
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
4949
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
5050
- `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`.
51+
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
52+
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
53+
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
5154
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
5255
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
5356
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.

docs/src/test-api/class-testproject.md

+3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ export default defineConfig({
9898
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: Page.screenshot.caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
9999
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
100100
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
101+
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestProject.snapshotPathTemplate`] for details.
102+
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
103+
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestProject.snapshotPathTemplate`] for details.
101104
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
102105
- `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`.
103106
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.

docs/src/test-snapshots-js.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
4848

4949
- `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`.
5050

51-
The snapshot name and path can be configured with [`snapshotPathTemplate`](./api/class-testproject#test-project-snapshot-path-template) in the playwright config.
51+
The snapshot name and path can be configured with [`property: TestConfig.snapshotPathTemplate`] in the playwright config.
5252

5353
## Updating screenshots
5454

packages/playwright/src/common/config.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export class FullProjectInternal {
170170
readonly fullyParallel: boolean;
171171
readonly expect: Project['expect'];
172172
readonly respectGitIgnore: boolean;
173-
readonly snapshotPathTemplate: string;
173+
readonly snapshotPathTemplate: string | undefined;
174174
readonly ignoreSnapshots: boolean;
175175
id = '';
176176
deps: FullProjectInternal[] = [];
@@ -179,8 +179,7 @@ export class FullProjectInternal {
179179
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
180180
this.fullConfig = fullConfig;
181181
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
182-
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
183-
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
182+
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
184183

185184
this.project = {
186185
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),

packages/playwright/src/matchers/toMatchAriaSnapshot.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export async function toMatchAriaSnapshot(
4949
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
5050

5151
const updateSnapshots = testInfo.config.updateSnapshots;
52+
const pathTemplate = testInfo._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate;
53+
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
5254

5355
const matcherOptions = {
5456
isNot: this.isNot,
@@ -63,15 +65,15 @@ export async function toMatchAriaSnapshot(
6365
timeout = options.timeout ?? this.timeout;
6466
} else {
6567
if (expectedParam?.name) {
66-
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
68+
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
6769
} else {
6870
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
6971
if (!snapshotNames) {
7072
snapshotNames = { anonymousSnapshotIndex: 0 };
7173
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
7274
}
7375
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
74-
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
76+
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml']);
7577
}
7678
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
7779
timeout = expectedParam?.timeout ?? this.timeout;

packages/playwright/src/matchers/toMatchSnapshot.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ class SnapshotHelper {
148148
outputBasePath = testInfo._getOutputPath(sanitizedName);
149149
this.attachmentBaseName = sanitizedName;
150150
}
151-
this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
151+
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
152+
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
152153
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
153154
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
154155
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');

packages/playwright/src/worker/testInfo.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -454,14 +454,15 @@ export class TestInfoImpl implements TestInfo {
454454
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
455455
}
456456

457-
snapshotPath(...pathSegments: string[]) {
457+
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
458458
const subPath = path.join(...pathSegments);
459459
const parsedSubPath = path.parse(subPath);
460460
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
461461
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
462462
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
463463

464-
const snapshotPath = (this._projectInternal.snapshotPathTemplate || '')
464+
const actualTemplate = (template || this._projectInternal.snapshotPathTemplate || defaultTemplate);
465+
const snapshotPath = actualTemplate
465466
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
466467
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
467468
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
@@ -477,6 +478,11 @@ export class TestInfoImpl implements TestInfo {
477478
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
478479
}
479480

481+
snapshotPath(...pathSegments: string[]) {
482+
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
483+
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
484+
}
485+
480486
skip(...args: [arg?: any, description?: string]) {
481487
this._modifier('skip', args);
482488
}

0 commit comments

Comments
 (0)