Revert "chore: hide setup, store, TestProject.setupMatch, storageStat… (#19756)

…eName (#19442)"

This reverts commit 92dd734e04.
This commit is contained in:
Yury Semikhatsky 2022-12-28 15:39:31 -08:00 committed by GitHub
parent d912cbf115
commit 137070d889
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 537 additions and 337 deletions

View file

@ -205,6 +205,14 @@ Learn more about [automatic screenshots](../test-configuration.md#automatic-scre
## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%% ## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%%
* since: v1.10 * since: v1.10
## property: TestOptions.storageStateName
* since: v1.29
- type: <[string]>
Name of the [TestStore] entry that should be used to initialize [`property: TestOptions.storageState`]. The value must be
written to the test storage before creation of a browser context that uses it (usually in [`property: TestProject.setupMatch`]). If both
this property and [`property: TestOptions.storageState`] are specified, this property will always take precedence.
## property: TestOptions.testIdAttribute ## property: TestOptions.testIdAttribute
* since: v1.27 * since: v1.27

View file

@ -146,7 +146,7 @@ Filter to only run tests with a title matching one of the patterns. For example,
* since: v1.10 * since: v1.10
- type: ?<[RegExp]|[Array]<[RegExp]>> - type: ?<[RegExp]|[Array]<[RegExp]>>
Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of [`property: TestProject.grep`]. Also available globally and in the [command line](../test-cli.md) with the `--grep-invert` option. Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of [`property: TestProject.grep`]. Also available globally and in the [command line](../test-cli.md) with the `--grep-invert` option. This filter and its command line counterpart also applies to the setup files. If all [`property: TestProject.setupMatch`] tests match the filter Playwright **will** run all setup files before running the matching tests.
`grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests). `grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests).
@ -162,6 +162,18 @@ Metadata that will be put directly to the test report serialized as JSON.
Project name is visible in the report and during test execution. Project name is visible in the report and during test execution.
## property: TestProject.setupMatch
* since: v1.29
- type: ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Project setup files that will be executed before all tests in the project.
**Details**
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.grep`] and [`property: TestProject.grepInvert`] and their command line counterparts also apply to the setup files. If such filters match only tests in the project, Playwright will run **all** setup files before running the matching tests.
If there is a file that matches both [`property: TestProject.setupMatch`] and [`property: TestProject.testMatch`] filters an error will be thrown.
## property: TestProject.snapshotDir ## property: TestProject.snapshotDir
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>

View file

@ -0,0 +1,56 @@
# class: TestStore
* since: v1.29
* langs: js
Playwright Test provides a global `store` object for passing values between project setup and tests. It is
an error to call store methods outside of setup and tests.
```js tab=js-js
const { setup, store } = require('@playwright/test');
setup('sign in', async ({ page, context }) => {
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await context.storageState();
await store.set('test-user', contextState)
});
```
```js tab=js-ts
import { setup, store } from '@playwright/test';
setup('sign in', async ({ page, context }) => {
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await context.storageState();
await store.set('test-user', contextState)
});
```
## async method: TestStore.get
* since: v1.29
- returns: <[any]>
Get named item from the store. Returns undefined if there is no value with given name.
### param: TestStore.get.name
* since: v1.29
- `name` <[string]>
Item name.
## async method: TestStore.set
* since: v1.29
Set value to the store.
### param: TestStore.set.name
* since: v1.29
- `name` <[string]>
Item name.
### param: TestStore.set.value
* since: v1.29
- `value` <[any]>
Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.

View file

@ -27,7 +27,7 @@ import { rootTestType, _setProjectSetup } from './testType';
export { expect } from './expect'; export { expect } from './expect';
export { addRunnerPlugin as _addRunnerPlugin } from './plugins'; export { addRunnerPlugin as _addRunnerPlugin } from './plugins';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
export const _store = _baseStore; export const store = _baseStore;
if ((process as any)['__pw_initiator__']) { if ((process as any)['__pw_initiator__']) {
const originalStackTraceLimit = Error.stackTraceLimit; const originalStackTraceLimit = Error.stackTraceLimit;
@ -47,7 +47,6 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_reuseContext: boolean, _reuseContext: boolean,
_setupContextOptionsAndArtifacts: void; _setupContextOptionsAndArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>; _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
_storageStateName: string | undefined;
}; };
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
_browserOptions: LaunchOptions; _browserOptions: LaunchOptions;
@ -144,7 +143,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }], permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }], proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }], storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
_storageStateName: [undefined, { option: true }], storageStateName: [undefined, { option: true }],
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }], timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }], userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }], viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
@ -174,7 +173,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions, permissions,
proxy, proxy,
storageState, storageState,
_storageStateName, storageStateName,
viewport, viewport,
timezoneId, timezoneId,
userAgent, userAgent,
@ -213,10 +212,10 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
options.permissions = permissions; options.permissions = permissions;
if (proxy !== undefined) if (proxy !== undefined)
options.proxy = proxy; options.proxy = proxy;
if (_storageStateName !== undefined) { if (storageStateName !== undefined) {
const value = await _store.get(_storageStateName); const value = await store.get(storageStateName);
if (!value) if (!value)
throw new Error(`Cannot find value in the _store for _storageStateName: "${_storageStateName}"`); throw new Error(`Cannot find value in the store for storageStateName: "${storageStateName}"`);
options.storageState = value as any; options.storageState = value as any;
} else if (storageState !== undefined) { } else if (storageState !== undefined) {
options.storageState = storageState; options.storageState = storageState;
@ -629,7 +628,7 @@ function normalizeScreenshotMode(screenshot: PlaywrightWorkerOptions['screenshot
const kTracingStarted = Symbol('kTracingStarted'); const kTracingStarted = Symbol('kTracingStarted');
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures); export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export const _setup = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures); export const setup = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
_setProjectSetup(_setup, true); _setProjectSetup(setup, true);
export default test; export default test;

View file

@ -275,7 +275,7 @@ export class Loader {
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir); const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, ''); const name = takeFirst(projectConfig.name, config.name, '');
const _setupMatch = takeFirst((projectConfig as any)._setupMatch, []); const _setupMatch = takeFirst(projectConfig.setupMatch, []);
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
@ -614,7 +614,7 @@ function validateProject(file: string, project: Project, title: string) {
throw errorWithFile(file, `${title}.testDir must be a string`); throw errorWithFile(file, `${title}.testDir must be a string`);
} }
for (const prop of ['testIgnore', 'testMatch'] as const) { for (const prop of ['testIgnore', 'testMatch', 'setupMatch'] as const) {
if (prop in project && project[prop] !== undefined) { if (prop in project && project[prop] !== undefined) {
const value = project[prop]; const value = project[prop];
if (Array.isArray(value)) { if (Array.isArray(value)) {

View file

@ -258,13 +258,13 @@ export class Runner {
if (!isTest && !isSetup) if (!isTest && !isSetup)
return false; return false;
if (isSetup && isTest) if (isSetup && isTest)
throw new Error(`File "${file}" matches both '_setup' and 'testMatch' filters in project "${project.name}"`); throw new Error(`File "${file}" matches both 'setup' and 'testMatch' filters in project "${project.name}"`);
if (fileToProjectName.has(file)) { if (fileToProjectName.has(file)) {
if (isSetup) { if (isSetup) {
if (!setupFiles.has(file)) if (!setupFiles.has(file))
throw new Error(`File "${file}" matches '_setup' filter in project "${project.name}" and 'testMatch' filter in project "${fileToProjectName.get(file)}"`); throw new Error(`File "${file}" matches 'setup' filter in project "${project.name}" and 'testMatch' filter in project "${fileToProjectName.get(file)}"`);
} else if (setupFiles.has(file)) { } else if (setupFiles.has(file)) {
throw new Error(`File "${file}" matches '_setup' filter in project "${fileToProjectName.get(file)}" and 'testMatch' filter in project "${project.name}"`); throw new Error(`File "${file}" matches 'setup' filter in project "${fileToProjectName.get(file)}" and 'testMatch' filter in project "${project.name}"`);
} }
} }
fileToProjectName.set(file, project.name); fileToProjectName.set(file, project.name);

View file

@ -16,10 +16,11 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { TestStore } from '../types/test';
import { currentTestInfo } from './globals'; import { currentTestInfo } from './globals';
import { sanitizeForFilePath, trimLongString } from './util'; import { sanitizeForFilePath, trimLongString } from './util';
class JsonStore { class JsonStore implements TestStore {
private _toFilePath(name: string) { private _toFilePath(name: string) {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)

View file

@ -84,14 +84,14 @@ export class TestTypeImpl {
if (this._projectSetup) if (this._projectSetup)
addFatalError(`${title} is called in a file which is not a part of project setup.`, location); addFatalError(`${title} is called in a file which is not a part of project setup.`, location);
else else
addFatalError(`${title} is called in a project setup file (use '_setup' instead of 'test').`, location); addFatalError(`${title} is called in a project setup file (use 'setup' instead of 'test').`, location);
} }
return suite; return suite;
} }
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) { private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._currentSuite(location, this._projectSetup ? '_setup()' : 'test()'); const suite = this._currentSuite(location, this._projectSetup ? 'setup()' : 'test()');
if (!suite) if (!suite)
return; return;
const test = new TestCase(title, fn, this, location); const test = new TestCase(title, fn, this, location);
@ -150,7 +150,7 @@ export class TestTypeImpl {
} }
private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, fn: Function) { private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, fn: Function) {
const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.${name}()`); const suite = this._currentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.${name}()`);
if (!suite) if (!suite)
return; return;
suite._hooks.push({ type: name, fn, location }); suite._hooks.push({ type: name, fn, location });
@ -158,7 +158,7 @@ export class TestTypeImpl {
private _configure(location: Location, options: { mode?: 'parallel' | 'serial', retries?: number, timeout?: number }) { private _configure(location: Location, options: { mode?: 'parallel' | 'serial', retries?: number, timeout?: number }) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.describe.configure()`); const suite = this._currentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.describe.configure()`);
if (!suite) if (!suite)
return; return;
@ -225,7 +225,7 @@ export class TestTypeImpl {
} }
private _use(location: Location, fixtures: Fixtures) { private _use(location: Location, fixtures: Fixtures) {
const suite = this._currentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.use()`); const suite = this._currentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.use()`);
if (!suite) if (!suite)
return; return;
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });
@ -234,7 +234,7 @@ export class TestTypeImpl {
private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> { private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) { if (!testInfo) {
addFatalError(`${this._projectSetup ? '_setup' : 'test'}.step() can only be called from a test`, location); addFatalError(`${this._projectSetup ? 'setup' : 'test'}.step() can only be called from a test`, location);
return undefined as any; return undefined as any;
} }
const step = testInfo._addStep({ const step = testInfo._addStep({

View file

@ -193,7 +193,10 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
/** /**
* Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of * Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of
* [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. This filter and its command line
* counterpart also applies to the setup files. If all
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) tests match
* the filter Playwright **will** run all setup files before running the matching tests.
* *
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests). * `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/ */
@ -2826,6 +2829,35 @@ type ConnectOptions = {
timeout?: number; timeout?: number;
}; };
/**
* Playwright Test provides a global `store` object for passing values between project setup and tests. It is an error
* to call store methods outside of setup and tests.
*
* ```js
* import { setup, store } from '@playwright/test';
*
* setup('sign in', async ({ page, context }) => {
* // Save signed-in state to an entry named 'github-test-user'.
* const contextState = await context.storageState();
* await store.set('test-user', contextState)
* });
* ```
*
*/
export interface TestStore {
/**
* Get named item from the store. Returns undefined if there is no value with given name.
* @param name Item name.
*/
get<T>(name: string): Promise<T | undefined>;
/**
* Set value to the store.
* @param name Item name.
* @param value Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.
*/
set<T>(name: string, value: T | undefined): Promise<void>;
}
/** /**
* Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more. * Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more.
* *
@ -3062,6 +3094,16 @@ export interface PlaywrightTestOptions {
* Either a path to the file with saved storage, or an object with the following fields: * Either a path to the file with saved storage, or an object with the following fields:
*/ */
storageState: StorageState | undefined; storageState: StorageState | undefined;
/**
* Name of the [TestStore] entry that should be used to initialize
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state). The value
* must be written to the test storage before creation of a browser context that uses it (usually in
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match)). If both this
* property and
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state) are
* specified, this property will always take precedence.
*/
storageStateName: string | undefined;
/** /**
* Changes the timezone of the context. See * Changes the timezone of the context. See
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) * [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
@ -3345,6 +3387,7 @@ export default test;
export const _baseTest: TestType<{}, {}>; export const _baseTest: TestType<{}, {}>;
export const expect: Expect; export const expect: Expect;
export const store: TestStore;
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {}; export {};
@ -4639,7 +4682,10 @@ interface TestProject {
/** /**
* Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of * Filter to only run tests with a title **not** matching one of the patterns. This is the opposite of
* [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally * [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep). Also available globally
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option. This filter and its command line
* counterpart also applies to the setup files. If all
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) tests match
* the filter Playwright **will** run all setup files before running the matching tests.
* *
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests). * `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/ */
@ -4655,6 +4701,24 @@ interface TestProject {
*/ */
name?: string; name?: string;
/**
* Project setup files that will be executed before all tests in the project.
*
* **Details**
*
* 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. [testProject.grep](https://playwright.dev/docs/api/class-testproject#test-project-grep)
* and [testProject.grepInvert](https://playwright.dev/docs/api/class-testproject#test-project-grep-invert) and their
* command line counterparts also apply to the setup files. If such filters match only tests in the project,
* Playwright will run **all** setup files before running the matching tests.
*
* If there is a file that matches both
* [testProject.setupMatch](https://playwright.dev/docs/api/class-testproject#test-project-setup-match) and
* [testProject.testMatch](https://playwright.dev/docs/api/class-testproject#test-project-test-match) filters an error
* will be thrown.
*/
setupMatch?: string|RegExp|Array<string|RegExp>;
/** /**
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to * The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir). * [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).

View file

@ -480,3 +480,41 @@ test('should have correct types for the config', async ({ runTSC }) => {
}); });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should throw when project.setupMatch has wrong type', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', setupMatch: 100 },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setupMatch must be a string or a RegExp`);
});
test('should throw when project.setupMatch has wrong array type', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
projects: [
{ name: 'a', setupMatch: [/100/, 100] },
],
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setupMatch[1] must be a string or a RegExp`);
});

View file

@ -17,7 +17,7 @@ import type { PlaywrightTestConfig, TestInfo, PlaywrightTestProject } from '@pla
import path from 'path'; import path from 'path';
import { test, expect } from './playwright-test-fixtures'; import { test, expect } from './playwright-test-fixtures';
function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject & any }): Record<string, string> { function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject }): Record<string, string> {
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })), projects: names.map(name => ({ ...projectTemplates?.[name], name, testDir: testInfo.outputPath(name) })),
}; };
@ -28,9 +28,9 @@ function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTe
test('${name} test', async () => { test('${name} test', async () => {
await new Promise(f => setTimeout(f, 100)); await new Promise(f => setTimeout(f, 100));
});`; });`;
files[`${name}/${name}._setup.ts`] = ` files[`${name}/${name}.setup.ts`] = `
const { _setup } = pwt; const { setup } = pwt;
_setup('${name} _setup', async () => { setup('${name} setup', async () => {
await new Promise(f => setTimeout(f, 100)); await new Promise(f => setTimeout(f, 100));
});`; });`;
} }
@ -98,15 +98,15 @@ function expectFilesRunBefore(timeline: Timeline, before: string[], after: strin
test('should work for one project', async ({ runGroups }, testInfo) => { test('should work for one project', async ({ runGroups }, testInfo) => {
const projectTemplates = { const projectTemplates = {
'a': { 'a': {
_setupMatch: ['**/*._setup.ts'] setupMatch: ['**/*.setup.ts']
}, },
}; };
const configWithFiles = createConfigWithProjects(['a'], testInfo, projectTemplates); const configWithFiles = createConfigWithProjects(['a'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles); const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(2); expect(passed).toBe(2);
expect(formatTimeline(timeline)).toEqual(`a > a${path.sep}a._setup.ts > a _setup [begin] expect(formatTimeline(timeline)).toEqual(`a > a${path.sep}a.setup.ts > a setup [begin]
a > a${path.sep}a._setup.ts > a _setup [end] a > a${path.sep}a.setup.ts > a setup [end]
a > a${path.sep}a.spec.ts > a test [begin] a > a${path.sep}a.spec.ts > a test [begin]
a > a${path.sep}a.spec.ts > a test [end]`); a > a${path.sep}a.spec.ts > a test [end]`);
}); });
@ -114,13 +114,13 @@ a > a${path.sep}a.spec.ts > a test [end]`);
test('should work for several projects', async ({ runGroups }, testInfo) => { test('should work for several projects', async ({ runGroups }, testInfo) => {
const projectTemplates = { const projectTemplates = {
'a': { 'a': {
_setupMatch: ['**/*._setup.ts'] setupMatch: ['**/*.setup.ts']
}, },
'b': { 'b': {
_setupMatch: /.*b._setup.ts/ setupMatch: /.*b.setup.ts/
}, },
'c': { 'c': {
_setupMatch: '**/c._setup.ts' setupMatch: '**/c.setup.ts'
}, },
}; };
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates); const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
@ -128,22 +128,22 @@ test('should work for several projects', async ({ runGroups }, testInfo) => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(6); expect(passed).toBe(6);
for (const name of ['a', 'b', 'c']) for (const name of ['a', 'b', 'c'])
expectFilesRunBefore(timeline, [`${name}${path.sep}${name}._setup.ts`], [`${name}${path.sep}${name}.spec.ts`]); expectFilesRunBefore(timeline, [`${name}${path.sep}${name}.setup.ts`], [`${name}${path.sep}${name}.spec.ts`]);
}); });
test('should stop project if _setup fails', async ({ runGroups }, testInfo) => { test('should stop project if setup fails', async ({ runGroups }, testInfo) => {
const projectTemplates = { const projectTemplates = {
'a': { 'a': {
_setupMatch: ['**/*._setup.ts'] setupMatch: ['**/*.setup.ts']
}, },
'b': { 'b': {
_setupMatch: /.*b._setup.ts/ setupMatch: /.*b.setup.ts/
}, },
}; };
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates); const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
configWithFiles[`a/a._setup.ts`] = ` configWithFiles[`a/a.setup.ts`] = `
const { _setup, expect } = pwt; const { setup, expect } = pwt;
_setup('a _setup', async () => { setup('a setup', async () => {
expect(1).toBe(2); expect(1).toBe(2);
});`; });`;
@ -152,17 +152,17 @@ test('should stop project if _setup fails', async ({ runGroups }, testInfo) => {
expect(passed).toBe(3); expect(passed).toBe(3);
expect(skipped).toBe(1); // 1 test from project 'a' expect(skipped).toBe(1); // 1 test from project 'a'
for (const name of ['a', 'b']) for (const name of ['a', 'b'])
expectFilesRunBefore(timeline, [`${name}${path.sep}${name}._setup.ts`], [`${name}${path.sep}${name}.spec.ts`]); expectFilesRunBefore(timeline, [`${name}${path.sep}${name}.setup.ts`], [`${name}${path.sep}${name}.spec.ts`]);
}); });
test('should run _setup in each project shard', async ({ runGroups }, testInfo) => { test('should run setup in each project shard', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -178,44 +178,44 @@ test('should run _setup in each project shard', async ({ runGroups }, testInfo)
test('test1', async () => { }); test('test1', async () => { });
test('test2', async () => { }); test('test2', async () => { });
`, `,
'c._setup.ts': ` 'c.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
}; };
{ // Shard 1/2 { // Shard 1/2
const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '1/2' }); const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '1/2' });
expect(output).toContain('Running 6 tests using 1 worker, shard 1 of 2'); expect(output).toContain('Running 6 tests using 1 worker, shard 1 of 2');
expect(fileNames(timeline)).toEqual(['a.test.ts', 'c._setup.ts']); expect(fileNames(timeline)).toEqual(['a.test.ts', 'c.setup.ts']);
expectFilesRunBefore(timeline, [`c._setup.ts`], [`a.test.ts`]); expectFilesRunBefore(timeline, [`c.setup.ts`], [`a.test.ts`]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(6); expect(passed).toBe(6);
} }
{ // Shard 2/2 { // Shard 2/2
const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '2/2' }); const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '2/2' });
expect(output).toContain('Running 4 tests using 1 worker, shard 2 of 2'); expect(output).toContain('Running 4 tests using 1 worker, shard 2 of 2');
expect(fileNames(timeline)).toEqual(['b.test.ts', 'c._setup.ts']); expect(fileNames(timeline)).toEqual(['b.test.ts', 'c.setup.ts']);
expectFilesRunBefore(timeline, [`c._setup.ts`], [`b.test.ts`]); expectFilesRunBefore(timeline, [`c.setup.ts`], [`b.test.ts`]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(4); expect(passed).toBe(4);
} }
}); });
test('should run _setup only for projects that have tests in the shard', async ({ runGroups }, testInfo) => { test('should run setup only for projects that have tests in the shard', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*p1._setup.ts$/, setupMatch: /.*p1.setup.ts$/,
testMatch: /.*a.test.ts/, testMatch: /.*a.test.ts/,
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*p2._setup.ts$/, setupMatch: /.*p2.setup.ts$/,
testMatch: /.*b.test.ts/, testMatch: /.*b.test.ts/,
}, },
] ]
@ -232,60 +232,60 @@ test('should run _setup only for projects that have tests in the shard', async (
test('test1', async () => { }); test('test1', async () => { });
test('test2', async () => { }); test('test2', async () => { });
`, `,
'p1._setup.ts': ` 'p1.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'p2._setup.ts': ` 'p2.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup3', async () => { }); setup('setup3', async () => { });
_setup('_setup4', async () => { }); setup('setup4', async () => { });
`, `,
}; };
{ // Shard 1/2 { // Shard 1/2
const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '1/2' }); const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '1/2' });
expect(output).toContain('Running 6 tests using 1 worker, shard 1 of 2'); expect(output).toContain('Running 6 tests using 1 worker, shard 1 of 2');
expect(fileNames(timeline)).toEqual(['a.test.ts', 'p1._setup.ts']); expect(fileNames(timeline)).toEqual(['a.test.ts', 'p1.setup.ts']);
expectFilesRunBefore(timeline, [`p1._setup.ts`], [`a.test.ts`]); expectFilesRunBefore(timeline, [`p1.setup.ts`], [`a.test.ts`]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(6); expect(passed).toBe(6);
} }
{ // Shard 2/2 { // Shard 2/2
const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '2/2' }); const { exitCode, passed, timeline, output } = await runGroups(files, { shard: '2/2' });
expect(output).toContain('Running 4 tests using 1 worker, shard 2 of 2'); expect(output).toContain('Running 4 tests using 1 worker, shard 2 of 2');
expect(fileNames(timeline)).toEqual(['b.test.ts', 'p2._setup.ts']); expect(fileNames(timeline)).toEqual(['b.test.ts', 'p2.setup.ts']);
expectFilesRunBefore(timeline, [`p2._setup.ts`], [`b.test.ts`]); expectFilesRunBefore(timeline, [`p2.setup.ts`], [`b.test.ts`]);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(4); expect(passed).toBe(4);
} }
}); });
test('--project only runs _setup from that project;', async ({ runGroups }, testInfo) => { test('--project only runs setup from that project;', async ({ runGroups }, testInfo) => {
const projectTemplates = { const projectTemplates = {
'a': { 'a': {
_setupMatch: /.*a._setup.ts/ setupMatch: /.*a.setup.ts/
}, },
'b': { 'b': {
_setupMatch: /.*b._setup.ts/ setupMatch: /.*b.setup.ts/
}, },
}; };
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates); const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles, { project: ['a', 'c'] }); const { exitCode, passed, timeline } = await runGroups(configWithFiles, { project: ['a', 'c'] });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(3); expect(passed).toBe(3);
expect(fileNames(timeline)).toEqual(['a._setup.ts', 'a.spec.ts', 'c.spec.ts']); expect(fileNames(timeline)).toEqual(['a.setup.ts', 'a.spec.ts', 'c.spec.ts']);
}); });
test('same file cannot be a _setup and a test in the same project', async ({ runGroups }, testInfo) => { test('same file cannot be a setup and a test in the same project', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*a.test.ts$/, setupMatch: /.*a.test.ts$/,
testMatch: /.*a.test.ts$/, testMatch: /.*a.test.ts$/,
}, },
] ]
@ -298,22 +298,22 @@ test('same file cannot be a _setup and a test in the same project', async ({ run
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
expect(output).toContain(`a.test.ts" matches both '_setup' and 'testMatch' filters in project "p1"`); expect(output).toContain(`a.test.ts" matches both 'setup' and 'testMatch' filters in project "p1"`);
}); });
test('same file cannot be a _setup and a test in different projects', async ({ runGroups }, testInfo) => { test('same file cannot be a setup and a test in different projects', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*a.test.ts$/, setupMatch: /.*a.test.ts$/,
testMatch: /.*noMatch.test.ts$/, testMatch: /.*noMatch.test.ts$/,
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*noMatch.test.ts$/, setupMatch: /.*noMatch.test.ts$/,
testMatch: /.*a.test.ts$/ testMatch: /.*a.test.ts$/
}, },
] ]
@ -326,41 +326,41 @@ test('same file cannot be a _setup and a test in different projects', async ({ r
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
expect(output).toContain(`a.test.ts" matches '_setup' filter in project "p1" and 'testMatch' filter in project "p2"`); expect(output).toContain(`a.test.ts" matches 'setup' filter in project "p1" and 'testMatch' filter in project "p2"`);
}); });
test('list-files should enumerate _setup files in same group', async ({ runCommand }, testInfo) => { test('list-files should enumerate setup files in same group', async ({ runCommand }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*a.._setup.ts$/, setupMatch: /.*a..setup.ts$/,
testMatch: /.*a.test.ts$/, testMatch: /.*a.test.ts$/,
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*b._setup.ts$/, setupMatch: /.*b.setup.ts$/,
testMatch: /.*b.test.ts$/ testMatch: /.*b.test.ts$/
}, },
] ]
};`, };`,
'a1._setup.ts': ` 'a1.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test1', async () => { }); setup('test1', async () => { });
`, `,
'a2._setup.ts': ` 'a2.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test1', async () => { }); setup('test1', async () => { });
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test } = pwt; const { test } = pwt;
test('test2', async () => { }); test('test2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test3', async () => { }); setup('test3', async () => { });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -372,42 +372,42 @@ test('list-files should enumerate _setup files in same group', async ({ runComma
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const json = JSON.parse(output); const json = JSON.parse(output);
expect(json.projects.map(p => p.name)).toEqual(['p1', 'p2']); expect(json.projects.map(p => p.name)).toEqual(['p1', 'p2']);
expect(json.projects[0].files.map(f => path.basename(f))).toEqual(['a.test.ts', 'a1._setup.ts', 'a2._setup.ts']); expect(json.projects[0].files.map(f => path.basename(f))).toEqual(['a.test.ts', 'a1.setup.ts', 'a2.setup.ts']);
expect(json.projects[1].files.map(f => path.basename(f))).toEqual(['b._setup.ts', 'b.test.ts']); expect(json.projects[1].files.map(f => path.basename(f))).toEqual(['b.setup.ts', 'b.test.ts']);
}); });
test('test --list should enumerate _setup tests as regular ones', async ({ runCommand }, testInfo) => { test('test --list should enumerate setup tests as regular ones', async ({ runCommand }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*a.._setup.ts$/, setupMatch: /.*a..setup.ts$/,
testMatch: /.*a.test.ts$/, testMatch: /.*a.test.ts$/,
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*b._setup.ts$/, setupMatch: /.*b.setup.ts$/,
testMatch: /.*b.test.ts$/ testMatch: /.*b.test.ts$/
}, },
] ]
};`, };`,
'a1._setup.ts': ` 'a1.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test1', async () => { }); setup('test1', async () => { });
`, `,
'a2._setup.ts': ` 'a2.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test1', async () => { }); setup('test1', async () => { });
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test } = pwt; const { test } = pwt;
test('test2', async () => { }); test('test2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('test3', async () => { }); setup('test3', async () => { });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -419,21 +419,21 @@ test('test --list should enumerate _setup tests as regular ones', async ({ runCo
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain(`Listing tests: expect(output).toContain(`Listing tests:
[p1] a.test.ts:6:7 test2 [p1] a.test.ts:6:7 test2
[p1] a1._setup.ts:5:7 test1 [p1] a1.setup.ts:5:7 test1
[p1] a2._setup.ts:5:7 test1 [p1] a2.setup.ts:5:7 test1
[p2] b._setup.ts:5:7 test3 [p2] b.setup.ts:5:7 test3
[p2] b.test.ts:6:7 test4 [p2] b.test.ts:6:7 test4
Total: 5 tests in 5 files`); Total: 5 tests in 5 files`);
}); });
test('should allow .only in _setup files', async ({ runGroups }, testInfo) => { test('should allow .only in setup files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -444,31 +444,31 @@ test('should allow .only in _setup files', async ({ runGroups }, testInfo) => {
test('test3', async () => { }); test('test3', async () => { });
test('test4', async () => { }); test('test4', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup.only('_setup1', async () => { }); setup.only('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
_setup.only('_setup3', async () => { }); setup.only('setup3', async () => { });
`, `,
}; };
const { exitCode, passed, timeline, output } = await runGroups(files); const { exitCode, passed, timeline, output } = await runGroups(files);
expect(output).toContain('Running 2 tests using 1 worker'); expect(output).toContain('Running 2 tests using 1 worker');
expect(output).toContain('[p1] a._setup.ts:5:14 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:13 setup1');
expect(output).toContain('[p1] a._setup.ts:7:14 _setup3'); expect(output).toContain('[p1] a.setup.ts:7:13 setup3');
expect(fileNames(timeline)).toEqual(['a._setup.ts']); expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(2); expect(passed).toBe(2);
}); });
test('should allow describe.only in _setup files', async ({ runGroups }, testInfo) => { test('should allow describe.only in setup files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -479,33 +479,33 @@ test('should allow describe.only in _setup files', async ({ runGroups }, testInf
test('test3', async () => { }); test('test3', async () => { });
test('test4', async () => { }); test('test4', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup.describe.only('main', () => { setup.describe.only('main', () => {
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
}); });
_setup('_setup3', async () => { }); setup('setup3', async () => { });
`, `,
}; };
const { exitCode, passed, timeline, output } = await runGroups(files); const { exitCode, passed, timeline, output } = await runGroups(files);
expect(output).toContain('Running 2 tests using 1 worker'); expect(output).toContain('Running 2 tests using 1 worker');
expect(output).toContain('[p1] a._setup.ts:6:9 main _setup1'); expect(output).toContain('[p1] a.setup.ts:6:9 main setup1');
expect(output).toContain('[p1] a._setup.ts:7:9 main _setup2'); expect(output).toContain('[p1] a.setup.ts:7:9 main setup2');
expect(fileNames(timeline)).toEqual(['a._setup.ts']); expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(2); expect(passed).toBe(2);
}); });
test('should filter describe line in _setup files', async ({ runGroups }, testInfo) => { test('should filter describe line in setup files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -516,33 +516,33 @@ test('should filter describe line in _setup files', async ({ runGroups }, testIn
test('test3', async () => { }); test('test3', async () => { });
test('test4', async () => { }); test('test4', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup.describe('main', () => { setup.describe('main', () => {
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
}); });
_setup('_setup3', async () => { }); setup('setup3', async () => { });
`, `,
}; };
const { exitCode, passed, timeline, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['a._setup.ts:5'] }); const { exitCode, passed, timeline, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['a.setup.ts:5'] });
expect(output).toContain('Running 2 tests using 1 worker'); expect(output).toContain('Running 2 tests using 1 worker');
expect(output).toContain('[p1] a._setup.ts:6:9 main _setup1'); expect(output).toContain('[p1] a.setup.ts:6:9 main setup1');
expect(output).toContain('[p1] a._setup.ts:7:9 main _setup2'); expect(output).toContain('[p1] a.setup.ts:7:9 main setup2');
expect(fileNames(timeline)).toEqual(['a._setup.ts']); expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(2); expect(passed).toBe(2);
}); });
test('should allow .only in both _setup and test files', async ({ runGroups }, testInfo) => { test('should allow .only in both setup and test files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -553,28 +553,28 @@ test('should allow .only in both _setup and test files', async ({ runGroups }, t
test('test3', async () => { }); test('test3', async () => { });
test('test4', async () => { }); test('test4', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup.only('_setup1', async () => { }); setup.only('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
_setup('_setup3', async () => { }); setup('setup3', async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('[p1] a._setup.ts:5:14 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:13 setup1');
expect(output).toContain('[p1] a.test.ts:7:12 test2'); expect(output).toContain('[p1] a.test.ts:7:12 test2');
}); });
test('should run full _setup when there is test.only', async ({ runGroups }, testInfo) => { test('should run full setup when there is test.only', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -585,14 +585,14 @@ test('should run full _setup when there is test.only', async ({ runGroups }, tes
test('test3', async () => { }); test('test3', async () => { });
test('test4', async () => { }); test('test4', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup3', async () => { }); setup('setup3', async () => { });
`, `,
}; };
@ -600,24 +600,24 @@ test('should run full _setup when there is test.only', async ({ runGroups }, tes
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(4); expect(passed).toBe(4);
expect(output).toContain('Running 4 tests using 2 workers'); expect(output).toContain('Running 4 tests using 2 workers');
expect(output).toContain('[p1] b._setup.ts:5:7 _setup3'); expect(output).toContain('[p1] b.setup.ts:5:7 setup3');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p1] a.test.ts:6:12 test1'); expect(output).toContain('[p1] a.test.ts:6:12 test1');
}); });
test('should allow filtering _setup by file:line', async ({ runGroups }, testInfo) => { test('should allow filtering setup by file:line', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*a._setup.ts/, setupMatch: /.*a.setup.ts/,
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*b._setup.ts/, setupMatch: /.*b.setup.ts/,
}, },
] ]
};`, };`,
@ -627,14 +627,14 @@ test('should allow filtering _setup by file:line', async ({ runGroups }, testInf
test('test2', async () => { }); test('test2', async () => { });
test('test3', async () => { }); test('test3', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -645,31 +645,31 @@ test('should allow filtering _setup by file:line', async ({ runGroups }, testInf
}; };
{ {
const { exitCode, passed, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*_setup.ts$'] }); const { exitCode, passed, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*setup.ts$'] });
expect(output).toContain('Running 3 tests using 2 workers'); expect(output).toContain('Running 3 tests using 2 workers');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p2] b._setup.ts:5:7 _setup1'); expect(output).toContain('[p2] b.setup.ts:5:7 setup1');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(3); expect(passed).toBe(3);
} }
{ {
const { exitCode, passed, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a._setup.ts:5'] }); const { exitCode, passed, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a.setup.ts:5'] });
expect(output).toContain('Running 1 test using 1 worker'); expect(output).toContain('Running 1 test using 1 worker');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(passed).toBe(1); expect(passed).toBe(1);
} }
}); });
test('should support filters matching both _setup and test', async ({ runGroups }, testInfo) => { test('should support filters matching both setup and test', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -679,14 +679,14 @@ test('should support filters matching both _setup and test', async ({ runGroups
test('test2', async () => { }); test('test2', async () => { });
test('test3', async () => { }); test('test3', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -694,17 +694,17 @@ test('should support filters matching both _setup and test', async ({ runGroups
`, `,
}; };
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a.(_setup|test).ts$'] }); const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a.(setup|test).ts$'] });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('Running 5 tests using 1 worker'); expect(output).toContain('Running 5 tests using 1 worker');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p1] a.test.ts:6:7 test1'); expect(output).toContain('[p1] a.test.ts:6:7 test1');
expect(output).toContain('[p1] a.test.ts:7:7 test2'); expect(output).toContain('[p1] a.test.ts:7:7 test2');
expect(output).toContain('[p1] a.test.ts:8:7 test3'); expect(output).toContain('[p1] a.test.ts:8:7 test3');
}); });
test('should run _setup for a project if tests match only in another project', async ({ runGroups }, testInfo) => { test('should run setup for a project if tests match only in another project', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
@ -712,12 +712,12 @@ test('should run _setup for a project if tests match only in another project', a
{ {
name: 'p1', name: 'p1',
testMatch: /.*a.test.ts/, testMatch: /.*a.test.ts/,
_setupMatch: /.*a._setup.ts/, setupMatch: /.*a.setup.ts/,
}, },
{ {
name: 'p2', name: 'p2',
testMatch: /.*b.test.ts/, testMatch: /.*b.test.ts/,
_setupMatch: /.*b._setup.ts/, setupMatch: /.*b.setup.ts/,
}, },
] ]
};`, };`,
@ -725,13 +725,13 @@ test('should run _setup for a project if tests match only in another project', a
const { test } = pwt; const { test } = pwt;
test('test1', async () => { }); test('test1', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -742,19 +742,19 @@ test('should run _setup for a project if tests match only in another project', a
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a.test.ts$'] }); const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['.*a.test.ts$'] });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('Running 3 tests using 2 workers'); expect(output).toContain('Running 3 tests using 2 workers');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a.test.ts:6:7 test1'); expect(output).toContain('[p1] a.test.ts:6:7 test1');
expect(output).toContain('[p2] b._setup.ts:5:7 _setup1'); expect(output).toContain('[p2] b.setup.ts:5:7 setup1');
}); });
test('should run all _setup files if only tests match filter', async ({ runGroups }, testInfo) => { test('should run all setup files if only tests match filter', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -764,34 +764,34 @@ test('should run all _setup files if only tests match filter', async ({ runGroup
test('test2', async () => { }); test('test2', async () => { });
test('test3', async () => { }); test('test3', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['a.test.ts:7'] }); const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['a.test.ts:7'] });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('Running 4 tests using 2 workers'); expect(output).toContain('Running 4 tests using 2 workers');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p1] b._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] b.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a.test.ts:7:7 test2'); expect(output).toContain('[p1] a.test.ts:7:7 test2');
}); });
test('should run all _setup files if only tests match grep filter', async ({ runGroups }, testInfo) => { test('should run all setup files if only tests match grep filter', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
@ -801,35 +801,35 @@ test('should run all _setup files if only tests match grep filter', async ({ run
test('test2', async () => { }); test('test2', async () => { });
test('test3', async () => { }); test('test3', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['--grep', '.*test2$'] }); const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['--grep', '.*test2$'] });
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('Running 4 tests using 2 workers'); expect(output).toContain('Running 4 tests using 2 workers');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p1] b._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] b.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a.test.ts:7:7 test2'); expect(output).toContain('[p1] a.test.ts:7:7 test2');
}); });
test('should apply project.grep filter to both _setup and tests', async ({ runGroups }, testInfo) => { test('should apply project.grep filter to both setup and tests', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
grep: /a.(test|_setup).ts.*(test|_setup)/, grep: /a.(test|setup).ts.*(test|setup)/,
}, },
] ]
};`, };`,
@ -839,65 +839,65 @@ test('should apply project.grep filter to both _setup and tests', async ({ runGr
test('test2', async () => { }); test('test2', async () => { });
test('foo', async () => { }); test('foo', async () => { });
`, `,
'a._setup.ts': ` 'a.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('_setup2', async () => { }); setup('setup2', async () => { });
`, `,
'b._setup.ts': ` 'b.setup.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup('_setup1', async () => { }); setup('setup1', async () => { });
_setup('foo', async () => { }); setup('foo', async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1'); expect(output).toContain('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:6:7 _setup2'); expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p1] a.test.ts:6:7 test1'); expect(output).toContain('[p1] a.test.ts:6:7 test1');
expect(output).toContain('[p1] a.test.ts:7:7 test2'); expect(output).toContain('[p1] a.test.ts:7:7 test2');
}); });
test('should prohibit _setup in test files', async ({ runGroups }, testInfo) => { test('should prohibit setup in test files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'a.test.ts': ` 'a.test.ts': `
const { _setup, test } = pwt; const { setup, test } = pwt;
_setup('test1', async () => { }); setup('test1', async () => { });
test('test2', async () => { }); test('test2', async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
expect(output).toContain('_setup() is called in a file which is not a part of project setup.'); expect(output).toContain('setup() is called in a file which is not a part of project setup.');
}); });
test('should prohibit _setup hooks in test files', async ({ runGroups }, testInfo) => { test('should prohibit setup hooks in test files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'a.test.ts': ` 'a.test.ts': `
const { _setup } = pwt; const { setup } = pwt;
_setup.beforeAll(async () => { }); setup.beforeAll(async () => { });
`, `,
}; };
const { exitCode, output } = await runGroups(files); const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
expect(output).toContain('_setup.beforeAll() is called in a file which is not a part of project setup'); expect(output).toContain('setup.beforeAll() is called in a file which is not a part of project setup');
}); });
test('should prohibit test in _setup files', async ({ runGroups }, testInfo) => { test('should prohibit test in setup files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
'a._setup.ts': ` 'a.setup.ts': `
const { test } = pwt; const { test } = pwt;
test('test1', async () => { }); test('test1', async () => { });
`, `,
@ -908,18 +908,18 @@ test('should prohibit test in _setup files', async ({ runGroups }, testInfo) =>
expect(output).toContain('test() is called in a project setup file'); expect(output).toContain('test() is called in a project setup file');
}); });
test('should prohibit test hooks in _setup files', async ({ runGroups }, testInfo) => { test('should prohibit test hooks in setup files', async ({ runGroups }, testInfo) => {
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*._setup.ts/, setupMatch: /.*.setup.ts/,
}, },
] ]
};`, };`,
'a._setup.ts': ` 'a.setup.ts': `
const { test } = pwt; const { test } = pwt;
test.beforeEach(async () => { }); test.beforeEach(async () => { });
`, `,

