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
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.
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
// github-login.setup.js
const { setup, store } = require('@playwright/test');
// global-setup.js
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.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click();
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await context.storageState();
await store.set('github-test-user', contextState)
});
// Save signed-in state to 'storageState.json'.
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
};
```
```js tab=js-ts
// github-login.setup.ts
import { setup, store } from '@playwright/setup';
// global-setup.ts
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.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password');
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'.
const contextState = await context.storageState();
await store.set('github-test-user', contextState)
});
export default globalSetup;
```
Configure project setup tests in the Playwright configuration file:
Register global setup script in the Playwright configuration file:
```js tab=js-ts
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
projects: [
{
name: 'chromium',
// Specify files that should run before regular tests in the project.
setup: /.*.setup.ts$/,
},
globalSetup: require.resolve('./global-setup'),
use: {
// Tell all tests to load signed-in state from 'storageState.json'.
storageState: 'storageState.json'
}
};
export default config;
```
@ -235,25 +238,19 @@ export default config;
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
projects: [
{
name: 'chromium',
// Specify files that should run before regular tests in the project.
setup: /.*.setup.ts$/,
},
globalSetup: require.resolve('./global-setup'),
use: {
// Tell all tests to load signed-in state from 'storageState.json'.
storageState: 'storageState.json'
}
};
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
import { test, expect } from '@playwright/test';
// Name of the storage state entry. The entry is saved in the project setup.
test.use({
storageStateName: 'github-test-user'
})
import { test } from '@playwright/test';
test('test', async ({ page }) => {
// page is signed in.
@ -263,46 +260,16 @@ test('test', async ({ page }) => {
```js tab=js-js
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 }) => {
// page is signed in.
});
```
### Reusing signed in state between test runs
* langs: js
:::note
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.
```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.
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.
:::
### Sign in via API request
* 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:
```js tab=js-js
// github-login.setup.js
const { setup, store } = require('@playwright/test');
// global-setup.js
const { request } = require('@playwright/test');
setup('sign in', async ({ request }) => {
await request.post('https://github.com/login', {
module.exports = async () => {
const requestContext = await request.newContext();
await requestContext.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await request.storageState();
await store.set('github-test-user', contextState)
});
// Save signed-in state to 'storageState.json'.
await requestContext.storageState({ path: 'storageState.json' });
await requestContext.dispose();
}
```
```js tab=js-ts
// github-login.setup.ts
import { setup, store } from '@playwright/test';
// global-setup.ts
import { request } from '@playwright/test';
setup('sign in', async ({ request }) => {
await request.post('https://github.com/login', {
async function globalSetup() {
const requestContext = await request.newContext();
await requestContext.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
// Save signed-in state to an entry named 'github-test-user'.
const contextState = await request.storageState();
await store.set('github-test-user', contextState)
});
// Save signed-in state to 'storageState.json'.
await requestContext.storageState({ path: 'storageState.json' });
await requestContext.dispose();
}
export default globalSetup;
```
### 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.
```js tab=js-js
// login-fixture.js
const { test: base, store } = require('@playwright/test');
// fixtures.js
const { test: base } = require('@playwright/test');
const users = [
{ username: 'user-1', password: 'password-1' },
@ -361,36 +332,26 @@ const users = [
];
exports.test = base.extend({
// Sign in corresponding user during test worker startup and save the state
// to store.
login: [async ({ browser }, use) => {
const page = await browser.newPage();
await page.goto('https://github.com/login');
// Use a unique credentials for each worker.
const index = test.info().parallelIndex;
await page.getByLabel('User Name').fill(users[index].username);
await page.getByLabel('Password').fill(users[index].password);
await page.getByText('Sign in').click();
const contextState = await page.context().storageState();
// Save with a worker-specific key.
await store.set(`test-user-${index}`, contextState);
await page.close();
await use();
}, { auto: true }],
storageState: async ({ browser }, use, testInfo) => {
// Override storage state, use worker index to look up logged-in info and generate it lazily.
const fileName = path.join(testInfo.project.outputDir, 'storage-' + testInfo.workerIndex);
if (!fs.existsSync(fileName)) {
// Make sure we are not using any other storage state.
const page = await browser.newPage({ storageState: undefined });
await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill(users[testInfo.workerIndex].username);
await page.getByLabel('Password').fill(users[testInfo.workerIndex].password);
await page.getByText('Sign in').click();
await page.context().storageState({ path: fileName });
await page.close();
}
await use(fileName);
},
});
exports.expect = base.expect;
exporta.store = base.store;
// example.spec.js
const { test, expect } = require('./login-fixture');
test.use({
// User worker specific user.
storageStateName: ({}, use) => use(`test-user-${test.info().parallelIndex}`)
});
const { test, expect } = require('./fixtures');
test('test', async ({ page }) => {
// page is signed in.
@ -398,8 +359,9 @@ test('test', async ({ page }) => {
```
```js tab=js-ts
// login-fixture.ts
import { test as base, store } from '@playwright/test';
// fixtures.ts
import { test as baseTest } from '@playwright/test';
export { expect } from '@playwright/test';
const users = [
{ username: 'user-1', password: 'password-1' },
@ -407,35 +369,27 @@ const users = [
// ... put your test users here ...
];
export const test = base.extend<{ login: void }>({
// Sign in corresponding user during test worker startup and save the state
// to the store.
login: [async ({ browser }, use) => {
const page = await browser.newPage();
await page.goto('https://github.com/login');
// Use a unique credentials for each worker.
const index = test.info().parallelIndex;
await page.getByLabel('User Name').fill(users[index].username);
await page.getByLabel('Password').fill(users[index].password);
await page.getByText('Sign in').click();
const contextState = await page.context().storageState();
// Save with a worker-specific key.
await store.set(`test-user-${index}`, contextState);
await page.close();
await use();
}, { auto: true }],
export const test = baseTest.extend({
storageState: async ({ browser }, use, testInfo) => {
// Override storage state, use worker index to look up logged-in info and generate it lazily.
const fileName = path.join(testInfo.project.outputDir, 'storage-' + testInfo.workerIndex);
if (!fs.existsSync(fileName)) {
// Make sure we are not using any other storage state.
const page = await browser.newPage({ storageState: undefined });
await page.goto('https://github.com/login');
// Create a unique username for each worker.
await page.getByLabel('User Name').fill(users[testInfo.workerIndex].username);
await page.getByLabel('Password').fill(users[testInfo.workerIndex].password);
await page.getByText('Sign in').click();
await page.context().storageState({ path: fileName });
await page.close();
}
await use(fileName);
},
});
export { expect } from '@playwright/test';
// example.spec.ts
import { test } from './login-fixture';
test.use({
// User worker specific user.
storageStateName: ({}, use) => use(`test-user-${test.info().parallelIndex}`)
});
import { test, expect } from './fixtures';
test('test', async ({ page }) => {
// page is signed in.
@ -445,62 +399,42 @@ test('test', async ({ page }) => {
## Multiple signed in roles
* 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
// login.setup.js
const { setup, store } = require('@playwright/test');
// global-setup.js
const { chromium } = require('@playwright/test');
// Run all logins in parallel.
setup.describe.configure({
mode: 'parallel'
});
module.exports = async config => {
const browser = await chromium.launch();
const adminPage = await browser.newPage();
// ... log in
await adminPage.context().storageState({ path: 'adminStorageState.json' });
setup(`login as regular user`, async ({ page }) => {
await page.goto('https://github.com/login');
//...
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);
});
const userPage = await browser.newPage();
// ... log in
await userPage.context().storageState({ path: 'userStorageState.json' });
await browser.close();
};
```
```js tab=js-ts
// login.setup.ts
import { setup, store } from '@playwright/test';
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
// Run all logins in parallel.
setup.describe.configure({
mode: 'parallel'
});
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const adminPage = await browser.newPage();
// ... log in
await adminPage.context().storageState({ path: 'adminStorageState.json' });
setup(`login as regular user`, async ({ page }) => {
await page.goto('https://github.com/login');
//...
const userPage = await browser.newPage();
// ... 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);
});
export default globalSetup;
```
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
import { test } from '@playwright/test';
test.use({ storageStateName: 'admin' });
test.use({ storageState: 'adminStorageState.json' });
test('admin test', async ({ page }) => {
// page is signed in as admin.
});
test.describe(() => {
test.use({ storageStateName: 'user' });
test.use({ storageState: 'userStorageState.json' });
test('user test', async ({ page }) => {
// page is signed in as a user.
@ -526,14 +460,14 @@ test.describe(() => {
```js tab=js-js
const { test } = require('@playwright/test');
test.use({ storageStateName: 'admin' });
test.use({ storageState: 'adminStorageState.json' });
test('admin test', async ({ page }) => {
// page is signed in as amin.
});
test.describe(() => {
test.use({ storageStateName: 'user' });
test.use({ storageState: 'userStorageState.json' });
test('user test', async ({ page }) => {
// page is signed in as a user.
@ -544,18 +478,18 @@ test.describe(() => {
### Testing multiple roles together
* 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
import { test, store } from '@playwright/test';
import { test } from '@playwright/test';
test('admin and user', async ({ browser }) => {
// 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();
// 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();
// ... interact with both adminPage and userPage ...
@ -563,15 +497,15 @@ test('admin and user', async ({ browser }) => {
```
```js tab=js-js
const { test, store } = require('@playwright/test');
const { test } = require('@playwright/test');
test('admin and user', async ({ browser }) => {
// 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();
// 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();
// ... interact with both adminPage and userPage ...
@ -581,14 +515,14 @@ test('admin and user', async ({ browser }) => {
### Testing multiple roles with POM fixtures
* 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.
```js tab=js-ts
// fixtures.ts
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.
// Here you can add locators and helper methods specific to the admin page.
@ -601,7 +535,7 @@ class AdminPage {
}
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();
return new AdminPage(page);
}
@ -622,7 +556,7 @@ class UserPage {
}
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();
return new UserPage(page);
}
@ -645,6 +579,7 @@ export const test = base.extend<MyFixtures>({
},
});
// example.spec.ts
// Import test with our new fixtures.
import { test, expect } from './fixtures';
@ -659,7 +594,7 @@ test('admin and user', async ({ adminPage, userPage }) => {
```js tab=js-js
// fixtures.js
const { test: base, store } = require('@playwright/test');
const { test: base } = require('@playwright/test');
// Page Object Model for 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) {
const context = await browser.newContext({ storageState: await store.get('admin') });
const context = await browser.newContext({ storageState: 'adminStorageState.json' });
const page = await context.newPage();
return new AdminPage(page);
}
@ -687,7 +622,7 @@ class UserPage {
}
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();
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-%%
* 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
* 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
- 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).
@ -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.
## 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
* since: v1.10
- 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 { addRunnerPlugin as _addRunnerPlugin } from './plugins';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
export const store = _baseStore;
export const _store = _baseStore;
if ((process as any)['__pw_initiator__']) {
const originalStackTraceLimit = Error.stackTraceLimit;
@ -47,6 +47,7 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_reuseContext: boolean,
_setupContextOptionsAndArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
_storageStateName: string | undefined;
};
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
_browserOptions: LaunchOptions;
@ -143,7 +144,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { 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 }],
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { 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,
proxy,
storageState,
storageStateName,
_storageStateName,
viewport,
timezoneId,
userAgent,
@ -212,10 +213,10 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
options.permissions = permissions;
if (proxy !== undefined)
options.proxy = proxy;
if (storageStateName !== undefined) {
const value = await store.get(storageStateName);
if (_storageStateName !== undefined) {
const value = await _store.get(_storageStateName);
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;
} else if (storageState !== undefined) {
options.storageState = storageState;
@ -622,7 +623,7 @@ export function shouldCaptureTrace(traceMode: TraceMode, testInfo: TestInfo) {
const kTracingStarted = Symbol('kTracingStarted');
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export const setup = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
_setProjectSetup(setup, true);
export const _setup = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
_setProjectSetup(_setup, true);
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 snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
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 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`);
}
for (const prop of ['testIgnore', 'testMatch', 'setupMatch'] as const) {
for (const prop of ['testIgnore', 'testMatch'] as const) {
if (prop in project && project[prop] !== undefined) {
const value = project[prop];
if (Array.isArray(value)) {

View file

@ -255,13 +255,13 @@ export class Runner {
if (!isTest && !isSetup)
return false;
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 (isSetup) {
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)) {
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);

View file

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

View file

@ -82,14 +82,14 @@ export class TestTypeImpl {
if (this._projectSetup !== suite._isProjectSetup) {
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 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;
}
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme', location: Location, title: string, fn: Function) {
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);
test._requireFile = suite._requireFile;
test._isProjectSetup = suite._isProjectSetup;
@ -144,13 +144,13 @@ export class TestTypeImpl {
}
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 });
}
private _configure(location: Location, options: { mode?: 'parallel' | 'serial', retries?: number, timeout?: number }) {
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)
suite._timeout = options.timeout;
@ -211,14 +211,14 @@ export class TestTypeImpl {
}
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 });
}
private async _step<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> {
const testInfo = currentTestInfo();
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({
category: 'test.step',
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
* [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
* 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.
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
*
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/
@ -2841,35 +2838,6 @@ type ConnectOptions = {
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.
*
@ -3106,16 +3074,6 @@ export interface PlaywrightTestOptions {
* Either a path to the file with saved storage, or an object with the following fields:
*/
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
* [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 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
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
* [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
* 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.
* and in the [command line](https://playwright.dev/docs/test-cli) with the `--grep-invert` option.
*
* `grepInvert` option is also useful for [tagging tests](https://playwright.dev/docs/test-annotations#tag-tests).
*/
@ -4749,24 +4703,6 @@ interface TestProject {
*/
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
* [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);
});
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 { 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 = {
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 () => {
await new Promise(f => setTimeout(f, 100));
});`;
files[`${name}/${name}.setup.ts`] = `
const { setup } = pwt;
setup('${name} setup', async () => {
files[`${name}/${name}._setup.ts`] = `
const { _setup } = pwt;
_setup('${name} _setup', async () => {
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) => {
const projectTemplates = {
'a': {
setupMatch: ['**/*.setup.ts']
_setupMatch: ['**/*._setup.ts']
},
};
const configWithFiles = createConfigWithProjects(['a'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles);
expect(exitCode).toBe(0);
expect(passed).toBe(2);
expect(formatTimeline(timeline)).toEqual(`a > a${path.sep}a.setup.ts > a setup [begin]
a > a${path.sep}a.setup.ts > a setup [end]
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.spec.ts > a test [begin]
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) => {
const projectTemplates = {
'a': {
setupMatch: ['**/*.setup.ts']
_setupMatch: ['**/*._setup.ts']
},
'b': {
setupMatch: /.*b.setup.ts/
_setupMatch: /.*b._setup.ts/
},
'c': {
setupMatch: '**/c.setup.ts'
_setupMatch: '**/c._setup.ts'
},
};
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(passed).toBe(6);
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 = {
'a': {
setupMatch: ['**/*.setup.ts']
_setupMatch: ['**/*._setup.ts']
},
'b': {
setupMatch: /.*b.setup.ts/
_setupMatch: /.*b._setup.ts/
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
configWithFiles[`a/a.setup.ts`] = `
const { setup, expect } = pwt;
setup('a setup', async () => {
configWithFiles[`a/a._setup.ts`] = `
const { _setup, expect } = pwt;
_setup('a _setup', async () => {
expect(1).toBe(2);
});`;
@ -152,17 +152,17 @@ test('should stop project if setup fails', async ({ runGroups }, testInfo) => {
expect(passed).toBe(3);
expect(skipped).toBe(1); // 1 test from project 'a'
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test2', async () => { });
`,
'c.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'c._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
};
{ // 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(fileNames(timeline)).toEqual(['a.test.ts', 'c.setup.ts']);
expectFilesRunBefore(timeline, [`c.setup.ts`], [`a.test.ts`]);
expect(fileNames(timeline)).toEqual(['a.test.ts', 'c._setup.ts']);
expectFilesRunBefore(timeline, [`c._setup.ts`], [`a.test.ts`]);
expect(exitCode).toBe(0);
expect(passed).toBe(6);
}
{ // 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(fileNames(timeline)).toEqual(['b.test.ts', 'c.setup.ts']);
expectFilesRunBefore(timeline, [`c.setup.ts`], [`b.test.ts`]);
expect(fileNames(timeline)).toEqual(['b.test.ts', 'c._setup.ts']);
expectFilesRunBefore(timeline, [`c._setup.ts`], [`b.test.ts`]);
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*p1.setup.ts$/,
_setupMatch: /.*p1._setup.ts$/,
testMatch: /.*a.test.ts/,
},
{
name: 'p2',
setupMatch: /.*p2.setup.ts$/,
_setupMatch: /.*p2._setup.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('test2', async () => { });
`,
'p1.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'p1._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'p2.setup.ts': `
const { setup } = pwt;
setup('setup3', async () => { });
setup('setup4', async () => { });
'p2._setup.ts': `
const { _setup } = pwt;
_setup('_setup3', async () => { });
_setup('_setup4', async () => { });
`,
};
{ // 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(fileNames(timeline)).toEqual(['a.test.ts', 'p1.setup.ts']);
expectFilesRunBefore(timeline, [`p1.setup.ts`], [`a.test.ts`]);
expect(fileNames(timeline)).toEqual(['a.test.ts', 'p1._setup.ts']);
expectFilesRunBefore(timeline, [`p1._setup.ts`], [`a.test.ts`]);
expect(exitCode).toBe(0);
expect(passed).toBe(6);
}
{ // 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(fileNames(timeline)).toEqual(['b.test.ts', 'p2.setup.ts']);
expectFilesRunBefore(timeline, [`p2.setup.ts`], [`b.test.ts`]);
expect(fileNames(timeline)).toEqual(['b.test.ts', 'p2._setup.ts']);
expectFilesRunBefore(timeline, [`p2._setup.ts`], [`b.test.ts`]);
expect(exitCode).toBe(0);
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 = {
'a': {
setupMatch: /.*a.setup.ts/
_setupMatch: /.*a._setup.ts/
},
'b': {
setupMatch: /.*b.setup.ts/
_setupMatch: /.*b._setup.ts/
},
};
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
const { exitCode, passed, timeline } = await runGroups(configWithFiles, { project: ['a', 'c'] });
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*a.test.ts$/,
_setupMatch: /.*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);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*a.test.ts$/,
_setupMatch: /.*a.test.ts$/,
testMatch: /.*noMatch.test.ts$/,
},
{
name: 'p2',
setupMatch: /.*noMatch.test.ts$/,
_setupMatch: /.*noMatch.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);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*a..setup.ts$/,
_setupMatch: /.*a.._setup.ts$/,
testMatch: /.*a.test.ts$/,
},
{
name: 'p2',
setupMatch: /.*b.setup.ts$/,
_setupMatch: /.*b._setup.ts$/,
testMatch: /.*b.test.ts$/
},
]
};`,
'a1.setup.ts': `
const { setup } = pwt;
setup('test1', async () => { });
'a1._setup.ts': `
const { _setup } = pwt;
_setup('test1', async () => { });
`,
'a2.setup.ts': `
const { setup } = pwt;
setup('test1', async () => { });
'a2._setup.ts': `
const { _setup } = pwt;
_setup('test1', async () => { });
`,
'a.test.ts': `
const { test } = pwt;
test('test2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('test3', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('test3', async () => { });
`,
'b.test.ts': `
const { test } = pwt;
@ -372,42 +372,42 @@ test('list-files should enumerate setup files in same group', async ({ runComman
expect(exitCode).toBe(0);
const json = JSON.parse(output);
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[1].files.map(f => path.basename(f))).toEqual(['b.setup.ts', 'b.test.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']);
});
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*a..setup.ts$/,
_setupMatch: /.*a.._setup.ts$/,
testMatch: /.*a.test.ts$/,
},
{
name: 'p2',
setupMatch: /.*b.setup.ts$/,
_setupMatch: /.*b._setup.ts$/,
testMatch: /.*b.test.ts$/
},
]
};`,
'a1.setup.ts': `
const { setup } = pwt;
setup('test1', async () => { });
'a1._setup.ts': `
const { _setup } = pwt;
_setup('test1', async () => { });
`,
'a2.setup.ts': `
const { setup } = pwt;
setup('test1', async () => { });
'a2._setup.ts': `
const { _setup } = pwt;
_setup('test1', async () => { });
`,
'a.test.ts': `
const { test } = pwt;
test('test2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('test3', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('test3', async () => { });
`,
'b.test.ts': `
const { test } = pwt;
@ -419,21 +419,21 @@ test('test --list should enumerate setup tests as regular ones', async ({ runCom
expect(exitCode).toBe(0);
expect(output).toContain(`Listing tests:
[p1] a.test.ts:6:7 test2
[p1] a1.setup.ts:5:7 test1
[p1] a2.setup.ts:5:7 test1
[p2] b.setup.ts:5:7 test3
[p1] a1._setup.ts:5:7 test1
[p1] a2._setup.ts:5:7 test1
[p2] b._setup.ts:5:7 test3
[p2] b.test.ts:6:7 test4
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test4', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup.only('setup1', async () => { });
setup('setup2', async () => { });
setup.only('setup3', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup.only('_setup1', async () => { });
_setup('_setup2', async () => { });
_setup.only('_setup3', async () => { });
`,
};
const { exitCode, passed, timeline, output } = await runGroups(files);
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:7:13 setup3');
expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(output).toContain('[p1] a._setup.ts:5:14 _setup1');
expect(output).toContain('[p1] a._setup.ts:7:14 _setup3');
expect(fileNames(timeline)).toEqual(['a._setup.ts']);
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test4', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup.describe.only('main', () => {
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup.describe.only('main', () => {
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
});
setup('setup3', async () => { });
_setup('_setup3', async () => { });
`,
};
const { exitCode, passed, timeline, output } = await runGroups(files);
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:7:9 main setup2');
expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(output).toContain('[p1] a._setup.ts:6:9 main _setup1');
expect(output).toContain('[p1] a._setup.ts:7:9 main _setup2');
expect(fileNames(timeline)).toEqual(['a._setup.ts']);
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test4', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup.describe('main', () => {
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup.describe('main', () => {
_setup('_setup1', 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('[p1] a.setup.ts:6:9 main setup1');
expect(output).toContain('[p1] a.setup.ts:7:9 main setup2');
expect(fileNames(timeline)).toEqual(['a.setup.ts']);
expect(output).toContain('[p1] a._setup.ts:6:9 main _setup1');
expect(output).toContain('[p1] a._setup.ts:7:9 main _setup2');
expect(fileNames(timeline)).toEqual(['a._setup.ts']);
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test4', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup.only('setup1', async () => { });
setup('setup2', async () => { });
setup('setup3', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup.only('_setup1', async () => { });
_setup('_setup2', async () => { });
_setup('_setup3', async () => { });
`,
};
const { exitCode, output } = await runGroups(files);
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');
});
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test4', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup3', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_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(passed).toBe(4);
expect(output).toContain('Running 4 tests using 2 workers');
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:6:7 setup2');
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:6:7 _setup2');
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*a.setup.ts/,
_setupMatch: /.*a._setup.ts/,
},
{
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('test3', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
'b.test.ts': `
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('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a.setup.ts:6:7 setup2');
expect(output).toContain('[p2] b.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('[p2] b._setup.ts:5:7 _setup1');
expect(exitCode).toBe(0);
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('[p1] a.setup.ts:5:7 setup1');
expect(output).toContain('[p1] a._setup.ts:5:7 _setup1');
expect(exitCode).toBe(0);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test3', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
'b.test.ts': `
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(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:6:7 setup2');
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.test.ts:6:7 test1');
expect(output).toContain('[p1] a.test.ts:7:7 test2');
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 = {
'playwright.config.ts': `
module.exports = {
@ -712,12 +712,12 @@ test('should run setup for a project if tests match only in another project', as
{
name: 'p1',
testMatch: /.*a.test.ts/,
setupMatch: /.*a.setup.ts/,
_setupMatch: /.*a._setup.ts/,
},
{
name: 'p2',
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;
test('test1', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
'b.test.ts': `
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$'] });
expect(exitCode).toBe(0);
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('[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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test3', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
};
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['a.test.ts:7'] });
expect(exitCode).toBe(0);
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:6:7 setup2');
expect(output).toContain('[p1] b.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] b._setup.ts:5:7 _setup1');
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
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('test3', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
`,
};
const { exitCode, output } = await runGroups(files, undefined, undefined, { additionalArgs: ['--grep', '.*test2$'] });
expect(exitCode).toBe(0);
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:6:7 setup2');
expect(output).toContain('[p1] b.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] b._setup.ts:5:7 _setup1');
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*.setup.ts/,
grep: /a.(test|setup).ts.*(test|setup)/,
_setupMatch: /.*._setup.ts/,
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('foo', async () => { });
`,
'a.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('setup2', async () => { });
'a._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('_setup2', async () => { });
`,
'b.setup.ts': `
const { setup } = pwt;
setup('setup1', async () => { });
setup('foo', async () => { });
'b._setup.ts': `
const { _setup } = pwt;
_setup('_setup1', async () => { });
_setup('foo', async () => { });
`,
};
const { exitCode, output } = await runGroups(files);
expect(exitCode).toBe(0);
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:5:7 _setup1');
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: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 = {
'a.test.ts': `
const { setup, test } = pwt;
setup('test1', async () => { });
const { _setup, test } = pwt;
_setup('test1', async () => { });
test('test2', async () => { });
`,
};
const { exitCode, output } = await runGroups(files);
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 = {
'a.test.ts': `
const { setup } = pwt;
setup.beforeAll(async () => { });
const { _setup } = pwt;
_setup.beforeAll(async () => { });
`,
};
const { exitCode, output } = await runGroups(files);
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 = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*.setup.ts/,
_setupMatch: /.*._setup.ts/,
},
]
};`,
'a.setup.ts': `
'a._setup.ts': `
const { test } = pwt;
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');
});
test('should prohibit test hooks in setup files', async ({ runGroups }, testInfo) => {
test('should prohibit test hooks in _setup files', async ({ runGroups }, testInfo) => {
const files = {
'playwright.config.ts': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*.setup.ts/,
_setupMatch: /.*._setup.ts/,
},
]
};`,
'a.setup.ts': `
'a._setup.ts': `
const { test } = pwt;
test.beforeEach(async () => { });
`,

View file

@ -16,24 +16,24 @@
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({
'playwright.config.js': `
module.exports = {};
`,
'a.test.ts': `
const { test, store } = pwt;
test('should store number', async ({ }) => {
expect(store).toBeTruthy();
expect(await store.get('number')).toBe(undefined);
await store.set('number', 2022)
expect(await store.get('number')).toBe(2022);
const { test, _store } = pwt;
test('should _store number', async ({ }) => {
expect(_store).toBeTruthy();
expect(await _store.get('number')).toBe(undefined);
await _store.set('number', 2022)
expect(await _store.get('number')).toBe(2022);
});
test('should store object', async ({ }) => {
expect(store).toBeTruthy();
expect(await store.get('object')).toBe(undefined);
await store.set('object', { 'a': 2022 })
expect(await store.get('object')).toEqual({ 'a': 2022 });
test('should _store object', async ({ }) => {
expect(_store).toBeTruthy();
expect(await _store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 })
expect(await _store.get('object')).toEqual({ 'a': 2022 });
});
`,
}, { workers: 1 });
@ -41,42 +41,42 @@ test('should provide store fixture', async ({ runInlineTest }) => {
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({
'playwright.config.js': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*store.setup.ts/
_setupMatch: /.*_store.setup.ts/
}
]
};
`,
'store.setup.ts': `
const { setup, expect, store } = pwt;
setup('should initialize store', async ({ }) => {
expect(await store.get('number')).toBe(undefined);
await store.set('number', 2022)
expect(await store.get('number')).toBe(2022);
'_store.setup.ts': `
const { _setup, expect, _store } = pwt;
_setup('should initialize _store', async ({ }) => {
expect(await _store.get('number')).toBe(undefined);
await _store.set('number', 2022)
expect(await _store.get('number')).toBe(2022);
expect(await store.get('object')).toBe(undefined);
await store.set('object', { 'a': 2022 })
expect(await store.get('object')).toEqual({ 'a': 2022 });
expect(await _store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 })
expect(await _store.get('object')).toEqual({ 'a': 2022 });
});
`,
'a.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should get data from setup', async ({ }) => {
expect(await store.get('number')).toBe(2022);
expect(await store.get('object')).toEqual({ 'a': 2022 });
expect(await _store.get('number')).toBe(2022);
expect(await _store.get('object')).toEqual({ 'a': 2022 });
});
`,
'b.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should get data from setup', async ({ }) => {
expect(await store.get('number')).toBe(2022);
expect(await store.get('object')).toEqual({ 'a': 2022 });
expect(await _store.get('number')).toBe(2022);
expect(await _store.get('object')).toEqual({ 'a': 2022 });
});
`,
}, { workers: 1 });
@ -84,25 +84,25 @@ test('should share store state between project setup and tests', async ({ runInl
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 = {
'playwright.config.js': `
module.exports = { };
`,
'a.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should have no data on first run', async ({ }) => {
expect(await store.get('number')).toBe(undefined);
await store.set('number', 2022)
expect(await store.get('object')).toBe(undefined);
await store.set('object', { 'a': 2022 })
expect(await _store.get('number')).toBe(undefined);
await _store.set('number', 2022)
expect(await _store.get('object')).toBe(undefined);
await _store.set('object', { 'a': 2022 })
});
`,
'b.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should get data from previous run', async ({ }) => {
expect(await store.get('number')).toBe(2022);
expect(await store.get('object')).toEqual({ 'a': 2022 });
expect(await _store.get('number')).toBe(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({
'playwright.config.js': `
module.exports = {
projects: [
{
name: 'p1',
setupMatch: /.*store.setup.ts/
_setupMatch: /.*_store.setup.ts/
},
{
name: 'p2',
setupMatch: /.*store.setup.ts/
_setupMatch: /.*_store.setup.ts/
}
]
};
`,
'store.setup.ts': `
const { setup, expect, store } = pwt;
setup('should initialize store', async ({ }) => {
expect(await store.get('number')).toBe(undefined);
await store.set('number', 2022)
expect(await store.get('number')).toBe(2022);
'_store.setup.ts': `
const { _setup, expect, _store } = pwt;
_setup('should initialize _store', async ({ }) => {
expect(await _store.get('number')).toBe(undefined);
await _store.set('number', 2022)
expect(await _store.get('number')).toBe(2022);
expect(await store.get('name')).toBe(undefined);
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(undefined);
await _store.set('name', 'str-' + _setup.info().project.name)
expect(await _store.get('name')).toBe('str-' + _setup.info().project.name);
});
`,
'a.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should get data from setup', async ({ }) => {
expect(await store.get('number')).toBe(2022);
expect(await store.get('name')).toBe('str-' + test.info().project.name);
expect(await _store.get('number')).toBe(2022);
expect(await _store.get('name')).toBe('str-' + test.info().project.name);
});
`,
'b.test.ts': `
const { test, store } = pwt;
const { test, _store } = pwt;
test('should get data from setup', async ({ }) => {
expect(await store.get('number')).toBe(2022);
expect(await store.get('name')).toBe('str-' + test.info().project.name);
expect(await _store.get('number')).toBe(2022);
expect(await _store.get('name')).toBe('str-' + test.info().project.name);
});
`,
}, { workers: 2 });
@ -165,7 +165,7 @@ test('should isolate store state between projects', async ({ runInlineTest }) =>
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) => {
res.setHeader('Set-Cookie', ['a=v1']);
res.end();
@ -176,24 +176,24 @@ test('should load context storageState from store', async ({ runInlineTest, serv
projects: [
{
name: 'p1',
setupMatch: /.*store.setup.ts/
_setupMatch: /.*_store.setup.ts/
}
]
};
`,
'store.setup.ts': `
const { setup, expect, store } = pwt;
setup('should save storageState', async ({ page, context }) => {
expect(await store.get('user')).toBe(undefined);
'_store.setup.ts': `
const { _setup, expect, _store } = pwt;
_setup('should save storageState', async ({ page, context }) => {
expect(await _store.get('user')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
await store.set('user', state);
await _store.set('user', state);
});
`,
'a.test.ts': `
const { test } = pwt;
test.use({
storageStateName: 'user'
_storageStateName: 'user'
})
test('should get data from setup', async ({ 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);
});
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) => {
res.setHeader('Set-Cookie', ['a=v1']);
res.end();
@ -225,24 +225,24 @@ test('should load storageStateName specified in the project config from store',
projects: [
{
name: 'p1',
setupMatch: /.*store.setup.ts/,
_setupMatch: /.*_store.setup.ts/,
use: {
storageStateName: 'stateInStorage',
_storageStateName: 'stateInStorage',
},
}
]
};
`,
'store.setup.ts': `
const { setup, expect, store } = pwt;
setup.use({
storageStateName: ({}, use) => use(undefined),
'_store.setup.ts': `
const { _setup, expect, _store } = pwt;
_setup.use({
_storageStateName: ({}, use) => use(undefined),
})
setup('should save storageState', async ({ page, context }) => {
expect(await store.get('stateInStorage')).toBe(undefined);
_setup('should save storageState', async ({ page, context }) => {
expect(await _store.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
await store.set('stateInStorage', state);
await _store.set('stateInStorage', state);
});
`,
'a.test.ts': `
@ -258,7 +258,7 @@ test('should load storageStateName specified in the project config from store',
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) => {
res.setHeader('Set-Cookie', ['a=v1']);
res.end();
@ -267,26 +267,26 @@ test('should load storageStateName specified in the global config from store', a
'playwright.config.js': `
module.exports = {
use: {
storageStateName: 'stateInStorage',
_storageStateName: 'stateInStorage',
},
projects: [
{
name: 'p1',
setupMatch: /.*store.setup.ts/,
_setupMatch: /.*_store.setup.ts/,
}
]
};
`,
'store.setup.ts': `
const { setup, expect, store } = pwt;
setup.use({
storageStateName: ({}, use) => use(undefined),
'_store.setup.ts': `
const { _setup, expect, _store } = pwt;
_setup.use({
_storageStateName: ({}, use) => use(undefined),
})
setup('should save storageStateName', async ({ page, context }) => {
expect(await store.get('stateInStorage')).toBe(undefined);
_setup('should save _storageStateName', async ({ page, context }) => {
expect(await _store.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
await store.set('stateInStorage', state);
await _store.set('stateInStorage', state);
});
`,
'a.test.ts': `
@ -302,7 +302,7 @@ test('should load storageStateName specified in the global config from store', a
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({
'playwright.config.js': `
module.exports = {
@ -310,7 +310,7 @@ test('should throw on unknown storageStateName value', async ({ runInlineTest, s
{
name: 'p1',
use: {
storageStateName: 'stateInStorage',
_storageStateName: 'stateInStorage',
},
}
]
@ -324,5 +324,5 @@ test('should throw on unknown storageStateName value', async ({ runInlineTest, s
}, { workers: 1 });
expect(result.exitCode).toBe(1);
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);
});
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;
};
export interface TestStore {
get<T>(name: string): Promise<T | undefined>;
set<T>(name: string, value: T | undefined): Promise<void>;
}
export interface PlaywrightWorkerOptions {
browserName: BrowserName;
defaultBrowserType: BrowserName;
@ -234,7 +229,6 @@ export interface PlaywrightTestOptions {
permissions: string[] | undefined;
proxy: Proxy | undefined;
storageState: StorageState | undefined;
storageStateName: string | undefined;
timezoneId: string | undefined;
userAgent: string | undefined;
viewport: ViewportSize | null | undefined;
@ -351,7 +345,6 @@ export default test;
export const _baseTest: TestType<{}, {}>;
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
export {};