Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1350,3 +1350,89 @@ Allows locating elements by their title. For example, this method will find the
```html
<button title='Place the order'>Order Now</button>
```

## test-config-snapshot-template-path
- `type` ?<[string]>
Comment thread
aslushnikov marked this conversation as resolved.

This configuration option allows to set a string with template values for precise control over snapshot path location.
Comment thread
aslushnikov marked this conversation as resolved.

```js tab=js-ts
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
testDir: './tests',
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
};

export default config;
```

The value might include some "tokens" that will be replaced with actual values during test execution.

Consider the following file structure:

```
playwright.config.ts
tests/
└── page/
└── page-click.spec.ts
```

And the following `page-click.spec.ts` that uses `toHaveScreenshot()` call:

```ts
// page-click.spec.ts
import { test, expect } from '@playwright/test';

test('should work', async ({ page }) => {
await expect(page).toHaveScreenshot(['foo', 'bar', 'baz.png']);
});
```

The list of supported tokens:

* `{testDir}` - Project's `testDir`.
* Example: `tests/`
* `{snapshotDir}` - Project's `snapshotDir`.
* Example: `tests/` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
* `{platform}` - The value of `process.platform`.
* `{snapshotSuffix}` - The value of `testInfo.snapshotSuffix`.
Comment thread
aslushnikov marked this conversation as resolved.
* `{projectName}` - Project's sanitized name, if any.
* Example: `undefined`.
Comment thread
aslushnikov marked this conversation as resolved.
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
* Example: `page/`
* `{testFileName}` - Test file name with extension.
* Example: `page-click.spec.ts`
* `{testFilePath}` - Relative path from `testDir` to **test file**
* Example: `page/page-click.spec.ts`
* `{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.
* Example: `foo/bar/baz`
* `{ext}` - snapshot extension (with dots)
* Example: `.png`

Each token can be preceded with a single character that will be used **only if** this token has non-empty value.

Consider the following config:

```js tab=js-ts
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
snapshotPathTemplate: '__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
testMatch: 'example.spec.ts',
projects: [
{ use: { browserName: 'firefox' } },
{ name: 'chromium', use: { browserName: 'chromium' } },
],
};
export default config;
```

In this config:
1. First project **does not** have a name, so its snapshots will be stored in `<configDir>/__screenshots__/example.spec.ts/...`.
1. Second project **does** have a name, so its snapshots will be stored in `<configDir>/__screenshots__/chromium/example.spec.ts/..`.
1. Since `snapshotPathTemplate` resolves to relative path, it will be resolved relative to `configDir`.
1. Forward slashes `"/"` can be used as path separators regarding of the platform and work everywhere.
Comment thread
aslushnikov marked this conversation as resolved.

40 changes: 3 additions & 37 deletions docs/src/test-api/class-testconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ The directory for each test can be accessed by [`property: TestInfo.snapshotDir`

This path will serve as the base directory for each test file snapshot directory. Setting `snapshotDir` to `'snapshots'`, the [`property: TestInfo.snapshotDir`] would resolve to `snapshots/a.spec.js-snapshots`.

## property: TestConfig.snapshotPathTemplate = %%-test-config-snapshot-template-path-%%
* since: v1.28

## property: TestConfig.preserveOutput
* since: v1.10
- type: ?<[PreserveOutput]<"always"|"never"|"failures-only">>
Expand Down Expand Up @@ -441,43 +444,6 @@ const config: PlaywrightTestConfig = {
export default config;
```

## property: TestConfig.screenshotsDir
* since: v1.10
* experimental
- type: ?<[string]>

The base directory, relative to the config file, for screenshot files created with [`method: PageAssertions.toHaveScreenshot#1`]. Defaults to

```
<directory-of-configuration-file>/__screenshots__/<platform name>/<project name>
```

This path will serve as the base directory for each test file screenshot directory. For example, the following test structure:

```
smoke-tests/
└── basic.spec.ts
```

will result in the following screenshots folder structure:

```
__screenshots__/
└── darwin/
├── Mobile Safari/
│ └── smoke-tests/
│ └── basic.spec.ts/
│ └── screenshot-expectation.png
└── Desktop Chrome/
└── smoke-tests/
└── basic.spec.ts/
└── screenshot-expectation.png
```

where:
* `darwin/` - a platform name folder
* `Mobile Safari` and `Desktop Chrome` - project names