View file

@ -16,24 +16,24 @@
import { expect, test } from './playwright-test-fixtures'; import { expect, test } from './playwright-test-fixtures';
test('should provide _store fixture', async ({ runInlineTest }) => { test('should provide store fixture', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = {}; module.exports = {};
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should _store number', async ({ }) => { test('should store number', async ({ }) => {
expect(_store).toBeTruthy(); expect(store).toBeTruthy();
expect(await _store.get('number')).toBe(undefined); expect(await store.get('number')).toBe(undefined);
await _store.set('number', 2022) await store.set('number', 2022)
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
}); });
test('should _store object', async ({ }) => { test('should store object', async ({ }) => {
expect(_store).toBeTruthy(); expect(store).toBeTruthy();
expect(await _store.get('object')).toBe(undefined); expect(await store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 }) await store.set('object', { 'a': 2022 })
expect(await _store.get('object')).toEqual({ 'a': 2022 }); expect(await store.get('object')).toEqual({ 'a': 2022 });
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });
@ -41,42 +41,42 @@ test('should provide _store fixture', async ({ runInlineTest }) => {
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
}); });
test('should share _store state between project setup and tests', async ({ runInlineTest }) => { test('should share store state between project setup and tests', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*_store.setup.ts/ setupMatch: /.*store.setup.ts/
} }
] ]
}; };
`, `,
'_store.setup.ts': ` 'store.setup.ts': `
const { _setup, expect, _store } = pwt; const { setup, expect, store } = pwt;
_setup('should initialize _store', async ({ }) => { setup('should initialize store', async ({ }) => {
expect(await _store.get('number')).toBe(undefined); expect(await store.get('number')).toBe(undefined);
await _store.set('number', 2022) await store.set('number', 2022)
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('object')).toBe(undefined); expect(await store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 }) await store.set('object', { 'a': 2022 })
expect(await _store.get('object')).toEqual({ 'a': 2022 }); expect(await store.get('object')).toEqual({ 'a': 2022 });
}); });
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should get data from setup', async ({ }) => { test('should get data from setup', async ({ }) => {
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('object')).toEqual({ 'a': 2022 }); expect(await store.get('object')).toEqual({ 'a': 2022 });
}); });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should get data from setup', async ({ }) => { test('should get data from setup', async ({ }) => {
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('object')).toEqual({ 'a': 2022 }); expect(await store.get('object')).toEqual({ 'a': 2022 });
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });
@ -84,25 +84,25 @@ test('should share _store state between project setup and tests', async ({ runIn
expect(result.passed).toBe(3); expect(result.passed).toBe(3);
}); });
test('should persist _store state between project runs', async ({ runInlineTest }) => { test('should persist store state between project runs', async ({ runInlineTest }) => {
const files = { const files = {
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { }; module.exports = { };
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should have no data on first run', async ({ }) => { test('should have no data on first run', async ({ }) => {
expect(await _store.get('number')).toBe(undefined); expect(await store.get('number')).toBe(undefined);
await _store.set('number', 2022) await store.set('number', 2022)
expect(await _store.get('object')).toBe(undefined); expect(await store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 }) await store.set('object', { 'a': 2022 })
}); });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should get data from previous run', async ({ }) => { test('should get data from previous run', async ({ }) => {
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('object')).toEqual({ 'a': 2022 }); expect(await store.get('object')).toEqual({ 'a': 2022 });
}); });
`, `,
}; };
@ -118,46 +118,46 @@ test('should persist _store state between project runs', async ({ runInlineTest
} }
}); });
test('should isolate _store state between projects', async ({ runInlineTest }) => { test('should isolate store state between projects', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*_store.setup.ts/ setupMatch: /.*store.setup.ts/
}, },
{ {
name: 'p2', name: 'p2',
_setupMatch: /.*_store.setup.ts/ setupMatch: /.*store.setup.ts/
} }
] ]
}; };
`, `,
'_store.setup.ts': ` 'store.setup.ts': `
const { _setup, expect, _store } = pwt; const { setup, expect, store } = pwt;
_setup('should initialize _store', async ({ }) => { setup('should initialize store', async ({ }) => {
expect(await _store.get('number')).toBe(undefined); expect(await store.get('number')).toBe(undefined);
await _store.set('number', 2022) await store.set('number', 2022)
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('name')).toBe(undefined); expect(await store.get('name')).toBe(undefined);
await _store.set('name', 'str-' + _setup.info().project.name) await store.set('name', 'str-' + setup.info().project.name)
expect(await _store.get('name')).toBe('str-' + _setup.info().project.name); expect(await store.get('name')).toBe('str-' + setup.info().project.name);
}); });
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should get data from setup', async ({ }) => { test('should get data from setup', async ({ }) => {
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('name')).toBe('str-' + test.info().project.name); expect(await store.get('name')).toBe('str-' + test.info().project.name);
}); });
`, `,
'b.test.ts': ` 'b.test.ts': `
const { test, _store } = pwt; const { test, store } = pwt;
test('should get data from setup', async ({ }) => { test('should get data from setup', async ({ }) => {
expect(await _store.get('number')).toBe(2022); expect(await store.get('number')).toBe(2022);
expect(await _store.get('name')).toBe('str-' + test.info().project.name); expect(await store.get('name')).toBe('str-' + test.info().project.name);
}); });
`, `,
}, { workers: 2 }); }, { workers: 2 });
@ -165,7 +165,7 @@ test('should isolate _store state between projects', async ({ runInlineTest }) =
expect(result.passed).toBe(6); expect(result.passed).toBe(6);
}); });
test('should load context storageState from _store', async ({ runInlineTest, server }) => { test('should load context storageState from store', async ({ runInlineTest, server }) => {
server.setRoute('/setcookie.html', (req, res) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=v1']); res.setHeader('Set-Cookie', ['a=v1']);
res.end(); res.end();
@ -176,24 +176,24 @@ test('should load context storageState from _store', async ({ runInlineTest, ser
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*_store.setup.ts/ setupMatch: /.*store.setup.ts/
} }
] ]
}; };
`, `,
'_store.setup.ts': ` 'store.setup.ts': `
const { _setup, expect, _store } = pwt; const { setup, expect, store } = pwt;
_setup('should save storageState', async ({ page, context }) => { setup('should save storageState', async ({ page, context }) => {
expect(await _store.get('user')).toBe(undefined); expect(await store.get('user')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html'); await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState(); const state = await page.context().storageState();
await _store.set('user', state); await store.set('user', state);
}); });
`, `,
'a.test.ts': ` 'a.test.ts': `
const { test } = pwt; const { test } = pwt;
test.use({ test.use({
_storageStateName: 'user' storageStateName: 'user'
}) })
test('should get data from setup', async ({ page }) => { test('should get data from setup', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}'); await page.goto('${server.EMPTY_PAGE}');
@ -214,7 +214,7 @@ test('should load context storageState from _store', async ({ runInlineTest, ser
expect(result.passed).toBe(3); expect(result.passed).toBe(3);
}); });
test('should load _storageStateName specified in the project config from _store', async ({ runInlineTest, server }) => { test('should load storageStateName specified in the project config from store', async ({ runInlineTest, server }) => {
server.setRoute('/setcookie.html', (req, res) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=v1']); res.setHeader('Set-Cookie', ['a=v1']);
res.end(); res.end();
@ -225,24 +225,24 @@ test('should load _storageStateName specified in the project config from _store'
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*_store.setup.ts/, setupMatch: /.*store.setup.ts/,
use: { use: {
_storageStateName: 'stateInStorage', storageStateName: 'stateInStorage',
}, },
} }
] ]
}; };
`, `,
'_store.setup.ts': ` 'store.setup.ts': `
const { _setup, expect, _store } = pwt; const { setup, expect, store } = pwt;
_setup.use({ setup.use({
_storageStateName: ({}, use) => use(undefined), storageStateName: ({}, use) => use(undefined),
}) })
_setup('should save storageState', async ({ page, context }) => { setup('should save storageState', async ({ page, context }) => {
expect(await _store.get('stateInStorage')).toBe(undefined); expect(await store.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html'); await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState(); const state = await page.context().storageState();
await _store.set('stateInStorage', state); await store.set('stateInStorage', state);
}); });
`, `,
'a.test.ts': ` 'a.test.ts': `
@ -258,7 +258,7 @@ test('should load _storageStateName specified in the project config from _store'
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
}); });
test('should load _storageStateName specified in the global config from _store', async ({ runInlineTest, server }) => { test('should load storageStateName specified in the global config from store', async ({ runInlineTest, server }) => {
server.setRoute('/setcookie.html', (req, res) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=v1']); res.setHeader('Set-Cookie', ['a=v1']);
res.end(); res.end();
@ -267,26 +267,26 @@ test('should load _storageStateName specified in the global config from _store',
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
use: { use: {
_storageStateName: 'stateInStorage', storageStateName: 'stateInStorage',
}, },
projects: [ projects: [
{ {
name: 'p1', name: 'p1',
_setupMatch: /.*_store.setup.ts/, setupMatch: /.*store.setup.ts/,
} }
] ]
}; };
`, `,
'_store.setup.ts': ` 'store.setup.ts': `
const { _setup, expect, _store } = pwt; const { setup, expect, store } = pwt;
_setup.use({ setup.use({
_storageStateName: ({}, use) => use(undefined), storageStateName: ({}, use) => use(undefined),
}) })
_setup('should save _storageStateName', async ({ page, context }) => { setup('should save storageStateName', async ({ page, context }) => {
expect(await _store.get('stateInStorage')).toBe(undefined); expect(await store.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html'); await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState(); const state = await page.context().storageState();
await _store.set('stateInStorage', state); await store.set('stateInStorage', state);
}); });
`, `,
'a.test.ts': ` 'a.test.ts': `
@ -302,7 +302,7 @@ test('should load _storageStateName specified in the global config from _store',
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
}); });
test('should throw on unknown _storageStateName value', async ({ runInlineTest, server }) => { test('should throw on unknown storageStateName value', async ({ runInlineTest, server }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
@ -310,7 +310,7 @@ test('should throw on unknown _storageStateName value', async ({ runInlineTest,
{ {
name: 'p1', name: 'p1',
use: { use: {
_storageStateName: 'stateInStorage', storageStateName: 'stateInStorage',
}, },
} }
] ]
@ -324,5 +324,5 @@ test('should throw on unknown _storageStateName value', async ({ runInlineTest,
}, { workers: 1 }); }, { workers: 1 });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.output).toContain('Error: Cannot find value in the _store for _storageStateName: "stateInStorage"'); expect(result.output).toContain('Error: Cannot find value in the store for storageStateName: "stateInStorage"');
}); });

View file

@ -188,3 +188,18 @@ test('config should allow void/empty options', async ({ runTSC }) => {
}); });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });
test('should provide store interface', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
const { test, store } = pwt;
test('my test', async () => {
await store.set('foo', 'bar');
const val = await store.get('foo');
// @ts-expect-error
await store.unknown();
});
`
});
expect(result.exitCode).toBe(0);
});

View file

@ -196,6 +196,11 @@ type ConnectOptions = {
timeout?: number; timeout?: number;
}; };
export interface TestStore {
get<T>(name: string): Promise<T | undefined>;
set<T>(name: string, value: T | undefined): Promise<void>;
}
export interface PlaywrightWorkerOptions { export interface PlaywrightWorkerOptions {
browserName: BrowserName; browserName: BrowserName;
defaultBrowserType: BrowserName; defaultBrowserType: BrowserName;
@ -229,6 +234,7 @@ export interface PlaywrightTestOptions {
permissions: string[] | undefined; permissions: string[] | undefined;
proxy: Proxy | undefined; proxy: Proxy | undefined;
storageState: StorageState | undefined; storageState: StorageState | undefined;
storageStateName: string | undefined;
timezoneId: string | undefined; timezoneId: string | undefined;
userAgent: string | undefined; userAgent: string | undefined;
viewport: ViewportSize | null | undefined; viewport: ViewportSize | null | undefined;
@ -351,6 +357,7 @@ export default test;
export const _baseTest: TestType<{}, {}>; export const _baseTest: TestType<{}, {}>;
export const expect: Expect; export const expect: Expect;
export const store: TestStore;
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {}; export {};