chore: hide setup, store, TestProject.setupMatch, storageStateName (#19442)

This commit is contained in:
Yury Semikhatsky 2022-12-13 22:48:38 -08:00 committed by GitHub
parent 184ab5b49d
commit 92dd734e04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 480 additions and 745 deletions

View file

@ -173,59 +173,62 @@ var context = await browser.NewContextAsync(new()
* langs: js * langs: js
Playwright provides a way to reuse the signed-in state in the tests. That way you can log Playwright provides a way to reuse the signed-in state in the tests. That way you can log
in only once per project and then skip the log in step for all of the tests. in only once and then skip the log in step for all of the tests.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state. Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
You can run authentication steps once during the project [`property: TestProject.setupMatch`] phase and save the context state into [TestStore]. The stored value can later be reused to automatically restore authenticated context state in every test of the project. This way the login will run once per project before all tests. Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
Create a setup test that performs login and saves the context state into project store: Create a new global setup script:
```js tab=js-js ```js tab=js-js
// github-login.setup.js // global-setup.js
const { setup, store } = require('@playwright/test'); const { chromium } = require('@playwright/test');
setup('sign in', async ({ page, context }) => { module.exports = async config => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://github.com/login'); await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill('user'); await page.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click(); await page.getByText('Sign in').click();
// Save signed-in state to 'storageState.json'.
// Save signed-in state to an entry named 'github-test-user'. await page.context().storageState({ path: 'storageState.json' });
const contextState = await context.storageState(); await browser.close();
await store.set('github-test-user', contextState) };
});
``` ```
```js tab=js-ts ```js tab=js-ts
// github-login.setup.ts // global-setup.ts
import { setup, store } from '@playwright/setup'; import { chromium, FullConfig } from '@playwright/test';
setup('sign in', async ({ page, context }) => { async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://github.com/login'); await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill('user'); await page.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click(); await page.getByText('Sign in').click();
// Save signed-in state to 'storageState.json'.
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
}
// Save signed-in state to an entry named 'github-test-user'. export default globalSetup;
const contextState = await context.storageState();
await store.set('github-test-user', contextState)
});
``` ```
Configure project setup tests in the Playwright configuration file: Register global setup script in the Playwright configuration file:
```js tab=js-ts ```js tab=js-ts
// playwright.config.ts // playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test'; import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
projects: [ globalSetup: require.resolve('./global-setup'),
{ use: {
name: 'chromium', // Tell all tests to load signed-in state from 'storageState.json'.
// Specify files that should run before regular tests in the project. storageState: 'storageState.json'
setup: /.*.setup.ts$/, }
},
}; };
export default config; export default config;
``` ```
@ -235,25 +238,19 @@ export default config;
// @ts-check // @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
projects: [ globalSetup: require.resolve('./global-setup'),
{ use: {
name: 'chromium', // Tell all tests to load signed-in state from 'storageState.json'.
// Specify files that should run before regular tests in the project. storageState: 'storageState.json'
setup: /.*.setup.ts$/, }
},
}; };
module.exports = config; module.exports = config;
``` ```
Specify [`property: TestOptions.storageStateName`] in the test files that need to be logged in. Playwright will use the previously saved state when creating a page. Tests start already authenticated because we specify `storageState` that was populated by global setup.
```js tab=js-ts ```js tab=js-ts
import { test, expect } from '@playwright/test'; import { test } from '@playwright/test';
// Name of the storage state entry. The entry is saved in the project setup.
test.use({
storageStateName: 'github-test-user'
})
test('test', async ({ page }) => { test('test', async ({ page }) => {
// page is signed in. // page is signed in.
@ -263,46 +260,16 @@ test('test', async ({ page }) => {
```js tab=js-js ```js tab=js-js
const { test } = require('@playwright/test'); const { test } = require('@playwright/test');
// Name of the storage state entry. The entry is saved in the project setup.
test.use({
storageStateName: 'github-test-user'
})
test('test', async ({ page }) => { test('test', async ({ page }) => {
// page is signed in. // page is signed in.
}); });
``` ```
### Reusing signed in state between test runs :::note
* langs: js If you can log in once and commit the `storageState.json` into the repository, you won't need the global setup at all, just specify the `storageState.json` in Playwright Config as above and it'll be picked up.
When you set an entry on [TestStore] Playwright will store it in a separate file under `.playwright-store/`. Playwright does not delete those files automatically. You can leverage this fact to persist storage state between test runs and only sign in if the entry is not in the store yet. However, periodically, you may need to update the `storageState.json` file if your app requires you to re-authenticate after some amount of time. For example, if your app prompts you to sign in every week even if you're on the same computer/browser, you'll need to update `storageState.json` at least this often.
:::
```js tab=js-js
// github-login.setup.js
const { setup, store } = require('@playwright/test');
setup('sign in', async ({ page, context }) => {
if (storage.get('github-test-user'))
return;
// ... login here ...
await store.set('github-test-user', await context.storageState());
});
```
```js tab=js-ts
// github-login.setup.ts
import { setup, store } from '@playwright/test';
setup('sign in', async ({ page, context }) => {
if (storage.get('github-test-user'))
return;
// ... login here ...
await store.set('github-test-user', await context.storageState());
});
```
You may need to periodically update the storage state entry if your app requires you to re-authenticate after some amount of time. For example, if your app prompts you to sign in every week even if you're on the same computer/browser, you'll need to update saved storage state at least this often. You can simply delete `.playwright-store/` directory to clear the store and run the tests again so that they populate it.
### Sign in via API request ### Sign in via API request
* langs: js * langs: js
@ -310,37 +277,41 @@ You may need to periodically update the storage state entry if your app requires
If your web application supports signing in via API, you can use [APIRequestContext] to simplify sign in flow. Global setup script from the example above would change like this: If your web application supports signing in via API, you can use [APIRequestContext] to simplify sign in flow. Global setup script from the example above would change like this:
```js tab=js-js ```js tab=js-js
// github-login.setup.js // global-setup.js
const { setup, store } = require('@playwright/test'); const { request } = require('@playwright/test');
setup('sign in', async ({ request }) => { module.exports = async () => {
await request.post('https://github.com/login', { const requestContext = await request.newContext();
await requestContext.post('https://github.com/login', {
form: { form: {
'user': 'user', 'user': 'user',
'password': 'password' 'password': 'password'
} }
}); });
// Save signed-in state to an entry named 'github-test-user'. // Save signed-in state to 'storageState.json'.
const contextState = await request.storageState(); await requestContext.storageState({ path: 'storageState.json' });
await store.set('github-test-user', contextState) await requestContext.dispose();
}); }
``` ```
```js tab=js-ts ```js tab=js-ts
// github-login.setup.ts // global-setup.ts
import { setup, store } from '@playwright/test'; import { request } from '@playwright/test';
setup('sign in', async ({ request }) => { async function globalSetup() {
await request.post('https://github.com/login', { const requestContext = await request.newContext();
await requestContext.post('https://github.com/login', {
form: { form: {
'user': 'user', 'user': 'user',
'password': 'password' 'password': 'password'
} }
}); });
// Save signed-in state to an entry named 'github-test-user'. // Save signed-in state to 'storageState.json'.
const contextState = await request.storageState(); await requestContext.storageState({ path: 'storageState.json' });
await store.set('github-test-user', contextState) await requestContext.dispose();
}); }
export default globalSetup;
``` ```
### Avoiding multiple sessions per account at a time ### Avoiding multiple sessions per account at a time
@ -351,8 +322,8 @@ By default, Playwright Test runs tests in parallel. If you reuse a single signed
In this example we [override `storageState` fixture](./test-fixtures.md#overriding-fixtures) and ensure we only sign in once per worker, using [`property: TestInfo.workerIndex`] to differentiate between workers. In this example we [override `storageState` fixture](./test-fixtures.md#overriding-fixtures) and ensure we only sign in once per worker, using [`property: TestInfo.workerIndex`] to differentiate between workers.
```js tab=js-js ```js tab=js-js
// login-fixture.js // fixtures.js
const { test: base, store } = require('@playwright/test'); const { test: base } = require('@playwright/test');
const users = [ const users = [
{ username: 'user-1', password: 'password-1' }, { username: 'user-1', password: 'password-1' },
@ -361,36 +332,26 @@ const users = [
]; ];
exports.test = base.extend({ exports.test = base.extend({
// Sign in corresponding user during test worker startup and save the state storageState: async ({ browser }, use, testInfo) => {
// to store. // Override storage state, use worker index to look up logged-in info and generate it lazily.
login: [async ({ browser }, use) => { const fileName = path.join(testInfo.project.outputDir, 'storage-' + testInfo.workerIndex);
const page = await browser.newPage(); if (!fs.existsSync(fileName)) {
await page.goto('https://github.com/login'); // Make sure we are not using any other storage state.
// Use a unique credentials for each worker. const page = await browser.newPage({ storageState: undefined });
const index = test.info().parallelIndex; await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill(users[index].username); await page.getByLabel('User Name').fill(users[testInfo.workerIndex].username);
await page.getByLabel('Password').fill(users[index].password); await page.getByLabel('Password').fill(users[testInfo.workerIndex].password);
await page.getByText('Sign in').click(); await page.getByText('Sign in').click();
await page.context().storageState({ path: fileName });
const contextState = await page.context().storageState(); await page.close();
// Save with a worker-specific key. }
await store.set(`test-user-${index}`, contextState); await use(fileName);
},
await page.close();
await use();
}, { auto: true }],
}); });
exports.expect = base.expect; exports.expect = base.expect;
exporta.store = base.store;
// example.spec.js // example.spec.js
const { test, expect } = require('./login-fixture'); const { test, expect } = require('./fixtures');
test.use({
// User worker specific user.
storageStateName: ({}, use) => use(`test-user-${test.info().parallelIndex}`)
});
test('test', async ({ page }) => { test('test', async ({ page }) => {
// page is signed in. // page is signed in.
@ -398,8 +359,9 @@ test('test', async ({ page }) => {
``` ```
```js tab=js-ts ```js tab=js-ts
// login-fixture.ts // fixtures.ts
import { test as base, store } from '@playwright/test'; import { test as baseTest } from '@playwright/test';
export { expect } from '@playwright/test';
const users = [ const users = [
{ username: 'user-1', password: 'password-1' }, { username: 'user-1', password: 'password-1' },
@ -407,35 +369,27 @@ const users = [
// ... put your test users here ... // ... put your test users here ...
]; ];
export const test = base.extend<{ login: void }>({ export const test = baseTest.extend({
// Sign in corresponding user during test worker startup and save the state storageState: async ({ browser }, use, testInfo) => {
// to the store. // Override storage state, use worker index to look up logged-in info and generate it lazily.
login: [async ({ browser }, use) => { const fileName = path.join(testInfo.project.outputDir, 'storage-' + testInfo.workerIndex);
const page = await browser.newPage(); if (!fs.existsSync(fileName)) {
await page.goto('https://github.com/login'); // Make sure we are not using any other storage state.
// Use a unique credentials for each worker. const page = await browser.newPage({ storageState: undefined });
const index = test.info().parallelIndex; await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill(users[index].username); // Create a unique username for each worker.
await page.getByLabel('Password').fill(users[index].password); await page.getByLabel('User Name').fill(users[testInfo.workerIndex].username);
await page.getByText('Sign in').click(); await page.getByLabel('Password').fill(users[testInfo.workerIndex].password);
await page.getByText('Sign in').click();
const contextState = await page.context().storageState(); await page.context().storageState({ path: fileName });
// Save with a worker-specific key. await page.close();
await store.set(`test-user-${index}`, contextState); }
await use(fileName);
await page.close(); },
await use();
}, { auto: true }],
}); });
export { expect } from '@playwright/test';
// example.spec.ts // example.spec.ts
import { test } from './login-fixture'; import { test, expect } from './fixtures';
test.use({
// User worker specific user.
storageStateName: ({}, use) => use(`test-user-${test.info().parallelIndex}`)
});
test('test', async ({ page }) => { test('test', async ({ page }) => {
// page is signed in. // page is signed in.
@ -445,62 +399,42 @@ test('test', async ({ page }) => {
## Multiple signed in roles ## Multiple signed in roles
* langs: js * langs: js
Sometimes you have more than one signed-in user in your end to end tests. You can achieve that via logging in for these users multiple times in project setup and saving that state into separate entries. Sometimes you have more than one signed-in user in your end to end tests. You can achieve that via logging in for these users multiple times in globalSetup and saving that state into different files.
```js tab=js-js ```js tab=js-js
// login.setup.js // global-setup.js
const { setup, store } = require('@playwright/test'); const { chromium } = require('@playwright/test');
// Run all logins in parallel. module.exports = async config => {
setup.describe.configure({ const browser = await chromium.launch();
mode: 'parallel' const adminPage = await browser.newPage();
}); // ... log in
await adminPage.context().storageState({ path: 'adminStorageState.json' });
setup(`login as regular user`, async ({ page }) => { const userPage = await browser.newPage();
await page.goto('https://github.com/login'); // ... log in
//... await userPage.context().storageState({ path: 'userStorageState.json' });
await browser.close();
const contextState = await page.context().storageState(); };
// Save the user state.
await store.set(`user`, contextState);
});
setup(`login as admin`, async ({ page }) => {
await page.goto('https://github.com/login');
//...
const contextState = await page.context().storageState();
// Save the admin state.
await store.set(`admin`, contextState);
});
``` ```
```js tab=js-ts ```js tab=js-ts
// login.setup.ts // global-setup.ts
import { setup, store } from '@playwright/test'; import { chromium, FullConfig } from '@playwright/test';
// Run all logins in parallel. async function globalSetup(config: FullConfig) {
setup.describe.configure({ const browser = await chromium.launch();
mode: 'parallel' const adminPage = await browser.newPage();
}); // ... log in
await adminPage.context().storageState({ path: 'adminStorageState.json' });
setup(`login as regular user`, async ({ page }) => { const userPage = await browser.newPage();
await page.goto('https://github.com/login'); // ... log in
//... await userPage.context().storageState({ path: 'userStorageState.json' });
await browser.close();
}
const contextState = await page.context().storageState(); export default globalSetup;
// Save the user state.
await store.set(`user`, contextState);
});
setup(`login as admin`, async ({ page }) => {
await page.goto('https://github.com/login');
//...
const contextState = await page.context().storageState();
// Save the admin state.
await store.set(`admin`, contextState);
});
``` ```
After that you can specify the user to use for each test file or each test group: After that you can specify the user to use for each test file or each test group:
@ -508,14 +442,14 @@ After that you can specify the user to use for each test file or each test group
```js tab=js-ts ```js tab=js-ts
import { test } from '@playwright/test'; import { test } from '@playwright/test';
test.use({ storageStateName: 'admin' }); test.use({ storageState: 'adminStorageState.json' });
test('admin test', async ({ page }) => { test('admin test', async ({ page }) => {
// page is signed in as admin. // page is signed in as admin.
}); });
test.describe(() => { test.describe(() => {
test.use({ storageStateName: 'user' }); test.use({ storageState: 'userStorageState.json' });
test('user test', async ({ page }) => { test('user test', async ({ page }) => {
// page is signed in as a user. // page is signed in as a user.
@ -526,14 +460,14 @@ test.describe(() => {
```js tab=js-js ```js tab=js-js
const { test } = require('@playwright/test'); const { test } = require('@playwright/test');
test.use({ storageStateName: 'admin' }); test.use({ storageState: 'adminStorageState.json' });
test('admin test', async ({ page }) => { test('admin test', async ({ page }) => {
// page is signed in as amin. // page is signed in as amin.
}); });
test.describe(() => { test.describe(() => {
test.use({ storageStateName: 'user' }); test.use({ storageState: 'userStorageState.json' });
test('user test', async ({ page }) => { test('user test', async ({ page }) => {
// page is signed in as a user. // page is signed in as a user.
@ -544,18 +478,18 @@ test.describe(() => {
### Testing multiple roles together ### Testing multiple roles together
* langs: js * langs: js
If you need to test how multiple authenticated roles interact together, use multiple [BrowserContext]s and [Page]s with different storage states in the same test. Any of the methods above to create multiple storage state entries would work. If you need to test how multiple authenticated roles interact together, use multiple [BrowserContext]s and [Page]s with different storage states in the same test. Any of the methods above to create multiple storage state files would work.
```js tab=js-ts ```js tab=js-ts
import { test, store } from '@playwright/test'; import { test } from '@playwright/test';
test('admin and user', async ({ browser }) => { test('admin and user', async ({ browser }) => {
// adminContext and all pages inside, including adminPage, are signed in as "admin". // adminContext and all pages inside, including adminPage, are signed in as "admin".
const adminContext = await browser.newContext({ storageState: await store.get('admin') }); const adminContext = await browser.newContext({ storageState: 'adminStorageState.json' });
const adminPage = await adminContext.newPage(); const adminPage = await adminContext.newPage();
// userContext and all pages inside, including userPage, are signed in as "user". // userContext and all pages inside, including userPage, are signed in as "user".
const userContext = await browser.newContext({ storageState: await store.get('user') }); const userContext = await browser.newContext({ storageState: 'userStorageState.json' });
const userPage = await userContext.newPage(); const userPage = await userContext.newPage();
// ... interact with both adminPage and userPage ... // ... interact with both adminPage and userPage ...
@ -563,15 +497,15 @@ test('admin and user', async ({ browser }) => {
``` ```
```js tab=js-js ```js tab=js-js
const { test, store } = require('@playwright/test'); const { test } = require('@playwright/test');
test('admin and user', async ({ browser }) => { test('admin and user', async ({ browser }) => {
// adminContext and all pages inside, including adminPage, are signed in as "admin". // adminContext and all pages inside, including adminPage, are signed in as "admin".
const adminContext = await browser.newContext({ storageState: await store.get('admin') }); const adminContext = await browser.newContext({ storageState: 'adminStorageState.json' });
const adminPage = await adminContext.newPage(); const adminPage = await adminContext.newPage();
// userContext and all pages inside, including userPage, are signed in as "user". // userContext and all pages inside, including userPage, are signed in as "user".
const userContext = await browser.newContext({ storageState: await store.get('user') }); const userContext = await browser.newContext({ storageState: 'userStorageState.json' });
const userPage = await userContext.newPage(); const userPage = await userContext.newPage();
// ... interact with both adminPage and userPage ... // ... interact with both adminPage and userPage ...
@ -581,14 +515,14 @@ test('admin and user', async ({ browser }) => {
### Testing multiple roles with POM fixtures ### Testing multiple roles with POM fixtures
* langs: js * langs: js
If many of your tests require multiple authenticated roles from within the same test, you can introduce fixtures for each role. Any of the methods above to create multiple storage state entries would work. If many of your tests require multiple authenticated roles from within the same test, you can introduce fixtures for each role. Any of the methods above to create multiple storage state files would work.
Below is an example that [creates fixtures](./test-fixtures.md#creating-a-fixture) for two [Page Object Models](./pom.md) - admin POM and user POM. It assumes `adminStorageState.json` and `userStorageState.json` files were created. Below is an example that [creates fixtures](./test-fixtures.md#creating-a-fixture) for two [Page Object Models](./pom.md) - admin POM and user POM. It assumes `adminStorageState.json` and `userStorageState.json` files were created.
```js tab=js-ts ```js tab=js-ts
// fixtures.ts // fixtures.ts
import { test as base, Page, Browser, Locator } from '@playwright/test'; import { test as base, Page, Browser, Locator } from '@playwright/test';
export { expect, store } from '@playwright/test'; export { expect } from '@playwright/test';
// Page Object Model for the "admin" page. // Page Object Model for the "admin" page.
// Here you can add locators and helper methods specific to the admin page. // Here you can add locators and helper methods specific to the admin page.
@ -601,7 +535,7 @@ class AdminPage {
} }
static async create(browser: Browser) { static async create(browser: Browser) {
const context = await browser.newContext({ storageState: await store.get('admin') }); const context = await browser.newContext({ storageState: 'adminStorageState.json' });
const page = await context.newPage(); const page = await context.newPage();
return new AdminPage(page); return new AdminPage(page);
} }
@ -622,7 +556,7 @@ class UserPage {
} }
static async create(browser: Browser) { static async create(browser: Browser) {
const context = await browser.newContext({ storageState: await store.get('user') }); const context = await browser.newContext({ storageState: 'userStorageState.json' });
const page = await context.newPage(); const page = await context.newPage();
return new UserPage(page); return new UserPage(page);
} }
@ -645,6 +579,7 @@ export const test = base.extend<MyFixtures>({
}, },
}); });
// example.spec.ts // example.spec.ts
// Import test with our new fixtures. // Import test with our new fixtures.
import { test, expect } from './fixtures'; import { test, expect } from './fixtures';
@ -659,7 +594,7 @@ test('admin and user', async ({ adminPage, userPage }) => {
```js tab=js-js ```js tab=js-js
// fixtures.js // fixtures.js
const { test: base, store } = require('@playwright/test'); const { test: base } = require('@playwright/test');
// Page Object Model for the "admin" page. // Page Object Model for the "admin" page.
// Here you can add locators and helper methods specific to the admin page. // Here you can add locators and helper methods specific to the admin page.
@ -670,7 +605,7 @@ class AdminPage {
} }
static async create(browser) { static async create(browser) {
const context = await browser.newContext({ storageState: await store.get('admin') }); const context = await browser.newContext({ storageState: 'adminStorageState.json' });
const page = await context.newPage(); const page = await context.newPage();
return new AdminPage(page); return new AdminPage(page);
} }
@ -687,7 +622,7 @@ class UserPage {
} }
static async create(browser) { static async create(browser) {
const context = await browser.newContext({ storageState: await store.get('user') }); const context = await browser.newContext({ storageState: 'userStorageState.json' });
const page = await context.newPage(); const page = await context.newPage();
return new UserPage(page); return new UserPage(page);
} }

View file

@ -205,14 +205,6 @@ 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

@ -148,7 +148,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. 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. 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.
`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).
@ -164,18 +164,6 @@ 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

@ -1,56 +0,0 @@
# 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,6 +47,7 @@ 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;
@ -143,7 +144,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 }],
@ -173,7 +174,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions, permissions,
proxy, proxy,
storageState, storageState,
storageStateName, _storageStateName,
viewport, viewport,
timezoneId, timezoneId,
userAgent, userAgent,
@ -212,10 +213,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;
@ -622,7 +623,7 @@ export function shouldCaptureTrace(traceMode: TraceMode, testInfo: TestInfo) {
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.setupMatch, []); const _setupMatch = takeFirst((projectConfig as any)._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);
@ -615,7 +615,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', 'setupMatch'] as const) { for (const prop of ['testIgnore', 'testMatch'] 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

@ -255,13 +255,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,11 +16,10 @@
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 implements TestStore { class JsonStore {
private _toFilePath(name: string) { private _toFilePath(name: string) {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)

View file

@ -82,14 +82,14 @@ export class TestTypeImpl {
if (this._projectSetup !== suite._isProjectSetup) { if (this._projectSetup !== suite._isProjectSetup) {
if (this._projectSetup) if (this._projectSetup)
throw errorWithLocation(location, `${title} is called in a file which is not a part of project setup.`); throw errorWithLocation(location, `${title} is called in a file which is not a part of project setup.`);
throw errorWithLocation(location, `${title} is called in a project setup file (use 'setup' instead of 'test').`); throw errorWithLocation(location, `${title} is called in a project setup file (use '_setup' instead of 'test').`);
} }
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._ensureCurrentSuite(location, this._projectSetup ? 'setup()' : 'test()'); const suite = this._ensureCurrentSuite(location, this._projectSetup ? '_setup()' : 'test()');
const test = new TestCase(title, fn, this, location); const test = new TestCase(title, fn, this, location);
test._requireFile = suite._requireFile; test._requireFile = suite._requireFile;
test._isProjectSetup = suite._isProjectSetup; test._isProjectSetup = suite._isProjectSetup;
@ -144,13 +144,13 @@ 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._ensureCurrentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.${name}()`); const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.${name}()`);
suite._hooks.push({ type: name, fn, location }); suite._hooks.push({ type: name, fn, location });
} }
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._ensureCurrentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.describe.configure()`); const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.describe.configure()`);
if (options.timeout !== undefined) if (options.timeout !== undefined)
suite._timeout = options.timeout; suite._timeout = options.timeout;
@ -211,14 +211,14 @@ export class TestTypeImpl {
} }
private _use(location: Location, fixtures: Fixtures) { private _use(location: Location, fixtures: Fixtures) {
const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? 'setup' : 'test'}.use()`); const suite = this._ensureCurrentSuite(location, `${this._projectSetup ? '_setup' : 'test'}.use()`);
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });
} }
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)
throw errorWithLocation(location, `${this._projectSetup ? 'setup' : 'test'}.step() can only be called from a test`); throw errorWithLocation(location, `${this._projectSetup ? '_setup' : 'test'}.step() can only be called from a test`);
const step = testInfo._addStep({ const step = testInfo._addStep({
category: 'test.step', category: 'test.step',
title, title,

View file

@ -193,10 +193,7 @@ 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. This filter and its command line * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
* 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).
*/ */
@ -2841,35 +2838,6 @@ 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.
* *
@ -3106,16 +3074,6 @@ 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)
@ -3393,7 +3351,6 @@ 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 {};
@ -4730,10 +4687,7 @@ 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. This filter and its command line * and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
* 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).
*/ */
@ -4749,24 +4703,6 @@ 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,41 +480,3 @@ 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 }): Record<string, string> { function createConfigWithProjects(names: string[], testInfo: TestInfo, projectTemplates?: { [name: string]: PlaywrightTestProject & any }): 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 ({ runG
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 ({ ru
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 ({ runComman
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 ({ runCom
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:13 setup1'); expect(output).toContain('[p1] a._setup.ts:5:14 _setup1');
expect(output).toContain('[p1] a.setup.ts:7:13 setup3'); expect(output).toContain('[p1] a._setup.ts:7:14 _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 }, 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.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 }, 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('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 }, te
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:13 setup1'); expect(output).toContain('[p1] a._setup.ts:5:14 _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 }, test
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 }, test
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 }, testInfo
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 }, testInfo
}; };
{ {
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', as
{ {
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', as
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', as
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 ({ 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 () => { });
`, `,
}; };
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 ({ runG
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 ({ runGro
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 ({ runInl
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, serv
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, serv
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', a
'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', a
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, s
{ {
name: 'p1', name: 'p1',
use: { use: {
storageStateName: 'stateInStorage', _storageStateName: 'stateInStorage',
}, },
} }
] ]
@ -324,5 +324,5 @@ test('should throw on unknown storageStateName value', async ({ runInlineTest, s
}, { 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,18 +188,3 @@ 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,11 +196,6 @@ 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;
@ -234,7 +229,6 @@ 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,7 +345,6 @@ 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 {};