## property: TestConfig.shard
* since: v1.10
- type: ?<[null]|[Object]>
Expand Down
2 changes: 2 additions & 0 deletions docs/src/test-api/class-testinfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ Line number where the currently running test is declared.

Absolute path to the snapshot output directory for this specific test. Each test suite gets its own directory so they cannot conflict.

This property does not account for the [`property: TestProject.snapshotPathTemplate`] configuration.

## property: TestInfo.outputDir
* since: v1.10
- type: <[string]>
Expand Down
41 changes: 3 additions & 38 deletions docs/src/test-api/class-testproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,44 +168,6 @@ Project name is visible in the report and during test execution.

Project setup files that would be executed before all tests in the project. If project setup fails the tests in this project will be skipped. All project setup files will run in every shard if the project is sharded.

## property: TestProject.screenshotsDir
* since: v1.10
* experimental
- type: ?<[string]>

The base directory, relative to the config file, for screenshot files created with `toHaveScreenshot`. Defaults to

```
<directory-of-configuration-file>/__screenshots__/<platform name>/<project name>
```

This path will serve as the base directory for each test file screenshot directory. For example, the following test structure:

```
smoke-tests/
└── basic.spec.ts
```

will result in the following screenshots folder structure:

```
__screenshots__/
└── darwin/
├── Mobile Safari/
│ └── smoke-tests/
│ └── basic.spec.ts/
│ └── screenshot-expectation.png
└── Desktop Chrome/
└── smoke-tests/
└── basic.spec.ts/
└── screenshot-expectation.png
```

where:
* `darwin/` - a platform name folder
* `Mobile Safari` and `Desktop Chrome` - project names


