Skip to content

Commit fbad9f7

Browse files
authored
cherry-pick(#34537): feat: per-assertion snapshot path template in config (#34551)
1 parent 67313fa commit fbad9f7

13 files changed

+239
-110
lines changed

docs/src/api/class-locatorassertions.md

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

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

2266+
Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
2267+
22662268
**Usage**
22672269

22682270
```js
22692271
await expect(page.locator('body')).toMatchAriaSnapshot();
2270-
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
2271-
```
2272-
2273-
```python async
2274-
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
2275-
```
2276-
2277-
```python sync
2278-
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
2279-
```
2280-
2281-
```csharp
2282-
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
2283-
```
2284-
2285-
```java
2286-
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
2272+
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
22872273
```
22882274

22892275
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
22902276
* since: v1.50
22912277
* langs: js
22922278
- `name` <[string]>
22932279

2294-
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
2280+
Name of the snapshot to store in the snapshot folder corresponding to this test.
22952281
Generates sequential names if not specified.
22962282

22972283
### 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
@@ -164,7 +164,7 @@ export class FullProjectInternal {
164164
readonly fullyParallel: boolean;
165165
readonly expect: Project['expect'];
166166
readonly respectGitIgnore: boolean;
167-
readonly snapshotPathTemplate: string;
167+
readonly snapshotPathTemplate: string | undefined;
168168
readonly ignoreSnapshots: boolean;
169169
id = '';
170170
deps: FullProjectInternal[] = [];
@@ -173,8 +173,7 @@ export class FullProjectInternal {
173173
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
174174
this.fullConfig = fullConfig;
175175
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
176-
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
177-
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
176+
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
178177

179178
this.project = {
180179
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)