## property: TestProject.snapshotDir
* since: v1.10
- type: ?<[string]>
Expand All @@ -216,6 +178,9 @@ The directory for each test can be accessed by [`property: TestInfo.snapshotDir`

This path will serve as the base directory for each test file snapshot directory. Setting `snapshotDir` to `'snapshots'`, the [`property: TestInfo.snapshotDir`] would resolve to `snapshots/a.spec.js-snapshots`.

## property: TestProject.snapshotPathTemplate = %%-test-config-snapshot-template-path-%%
* since: v1.28

## property: TestProject.outputDir
* since: v1.10
- type: ?<[string]>
Expand Down
13 changes: 3 additions & 10 deletions packages/playwright-test/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ export class Loader {
config.testDir = path.resolve(configDir, config.testDir);
if (config.outputDir !== undefined)
config.outputDir = path.resolve(configDir, config.outputDir);
if ((config as any).screenshotsDir !== undefined)
(config as any).screenshotsDir = path.resolve(configDir, (config as any).screenshotsDir);
if (config.snapshotDir !== undefined)
config.snapshotDir = path.resolve(configDir, config.snapshotDir);

Expand Down Expand Up @@ -267,8 +265,6 @@ export class Loader {
projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir);
if (projectConfig.outputDir !== undefined)
projectConfig.outputDir = path.resolve(this._configDir, projectConfig.outputDir);
if ((projectConfig as any).screenshotsDir !== undefined)
(projectConfig as any).screenshotsDir = path.resolve(this._configDir, (projectConfig as any).screenshotsDir);
if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir);

Expand All @@ -280,11 +276,8 @@ export class Loader {
const name = takeFirst(projectConfig.name, config.name, '');
const _setup = takeFirst(projectConfig.setup, []);

let screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
if (process.env.PLAYWRIGHT_DOCKER) {
screenshotsDir = path.join(testDir, '__screenshots__', name);
process.env.PWTEST_USE_SCREENSHOTS_DIR = '1';
}
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
const snapshotPathTemplate = takeFirst((projectConfig as any).snapshotPathTemplate, (config as any).snapshotPathTemplate, defaultSnapshotPathTemplate);
Comment thread
aslushnikov marked this conversation as resolved.
return {
_id: '',
_fullConfig: fullConfig,
Expand All @@ -301,7 +294,7 @@ export class Loader {
_setup,
_respectGitIgnore: respectGitIgnore,
snapshotDir,
_screenshotsDir: screenshotsDir,
snapshotPathTemplate: snapshotPathTemplate,
Comment thread
aslushnikov marked this conversation as resolved.
testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []),
testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).*'),
timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout),
Expand Down
4 changes: 1 addition & 3 deletions packages/playwright-test/src/matchers/toMatchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,7 @@ export async function toHaveScreenshot(
return { pass: !this.isNot, message: () => '' };

const config = (testInfo.project._expect as any)?.toHaveScreenshot;
const snapshotPathResolver = process.env.PWTEST_USE_SCREENSHOTS_DIR
? testInfo._screenshotPath.bind(testInfo)
: testInfo.snapshotPath.bind(testInfo);
const snapshotPathResolver = testInfo.snapshotPath.bind(testInfo);
const helper = new SnapshotHelper(
testInfo, snapshotPathResolver, 'png',
{
Expand Down
41 changes: 17 additions & 24 deletions packages/playwright-test/src/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { Loader } from './loader';
import type { TestCase } from './test';
import { TimeoutManager } from './timeoutManager';
import type { Annotation, FullConfigInternal, FullProjectInternal, TestStepInternal } from './types';
import { addSuffixToFilePath, getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from './util';
import { getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from './util';

export class TestInfoImpl implements TestInfo {
private _addStepImpl: (data: Omit<TestStepInternal, 'complete'>) => TestStepInternal;
Expand All @@ -32,7 +32,6 @@ export class TestInfoImpl implements TestInfo {
readonly _startTime: number;
readonly _startWallTime: number;
private _hasHardError: boolean = false;
readonly _screenshotsDir: string;
readonly _onTestFailureImmediateCallbacks = new Map<() => Promise<void>, string>(); // fn -> title
_didTimeout = false;

Expand Down Expand Up @@ -130,10 +129,6 @@ export class TestInfoImpl implements TestInfo {
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile);
return path.join(this.project.snapshotDir, relativeTestFilePath + '-snapshots');
})();
this._screenshotsDir = (() => {
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile);
return path.join(this.project._screenshotsDir, relativeTestFilePath);
})();
}

private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string]) {
Expand Down Expand Up @@ -240,25 +235,23 @@ export class TestInfoImpl implements TestInfo {
}

snapshotPath(...pathSegments: string[]) {
let suffix = '';
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
if (projectNamePathSegment)
suffix += '-' + projectNamePathSegment;
if (this.snapshotSuffix)
suffix += '-' + this.snapshotSuffix;
const subPath = addSuffixToFilePath(path.join(...pathSegments), suffix);
const snapshotPath = getContainedPath(this.snapshotDir, subPath);
if (snapshotPath)
return snapshotPath;
throw new Error(`The snapshotPath is not allowed outside of the parent directory. Please fix the defined path.\n\n\tsnapshotPath: ${subPath}`);
}

_screenshotPath(...pathSegments: string[]) {
const subPath = path.join(...pathSegments);
const screenshotPath = getContainedPath(this._screenshotsDir, subPath);
if (screenshotPath)
return screenshotPath;
throw new Error(`Screenshot name "${subPath}" should not point outside of the parent directory.`);
const parsedSubPath = path.parse(subPath);
const relativeTestFilePath = path.relative(this.project.testDir, this._test._requireFile);
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
const snapshotPath = path.resolve(this.config._configDir, this.project.snapshotPathTemplate
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : ''))
.replace(/\{(.)?platform\}/g, '$1' + process.platform)
.replace(/\{(.)?projectName\}/g, projectNamePathSegment ? '$1' + projectNamePathSegment : projectNamePathSegment)
Comment thread
aslushnikov marked this conversation as resolved.
.replace(/\{(.)?testFileDir\}/g, '$1' + parsedRelativeTestFilePath.dir)
.replace(/\{(.)?testFileName\}/g, '$1' + parsedRelativeTestFilePath.base)
.replace(/\{(.)?testFilePath\}/g, '$1' + relativeTestFilePath)
.replace(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name))
.replace(/\{(.)?ext\}/g, '$1' + parsedSubPath.ext);
Comment thread
aslushnikov marked this conversation as resolved.
return path.normalize(snapshotPath);
}

skip(...args: [arg?: any, description?: string]) {
Expand Down
1 change: 0 additions & 1 deletion packages/playwright-test/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export interface FullProjectInternal extends FullProjectPublic {
_fullConfig: FullConfigInternal;
_fullyParallel: boolean;
_expect: Project['expect'];
_screenshotsDir: string;
_respectGitIgnore: boolean;
_setup: string | RegExp | (string | RegExp)[];
}
Expand Down
Loading