feat(test runner): replace declare/define with "options" (#10293)
1. Fixtures defined in test.extend() can now have `{ option: true }` configuration that makes them overridable in the config. Options support all other properties of fixtures - value/function, scope, auto. ``` const test = base.extend<MyOptions>({ foo: ['default', { option: true }], }); ``` 2. test.declare() and project.define are removed. 3. project.use applies overrides to default option values and nothing else. Any test.extend() and test.use() calls take priority over config options. Required user changes: if someone used to define fixture options with test.extend(), overriding them in config will stop working. The solution is to add `{ option: true }`. ``` // Old code export const test = base.extend<{ myOption: number, myFixture: number }>({ myOption: 123, myFixture: ({ myOption }, use) => use(2 * myOption), }); // New code export const test = base.extend<{ myOption: number, myFixture: number }>({ myOption: [123, { option: true }], myFixture: ({ myOption }, use) => use(2 * myOption), }); ```
This commit is contained in:
parent
17a2454226
commit
d9f849fb14
|
@ -367,7 +367,7 @@ test('test', async ({ page }) => {
|
|||
|
||||
Playwright Test supports running multiple test projects at the same time. This is useful for running the same tests in multiple configurations. For example, consider running tests against multiple versions of some REST backend.
|
||||
|
||||
To make use of this feature, we will declare an "option fixture" for the backend version, and use it in the tests.
|
||||
In the following example, we will declare an option for the backend version, and a fixture that uses the option, and we'll be configuring two projects that test against different versions.
|
||||
|
||||
```js js-flavor=js
|
||||
// my-test.js
|
||||
|
@ -375,8 +375,9 @@ const base = require('@playwright/test');
|
|||
const { startBackend } = require('./my-backend');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
// Default value for the version.
|
||||
version: '1.0',
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
version: ['1.0', { option: true }],
|
||||
|
||||
// Use version when starting the backend.
|
||||
backendURL: async ({ version }, use) => {
|
||||
|
@ -392,14 +393,13 @@ exports.test = base.test.extend({
|
|||
import { test as base } from '@playwright/test';
|
||||
import { startBackend } from './my-backend';
|
||||
|
||||
export type TestOptions = {
|
||||
version: string;
|
||||
backendURL: string;
|
||||
};
|
||||
export type TestOptions = { version: string };
|
||||
type TestFixtures = { backendURL: string };
|
||||
|
||||
export const test = base.extend<TestOptions>({
|
||||
// Default value for the version.
|
||||
version: '1.0',
|
||||
export const test = base.extend<TestOptions & TestFixtures>({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
version: ['1.0', { option: true }],
|
||||
|
||||
// Use version when starting the backend.
|
||||
backendURL: async ({ version }, use) => {
|
||||
|
@ -410,7 +410,7 @@ export const test = base.extend<TestOptions>({
|
|||
});
|
||||
```
|
||||
|
||||
We can use our fixtures in the test.
|
||||
We can use our fixture and/or option in the test.
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
const { test } = require('./my-test');
|
||||
|
@ -445,14 +445,13 @@ test('test 2', async ({ version, page, backendURL }) => {
|
|||
});
|
||||
```
|
||||
|
||||
Now, we can run test in multiple configurations by using projects.
|
||||
Now, we can run tests in multiple configurations by using projects.
|
||||
```js js-flavor=js
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ version: string }>} */
|
||||
const config = {
|
||||
timeout: 20000,
|
||||
projects: [
|
||||
{
|
||||
name: 'v1',
|
||||
|
@ -474,7 +473,6 @@ import { PlaywrightTestConfig } from '@playwright/test';
|
|||
import { TestOptions } from './my-test';
|
||||
|
||||
const config: PlaywrightTestConfig<TestOptions> = {
|
||||
timeout: 20000,
|
||||
projects: [
|
||||
{
|
||||
name: 'v1',
|
||||
|
@ -489,7 +487,7 @@ const config: PlaywrightTestConfig<TestOptions> = {
|
|||
export default config;
|
||||
```
|
||||
|
||||
Each project can be configured separately, and run different set of tests with different parameters. See [project options][TestProject] for the list of options available to each project.
|
||||
Each project can be configured separately, and run different set of tests with different of [built-in][TestProject] and custom options.
|
||||
|
||||
You can run all projects or just a single one:
|
||||
```bash
|
||||
|
|
|
@ -204,6 +204,7 @@ Hook function that takes one or two arguments: an object with fixtures and optio
|
|||
|
||||
|
||||
|
||||
|
||||
## method: Test.describe
|
||||
|
||||
Declares a group of tests.
|
||||
|
@ -418,6 +419,137 @@ A callback that is run immediately when calling [`method: Test.describe.serial.o
|
|||
|
||||
|
||||
|
||||
|
||||
## method: Test.extend
|
||||
- returns: <[Test]>
|
||||
|
||||
Extends the `test` object by defining fixtures and/or options that can be used in the tests.
|
||||
|
||||
First define a fixture and/or an option.
|
||||
|
||||
```js js-flavor=js
|
||||
// my-test.js
|
||||
const base = require('@playwright/test');
|
||||
const { TodoPage } = require('./todo-page');
|
||||
|
||||
// Extend basic test by providing a "defaultItem" option and a "todoPage" fixture.
|
||||
exports.test = base.test.extend({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
defaultItem: ['Do stuff', { option: true }],
|
||||
|
||||
// Define a fixture. Note that it can use built-in fixture "page"
|
||||
// and a new option "defaultItem".
|
||||
todoPage: async ({ page, defaultItem }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo(defaultItem);
|
||||
await use(todoPage);
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```js js-flavor=ts
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
|
||||
export type Options = { defaultItem: string };
|
||||
|
||||
// Extend basic test by providing a "defaultItem" option and a "todoPage" fixture.
|
||||
export const test = base.extend<Options & { todoPage: TodoPage }>({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
defaultItem: ['Do stuff', { option: true }],
|
||||
|
||||
// Define a fixture. Note that it can use built-in fixture "page"
|
||||
// and a new option "defaultItem".
|
||||
todoPage: async ({ page, defaultItem }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo(defaultItem);
|
||||
await use(todoPage);
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then use the fixture in the test.
|
||||
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
const { test } = require('./my-test');
|
||||
|
||||
test('test 1', async ({ todoPage }) => {
|
||||
await todoPage.addToDo('my todo');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```js js-flavor=ts
|
||||
// example.spec.ts
|
||||
import { test } from './my-test';
|
||||
|
||||
test('test 1', async ({ todoPage }) => {
|
||||
await todoPage.addToDo('my todo');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Configure the option in config file.
|
||||
|
||||
```js js-flavor=js
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ defaultItem: string }>} */
|
||||
const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'shopping',
|
||||
use: { defaultItem: 'Buy milk' },
|
||||
},
|
||||
{
|
||||
name: 'wellbeing',
|
||||
use: { defaultItem: 'Exercise!' },
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
```
|
||||
|
||||
```js js-flavor=ts
|
||||
// playwright.config.ts
|
||||
import { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { Options } from './my-test';
|
||||
|
||||
const config: PlaywrightTestConfig<Options> = {
|
||||
projects: [
|
||||
{
|
||||
name: 'shopping',
|
||||
use: { defaultItem: 'Buy milk' },
|
||||
},
|
||||
{
|
||||
name: 'wellbeing',
|
||||
use: { defaultItem: 'Exercise!' },
|
||||
},
|
||||
]
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
Learn more about [fixtures](./test-fixtures.md) and [parametrizing tests](./test-parameterize.md).
|
||||
|
||||
### param: Test.extend.fixtures
|
||||
- `fixtures` <[Object]>
|
||||
|
||||
An object containing fixtures and/or options. Learn more about [fixtures format](./test-fixtures.md).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## method: Test.fail
|
||||
|
||||
Marks a test or a group of tests as "should fail". Playwright Test runs these tests and ensures that they are actually failing. This is useful for documentation purposes to acknowledge that some functionality is broken until it is fixed.
|
||||
|
|
|
@ -82,7 +82,7 @@ test('should remove an item', async ({ todoPage }) => {
|
|||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
|
||||
// Extend basic test by providing a "table" fixture.
|
||||
// Extend basic test by providing a "todoPage" fixture.
|
||||
const test = base.extend<{ todoPage: TodoPage }>({
|
||||
todoPage: async ({ page }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
|
@ -139,21 +139,21 @@ test('hello world', ({ helloWorld }) => {
|
|||
});
|
||||
```
|
||||
|
||||
It uses fixtures `hello` and `helloWorld` that are set up by the framework for each test run.
|
||||
It uses an option `hello` and a fixture `helloWorld` that are set up by the framework for each test run.
|
||||
|
||||
Here is how test fixtures are declared and defined. Fixtures can use other fixtures - note how `helloWorld` uses `hello`.
|
||||
Here is how test fixtures are defined. Fixtures can use other fixtures and/or options - note how `helloWorld` uses `hello`.
|
||||
|
||||
```js js-flavor=js
|
||||
// hello.js
|
||||
const base = require('@playwright/test');
|
||||
|
||||
// Extend base test with fixtures "hello" and "helloWorld".
|
||||
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
|
||||
module.exports = base.test.extend({
|
||||
// This fixture is a constant, so we can just provide the value.
|
||||
hello: 'Hello',
|
||||
// Extend base test with our options and fixtures.
|
||||
const test = base.test.extend({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
hello: ['Hello', { option: true }],
|
||||
|
||||
// This fixture has some complex logic and is defined with a function.
|
||||
// Define a fixture.
|
||||
helloWorld: async ({ hello }, use) => {
|
||||
// Set up the fixture.
|
||||
const value = hello + ', world!';
|
||||
|
@ -164,24 +164,29 @@ module.exports = base.test.extend({
|
|||
// Clean up the fixture. Nothing to cleanup in this example.
|
||||
},
|
||||
});
|
||||
|
||||
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
|
||||
module.exports = test;
|
||||
```
|
||||
|
||||
```js js-flavor=ts
|
||||
// hello.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
// Define test fixtures "hello" and "helloWorld".
|
||||
type TestFixtures = {
|
||||
type TestOptions = {
|
||||
hello: string;
|
||||
};
|
||||
type TestFixtures = {
|
||||
helloWorld: string;
|
||||
};
|
||||
|
||||
// Extend base test with our fixtures.
|
||||
const test = base.extend<TestFixtures>({
|
||||
// This fixture is a constant, so we can just provide the value.
|
||||
hello: 'Hello',
|
||||
// Extend base test with our options and fixtures.
|
||||
const test = base.extend<TestOptions & TestFixtures>({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
hello: ['Hello', { option: true }],
|
||||
|
||||
// This fixture has some complex logic and is defined with a function.
|
||||
// Define a fixture.
|
||||
helloWorld: async ({ hello }, use) => {
|
||||
// Set up the fixture.
|
||||
const value = hello + ', world!';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
id: test-parameterize
|
||||
title: "Parameterize tests"
|
||||
title: "Parametrize tests"
|
||||
---
|
||||
|
||||
You can either parameterize tests on a test level or on a project level.
|
||||
You can either parametrize tests on a test level or on a project level.
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
|
@ -33,16 +33,18 @@ for (const name of people) {
|
|||
|
||||
## Parametrized Projects
|
||||
|
||||
Playwright Test supports running multiple test projects at the same time. In the following example, we'll run two projects with different parameters.
|
||||
A parameter itself is represented as a [`fixture`](./api/class-fixtures), where the value gets set from the config. The first project runs with the value `Alice` and the second with the value `Bob`.
|
||||
Playwright Test supports running multiple test projects at the same time. In the following example, we'll run two projects with different options.
|
||||
|
||||
We declare the option `person` and set the value in the config. The first project runs with the value `Alice` and the second with the value `Bob`.
|
||||
|
||||
```js js-flavor=js
|
||||
// my-test.js
|
||||
const base = require('@playwright/test');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
// Default value for person.
|
||||
person: 'not-set',
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
person: ['John', { option: true }],
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -55,12 +57,14 @@ export type TestOptions = {
|
|||
};
|
||||
|
||||
export const test = base.extend<TestOptions>({
|
||||
// Default value for the person.
|
||||
person: 'not-set',
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
person: ['John', { option: true }],
|
||||
});
|
||||
```
|
||||
|
||||
We can use our fixtures in the test.
|
||||
We can use this option in the test, similarly to [fixtures](./test-fixtures.md).
|
||||
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
const { test } = require('./my-test');
|
||||
|
@ -83,7 +87,8 @@ test('test 1', async ({ page, person }) => {
|
|||
});
|
||||
```
|
||||
|
||||
Now, we can run test in multiple configurations by using projects.
|
||||
Now, we can run tests in multiple configurations by using projects.
|
||||
|
||||
```js js-flavor=js
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
@ -92,11 +97,11 @@ Now, we can run test in multiple configurations by using projects.
|
|||
const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'Alice',
|
||||
name: 'alice',
|
||||
use: { person: 'Alice' },
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
name: 'bob',
|
||||
use: { person: 'Bob' },
|
||||
},
|
||||
]
|
||||
|
@ -111,17 +116,64 @@ import { PlaywrightTestConfig } from '@playwright/test';
|
|||
import { TestOptions } from './my-test';
|
||||
|
||||
const config: PlaywrightTestConfig<TestOptions> = {
|
||||
timeout: 20000,
|
||||
projects: [
|
||||
{
|
||||
name: 'alice',
|
||||
use: { person: 'Alice' },
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
name: 'bob',
|
||||
use: { person: 'Bob' },
|
||||
},
|
||||
]
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
We can also use the option in a fixture. Learn more about [fixtures](./test-fixtures.md).
|
||||
|
||||
```js js-flavor=js
|
||||
// my-test.js
|
||||
const base = require('@playwright/test');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
person: ['John', { option: true }],
|
||||
|
||||
// Override default "page" fixture.
|
||||
page: async ({ page, person }, use) => {
|
||||
await page.goto('/chat');
|
||||
// We use "person" parameter as a "name" for the chat room.
|
||||
await page.locator('#name').fill(person);
|
||||
await page.click('text=Enter chat room');
|
||||
// Each test will get a "page" that already has the person name.
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```js js-flavor=ts
|
||||
// my-test.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export type TestOptions = {
|
||||
person: string;
|
||||
};
|
||||
|
||||
export const test = base.test.extend<TestOptions>({
|
||||
// Define an option and provide a default value.
|
||||
// We can later override it in the config.
|
||||
person: ['John', { option: true }],
|
||||
|
||||
// Override default "page" fixture.
|
||||
page: async ({ page, person }, use) => {
|
||||
await page.goto('/chat');
|
||||
// We use "person" parameter as a "name" for the chat room.
|
||||
await page.locator('#name').fill(person);
|
||||
await page.click('text=Enter chat room');
|
||||
// Each test will get a "page" that already has the person name.
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
|
@ -103,6 +103,14 @@ class Fixture {
|
|||
}
|
||||
}
|
||||
|
||||
function isFixtureTuple(value: any): value is [any, any] {
|
||||
return Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1] || 'option' in value[1]);
|
||||
}
|
||||
|
||||
export function isFixtureOption(value: any): value is [any, any] {
|
||||
return isFixtureTuple(value) && !!value[1].option;
|
||||
}
|
||||
|
||||
export class FixturePool {
|
||||
readonly digest: string;
|
||||
readonly registrations: Map<string, FixtureRegistration>;
|
||||
|
@ -115,7 +123,7 @@ export class FixturePool {
|
|||
const name = entry[0];
|
||||
let value = entry[1];
|
||||
let options: { auto: boolean, scope: FixtureScope } | undefined;
|
||||
if (Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1])) {
|
||||
if (isFixtureTuple(value)) {
|
||||
options = {
|
||||
auto: !!value[1].auto,
|
||||
scope: value[1].scope || 'test'
|
||||
|
|
|
@ -31,16 +31,16 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
|||
_setupContextOptionsAndArtifacts: void;
|
||||
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||
};
|
||||
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
_browserType: BrowserType;
|
||||
_browserOptions: LaunchOptions;
|
||||
_artifactsDir: () => string;
|
||||
_snapshotSuffix: string;
|
||||
};
|
||||
|
||||
export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
||||
defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
|
||||
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
|
||||
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||
defaultBrowserType: [ 'chromium', { scope: 'worker', option: true } ],
|
||||
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true } ],
|
||||
playwright: [async ({}, use, workerInfo) => {
|
||||
if (process.env.PW_GRID) {
|
||||
const gridClient = await GridClient.connect(process.env.PW_GRID);
|
||||
|
@ -50,12 +50,12 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||
await use(require('playwright-core'));
|
||||
}
|
||||
}, { scope: 'worker' } ],
|
||||
headless: [ undefined, { scope: 'worker' } ],
|
||||
channel: [ undefined, { scope: 'worker' } ],
|
||||
launchOptions: [ {}, { scope: 'worker' } ],
|
||||
screenshot: [ 'off', { scope: 'worker' } ],
|
||||
video: [ 'off', { scope: 'worker' } ],
|
||||
trace: [ 'off', { scope: 'worker' } ],
|
||||
headless: [ undefined, { scope: 'worker', option: true } ],
|
||||
channel: [ undefined, { scope: 'worker', option: true } ],
|
||||
launchOptions: [ {}, { scope: 'worker', option: true } ],
|
||||
screenshot: [ 'off', { scope: 'worker', option: true } ],
|
||||
video: [ 'off', { scope: 'worker', option: true } ],
|
||||
trace: [ 'off', { scope: 'worker', option: true } ],
|
||||
|
||||
_artifactsDir: [async ({}, use, workerInfo) => {
|
||||
let dir: string | undefined;
|
||||
|
@ -74,31 +74,31 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||
_browserType: [browserTypeWorkerFixture, { scope: 'worker' }],
|
||||
browser: [browserWorkerFixture, { scope: 'worker' } ],
|
||||
|
||||
acceptDownloads: undefined,
|
||||
bypassCSP: undefined,
|
||||
colorScheme: undefined,
|
||||
deviceScaleFactor: undefined,
|
||||
extraHTTPHeaders: undefined,
|
||||
geolocation: undefined,
|
||||
hasTouch: undefined,
|
||||
httpCredentials: undefined,
|
||||
ignoreHTTPSErrors: undefined,
|
||||
isMobile: undefined,
|
||||
javaScriptEnabled: undefined,
|
||||
locale: undefined,
|
||||
offline: undefined,
|
||||
permissions: undefined,
|
||||
proxy: undefined,
|
||||
storageState: undefined,
|
||||
timezoneId: undefined,
|
||||
userAgent: undefined,
|
||||
viewport: undefined,
|
||||
actionTimeout: undefined,
|
||||
navigationTimeout: undefined,
|
||||
baseURL: async ({ }, use) => {
|
||||
acceptDownloads: [ undefined, { option: true } ],
|
||||
bypassCSP: [ undefined, { option: true } ],
|
||||
colorScheme: [ undefined, { option: true } ],
|
||||
deviceScaleFactor: [ undefined, { option: true } ],
|
||||
extraHTTPHeaders: [ undefined, { option: true } ],
|
||||
geolocation: [ undefined, { option: true } ],
|
||||
hasTouch: [ undefined, { option: true } ],
|
||||
httpCredentials: [ undefined, { option: true } ],
|
||||
ignoreHTTPSErrors: [ undefined, { option: true } ],
|
||||
isMobile: [ undefined, { option: true } ],
|
||||
javaScriptEnabled: [ undefined, { option: true } ],
|
||||
locale: [ undefined, { option: true } ],
|
||||
offline: [ undefined, { option: true } ],
|
||||
permissions: [ undefined, { option: true } ],
|
||||
proxy: [ undefined, { option: true } ],
|
||||
storageState: [ undefined, { option: true } ],
|
||||
timezoneId: [ undefined, { option: true } ],
|
||||
userAgent: [ undefined, { option: true } ],
|
||||
viewport: [ undefined, { option: true } ],
|
||||
actionTimeout: [ undefined, { option: true } ],
|
||||
navigationTimeout: [ undefined, { option: true } ],
|
||||
baseURL: [ async ({ }, use) => {
|
||||
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
|
||||
},
|
||||
contextOptions: {},
|
||||
}, { option: true } ],
|
||||
contextOptions: [ {}, { option: true } ],
|
||||
|
||||
_combinedContextOptions: async ({
|
||||
acceptDownloads,
|
||||
|
|
|
@ -173,7 +173,6 @@ export class Loader {
|
|||
if (!path.isAbsolute(snapshotDir))
|
||||
snapshotDir = path.resolve(configDir, snapshotDir);
|
||||
const fullProject: FullProject = {
|
||||
define: takeFirst(this._configOverrides.define, projectConfig.define, this._config.define, []),
|
||||
expect: takeFirst(this._configOverrides.expect, projectConfig.expect, this._config.expect, undefined),
|
||||
outputDir,
|
||||
repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, this._config.repeatEach, 1),
|
||||
|
@ -357,16 +356,6 @@ function validateProject(file: string, project: Project, title: string) {
|
|||
if (typeof project !== 'object' || !project)
|
||||
throw errorWithFile(file, `${title} must be an object`);
|
||||
|
||||
if ('define' in project && project.define !== undefined) {
|
||||
if (Array.isArray(project.define)) {
|
||||
project.define.forEach((item, index) => {
|
||||
validateDefine(file, item, `${title}.define[${index}]`);
|
||||
});
|
||||
} else {
|
||||
validateDefine(file, project.define, `${title}.define`);
|
||||
}
|
||||
}
|
||||
|
||||
if ('name' in project && project.name !== undefined) {
|
||||
if (typeof project.name !== 'string')
|
||||
throw errorWithFile(file, `${title}.name must be a string`);
|
||||
|
@ -417,11 +406,6 @@ function validateProject(file: string, project: Project, title: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateDefine(file: string, define: any, title: string) {
|
||||
if (!define || typeof define !== 'object' || !define.test || !define.fixtures)
|
||||
throw errorWithFile(file, `${title} must be an object with "test" and "fixtures" properties`);
|
||||
}
|
||||
|
||||
const baseFullConfig: FullConfig = {
|
||||
forbidOnly: false,
|
||||
globalSetup: null,
|
||||
|
|
|
@ -14,39 +14,26 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestType, FullProject, Fixtures, FixturesWithLocation } from './types';
|
||||
import type { FullProject, Fixtures, FixturesWithLocation } from './types';
|
||||
import { Suite, TestCase } from './test';
|
||||
import { FixturePool } from './fixtures';
|
||||
import { DeclaredFixtures, TestTypeImpl } from './testType';
|
||||
import { FixturePool, isFixtureOption } from './fixtures';
|
||||
import { TestTypeImpl } from './testType';
|
||||
|
||||
export class ProjectImpl {
|
||||
config: FullProject;
|
||||
private index: number;
|
||||
private defines = new Map<TestType<any, any>, Fixtures>();
|
||||
private testTypePools = new Map<TestTypeImpl, FixturePool>();
|
||||
private testPools = new Map<TestCase, FixturePool>();
|
||||
|
||||
constructor(project: FullProject, index: number) {
|
||||
this.config = project;
|
||||
this.index = index;
|
||||
this.defines = new Map();
|
||||
for (const { test, fixtures } of Array.isArray(project.define) ? project.define : [project.define])
|
||||
this.defines.set(test, fixtures);
|
||||
}
|
||||
|
||||
private buildTestTypePool(testType: TestTypeImpl): FixturePool {
|
||||
if (!this.testTypePools.has(testType)) {
|
||||
const fixtures = this.resolveFixtures(testType);
|
||||
const overrides: Fixtures = this.config.use;
|
||||
const overridesWithLocation = {
|
||||
fixtures: overrides,
|
||||
location: {
|
||||
file: `<configuration file>`,
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
};
|
||||
const pool = new FixturePool([...fixtures, overridesWithLocation]);
|
||||
const fixtures = this.resolveFixtures(testType, this.config.use);
|
||||
const pool = new FixturePool(fixtures);
|
||||
this.testTypePools.set(testType, pool);
|
||||
}
|
||||
return this.testTypePools.get(testType)!;
|
||||
|
@ -121,13 +108,18 @@ export class ProjectImpl {
|
|||
return this._cloneEntries(suite, result, repeatEachIndex, filter) ? result : undefined;
|
||||
}
|
||||
|
||||
private resolveFixtures(testType: TestTypeImpl): FixturesWithLocation[] {
|
||||
private resolveFixtures(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
|
||||
return testType.fixtures.map(f => {
|
||||
if (f instanceof DeclaredFixtures) {
|
||||
const fixtures = this.defines.get(f.testType.test) || {};
|
||||
return { fixtures, location: f.location };
|
||||
const configKeys = new Set(Object.keys(configUse || {}));
|
||||
const resolved = { ...f.fixtures };
|
||||
for (const [key, value] of Object.entries(resolved)) {
|
||||
if (!isFixtureOption(value) || !configKeys.has(key))
|
||||
continue;
|
||||
// Apply override from config file.
|
||||
const override = (configUse as any)[key];
|
||||
(resolved as any)[key] = [override, value[1]];
|
||||
}
|
||||
return f;
|
||||
return { fixtures: resolved, location: f.location };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,20 +22,17 @@ import { Fixtures, FixturesWithLocation, Location, TestType } from './types';
|
|||
import { errorWithLocation, serializeError } from './util';
|
||||
|
||||
const countByFile = new Map<string, number>();
|
||||
|
||||
export class DeclaredFixtures {
|
||||
testType!: TestTypeImpl;
|
||||
location!: Location;
|
||||
}
|
||||
const testTypeSymbol = Symbol('testType');
|
||||
|
||||
export class TestTypeImpl {
|
||||
readonly fixtures: (FixturesWithLocation | DeclaredFixtures)[];
|
||||
readonly fixtures: FixturesWithLocation[];
|
||||
readonly test: TestType<any, any>;
|
||||
|
||||
constructor(fixtures: (FixturesWithLocation | DeclaredFixtures)[]) {
|
||||
constructor(fixtures: FixturesWithLocation[]) {
|
||||
this.fixtures = fixtures;
|
||||
|
||||
const test: any = wrapFunctionWithLocation(this._createTest.bind(this, 'default'));
|
||||
test[testTypeSymbol] = this;
|
||||
test.expect = expect;
|
||||
test.only = wrapFunctionWithLocation(this._createTest.bind(this, 'only'));
|
||||
test.describe = wrapFunctionWithLocation(this._describe.bind(this, 'default'));
|
||||
|
@ -56,7 +53,7 @@ export class TestTypeImpl {
|
|||
test.step = wrapFunctionWithLocation(this._step.bind(this));
|
||||
test.use = wrapFunctionWithLocation(this._use.bind(this));
|
||||
test.extend = wrapFunctionWithLocation(this._extend.bind(this));
|
||||
test.declare = wrapFunctionWithLocation(this._declare.bind(this));
|
||||
test.extendTest = wrapFunctionWithLocation(this._extendTest.bind(this));
|
||||
this.test = test;
|
||||
}
|
||||
|
||||
|
@ -203,16 +200,19 @@ export class TestTypeImpl {
|
|||
}
|
||||
|
||||
private _extend(location: Location, fixtures: Fixtures) {
|
||||
const fixturesWithLocation = { fixtures, location };
|
||||
if ((fixtures as any)[testTypeSymbol])
|
||||
throw new Error(`test.extend() accepts fixtures object, not a test object.\nDid you mean to call test.extendTest()?`);
|
||||
const fixturesWithLocation: FixturesWithLocation = { fixtures, location };
|
||||
return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test;
|
||||
}
|
||||
|
||||
private _declare(location: Location) {
|
||||
const declared = new DeclaredFixtures();
|
||||
declared.location = location;
|
||||
const child = new TestTypeImpl([...this.fixtures, declared]);
|
||||
declared.testType = child;
|
||||
return child.test;
|
||||
private _extendTest(location: Location, test: TestType<any, any>) {
|
||||
const testTypeImpl = (test as any)[testTypeSymbol] as TestTypeImpl;
|
||||
if (!testTypeImpl)
|
||||
throw new Error(`test.extendTest() accepts a single "test" parameter.\nDid you mean to call test.extend() with fixtures instead?`);
|
||||
// Filter out common ancestor fixtures.
|
||||
const newFixtures = testTypeImpl.fixtures.filter(theirs => !this.fixtures.find(ours => ours.fixtures === theirs.fixtures));
|
||||
return new TestTypeImpl([...this.fixtures, ...newFixtures]).test;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ export type ReportSlowTests = { max: number, threshold: number } | null;
|
|||
export type PreserveOutput = 'always' | 'never' | 'failures-only';
|
||||
export type UpdateSnapshots = 'all' | 'none' | 'missing';
|
||||
|
||||
type FixtureDefine<TestArgs extends KeyValue = {}, WorkerArgs extends KeyValue = {}> = { test: TestType<TestArgs, WorkerArgs>, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> };
|
||||
type UseOptions<TestArgs, WorkerArgs> = { [K in keyof WorkerArgs]?: WorkerArgs[K] } & { [K in keyof TestArgs]?: TestArgs[K] };
|
||||
|
||||
type ExpectSettings = {
|
||||
|
@ -319,7 +318,6 @@ interface TestProject {
|
|||
*
|
||||
*/
|
||||
export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
|
||||
define?: FixtureDefine | FixtureDefine[];
|
||||
/**
|
||||
* Options for all tests in this project, for example
|
||||
* [testOptions.browserName](https://playwright.dev/docs/api/class-testoptions#test-options-browser-name). Learn more about
|
||||
|
@ -782,7 +780,6 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
|
|||
* Playwright Test supports running multiple test projects at the same time. See [TestProject] for more information.
|
||||
*/
|
||||
projects?: Project<TestArgs, WorkerArgs>[];
|
||||
define?: FixtureDefine | FixtureDefine[];
|
||||
/**
|
||||
* Global options for all tests, for example
|
||||
* [testOptions.browserName](https://playwright.dev/docs/api/class-testoptions#test-options-browser-name). Learn more about
|
||||
|
@ -2575,8 +2572,74 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
|||
* [expect library documentation](https://jestjs.io/docs/expect) for more details.
|
||||
*/
|
||||
expect: Expect;
|
||||
declare<T extends KeyValue = {}, W extends KeyValue = {}>(): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
/**
|
||||
* Extends the `test` object by defining fixtures and/or options that can be used in the tests.
|
||||
*
|
||||
* First define a fixture and/or an option.
|
||||
*
|
||||
* ```ts
|
||||
* import { test as base } from '@playwright/test';
|
||||
* import { TodoPage } from './todo-page';
|
||||
*
|
||||
* export type Options = { defaultItem: string };
|
||||
*
|
||||
* // Extend basic test by providing a "defaultItem" option and a "todoPage" fixture.
|
||||
* export const test = base.extend<Options & { todoPage: TodoPage }>({
|
||||
* // Define an option and provide a default value.
|
||||
* // We can later override it in the config.
|
||||
* defaultItem: ['Do stuff', { option: true }],
|
||||
*
|
||||
* // Define a fixture. Note that it can use built-in fixture "page"
|
||||
* // and a new option "defaultItem".
|
||||
* todoPage: async ({ page, defaultItem }, use) => {
|
||||
* const todoPage = new TodoPage(page);
|
||||
* await todoPage.goto();
|
||||
* await todoPage.addToDo(defaultItem);
|
||||
* await use(todoPage);
|
||||
* await todoPage.removeAll();
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Then use the fixture in the test.
|
||||
*
|
||||
* ```ts
|
||||
* // example.spec.ts
|
||||
* import { test } from './my-test';
|
||||
*
|
||||
* test('test 1', async ({ todoPage }) => {
|
||||
* await todoPage.addToDo('my todo');
|
||||
* // ...
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Configure the option in config file.
|
||||
*
|
||||
* ```ts
|
||||
* // playwright.config.ts
|
||||
* import { PlaywrightTestConfig } from '@playwright/test';
|
||||
* import { Options } from './my-test';
|
||||
*
|
||||
* const config: PlaywrightTestConfig<Options> = {
|
||||
* projects: [
|
||||
* {
|
||||
* name: 'shopping',
|
||||
* use: { defaultItem: 'Buy milk' },
|
||||
* },
|
||||
* {
|
||||
* name: 'wellbeing',
|
||||
* use: { defaultItem: 'Exercise!' },
|
||||
* },
|
||||
* ]
|
||||
* };
|
||||
* export default config;
|
||||
* ```
|
||||
*
|
||||
* Learn more about [fixtures](https://playwright.dev/docs/test-fixtures) and [parametrizing tests](https://playwright.dev/docs/test-parameterize).
|
||||
* @param fixtures An object containing fixtures and/or options. Learn more about [fixtures format](https://playwright.dev/docs/test-fixtures).
|
||||
*/
|
||||
extend<T, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
extendTest<T, W>(other: TestType<T, W>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
}
|
||||
|
||||
type KeyValue = { [key: string]: any };
|
||||
|
@ -2589,9 +2652,9 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
|||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test' }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
|
@ -16,22 +16,17 @@
|
|||
|
||||
import { test } from '@playwright/test';
|
||||
import { commonFixtures, CommonFixtures } from './commonFixtures';
|
||||
import { serverFixtures, ServerFixtures, ServerWorkerOptions } from './serverFixtures';
|
||||
import { coverageFixtures, CoverageWorkerOptions } from './coverageFixtures';
|
||||
import { platformFixtures, PlatformWorkerFixtures } from './platformFixtures';
|
||||
import { testModeFixtures, TestModeWorkerFixtures } from './testModeFixtures';
|
||||
|
||||
|
||||
export type BaseTestWorkerFixtures = {
|
||||
_snapshotSuffix: string;
|
||||
};
|
||||
import { serverTest } from './serverFixtures';
|
||||
import { coverageTest } from './coverageFixtures';
|
||||
import { platformTest } from './platformFixtures';
|
||||
import { testModeTest } from './testModeFixtures';
|
||||
|
||||
export const baseTest = test
|
||||
.extend<{}, CoverageWorkerOptions>(coverageFixtures as any)
|
||||
.extend<{}, PlatformWorkerFixtures>(platformFixtures)
|
||||
.extend<{}, TestModeWorkerFixtures>(testModeFixtures as any)
|
||||
.extendTest(coverageTest)
|
||||
.extendTest(platformTest)
|
||||
.extendTest(testModeTest)
|
||||
.extend<CommonFixtures>(commonFixtures)
|
||||
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures as any)
|
||||
.extend<{}, BaseTestWorkerFixtures>({
|
||||
.extendTest(serverTest)
|
||||
.extend<{}, { _snapshotSuffix: string }>({
|
||||
_snapshotSuffix: ['', { scope: 'worker' }],
|
||||
});
|
||||
|
|
|
@ -14,18 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Fixtures } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { installCoverageHooks } from './coverage';
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
export type CoverageWorkerOptions = {
|
||||
coverageName?: string;
|
||||
};
|
||||
|
||||
export const coverageFixtures: Fixtures<{}, CoverageWorkerOptions & { __collectCoverage: void }> = {
|
||||
coverageName: [ undefined, { scope: 'worker' } ],
|
||||
|
||||
export const coverageTest = test.extend<{}, { __collectCoverage: void } & CoverageWorkerOptions>({
|
||||
coverageName: [ undefined, { scope: 'worker', option: true } ],
|
||||
__collectCoverage: [ async ({ coverageName }, run, workerInfo) => {
|
||||
if (!coverageName) {
|
||||
await run();
|
||||
|
@ -40,4 +39,4 @@ export const coverageFixtures: Fixtures<{}, CoverageWorkerOptions & { __collectC
|
|||
await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true });
|
||||
await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8');
|
||||
}, { scope: 'worker', auto: true } ],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import { TestModeWorkerFixtures } from './testModeFixtures';
|
||||
import { TestModeWorkerOptions } from './testModeFixtures';
|
||||
import { CoverageWorkerOptions } from './coverageFixtures';
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
@ -38,7 +38,7 @@ const trace = !!process.env.PWTEST_TRACE;
|
|||
|
||||
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||
const testDir = path.join(__dirname, '..');
|
||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerFixtures> = {
|
||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
||||
testDir,
|
||||
outputDir,
|
||||
expect: {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Fixtures } from '@playwright/test';
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
export type PlatformWorkerFixtures = {
|
||||
platform: 'win32' | 'darwin' | 'linux';
|
||||
|
@ -23,9 +23,9 @@ export type PlatformWorkerFixtures = {
|
|||
isLinux: boolean;
|
||||
};
|
||||
|
||||
export const platformFixtures: Fixtures<{}, PlatformWorkerFixtures> = {
|
||||
export const platformTest = test.extend<{}, PlatformWorkerFixtures>({
|
||||
platform: [ process.platform as 'win32' | 'darwin' | 'linux', { scope: 'worker' } ],
|
||||
isWindows: [ process.platform === 'win32', { scope: 'worker' } ],
|
||||
isMac: [ process.platform === 'darwin', { scope: 'worker' } ],
|
||||
isLinux: [ process.platform === 'linux', { scope: 'worker' } ],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures } from '@playwright/test';
|
||||
import { test, Fixtures } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import socks from 'socksv5';
|
||||
import { TestServer } from '../../utils/testserver';
|
||||
|
@ -33,8 +33,8 @@ export type ServerFixtures = {
|
|||
};
|
||||
|
||||
export type ServersInternal = ServerFixtures & { socksServer: socks.SocksServer };
|
||||
export const serverFixtures: Fixtures<ServerFixtures, ServerWorkerOptions & { __servers: ServersInternal }> = {
|
||||
loopback: [ undefined, { scope: 'worker' } ],
|
||||
export const serverFixtures: Fixtures<ServerFixtures, { __servers: ServersInternal } & ServerWorkerOptions> = {
|
||||
loopback: [ undefined, { scope: 'worker', option: true } ],
|
||||
__servers: [ async ({ loopback }, run, workerInfo) => {
|
||||
const assetsPath = path.join(__dirname, '..', 'assets');
|
||||
const cachedPath = path.join(__dirname, '..', 'assets', 'cached');
|
||||
|
@ -110,3 +110,5 @@ export const serverFixtures: Fixtures<ServerFixtures, ServerWorkerOptions & { __
|
|||
await run(__servers.asset);
|
||||
},
|
||||
};
|
||||
|
||||
export const serverTest = test.extend<ServerFixtures, ServerWorkerOptions & { __servers: ServersInternal }>(serverFixtures);
|
||||
|
|
|
@ -14,18 +14,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Fixtures } from '@playwright/test';
|
||||
import { test } from '@playwright/test';
|
||||
import { DefaultTestMode, DriverTestMode, ServiceTestMode, TestModeName } from './testMode';
|
||||
|
||||
export type TestModeWorkerFixtures = {
|
||||
export type TestModeWorkerOptions = {
|
||||
mode: TestModeName;
|
||||
playwright: typeof import('playwright-core');
|
||||
};
|
||||
|
||||
export type TestModeWorkerFixtures = {
|
||||
playwright: typeof import('@playwright/test');
|
||||
toImpl: (rpcObject: any) => any;
|
||||
};
|
||||
|
||||
export const testModeFixtures: Fixtures<{}, TestModeWorkerFixtures> = {
|
||||
mode: [ 'default', { scope: 'worker' } ],
|
||||
|
||||
export const testModeTest = test.extend<{}, TestModeWorkerOptions & TestModeWorkerFixtures>({
|
||||
mode: [ 'default', { scope: 'worker', option: true } ],
|
||||
playwright: [ async ({ mode }, run) => {
|
||||
const testMode = {
|
||||
default: new DefaultTestMode(),
|
||||
|
@ -39,4 +41,4 @@ export const testModeFixtures: Fixtures<{}, TestModeWorkerFixtures> = {
|
|||
}, { scope: 'worker' } ],
|
||||
|
||||
toImpl: [ async ({ playwright }, run) => run((playwright as any)._toImpl), { scope: 'worker' } ],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { TestType } from '@playwright/test';
|
||||
import { PlatformWorkerFixtures } from '../config/platformFixtures';
|
||||
import { TestModeWorkerFixtures } from '../config/testModeFixtures';
|
||||
import { TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures';
|
||||
import { androidTest } from '../android/androidTest';
|
||||
import { browserTest } from '../config/browserTest';
|
||||
import { electronTest } from '../electron/electronTest';
|
||||
|
@ -24,7 +24,7 @@ import { PageTestFixtures, PageWorkerFixtures } from './pageTestApi';
|
|||
import { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
let impl: TestType<PageTestFixtures & ServerFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & ServerWorkerOptions> = browserTest;
|
||||
let impl: TestType<PageTestFixtures & ServerFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & TestModeWorkerOptions & ServerWorkerOptions> = browserTest;
|
||||
|
||||
if (process.env.PWPAGE_IMPL === 'android')
|
||||
impl = androidTest;
|
||||
|
|
|
@ -361,7 +361,7 @@ test('should inerhit use options in projects', async ({ runInlineTest }) => {
|
|||
};
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
const test = pwt.test.extend({ foo: ['', {option:true}], bar: ['', {option: true}] });
|
||||
test('pass', async ({ foo, bar }, testInfo) => {
|
||||
test.expect(foo).toBe('config');
|
||||
test.expect(bar).toBe('project');
|
||||
|
|
|
@ -492,7 +492,8 @@ test('should understand worker fixture params in overrides calling base', async
|
|||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const test1 = pwt.test.extend({
|
||||
param: [ 'param', { scope: 'worker' }],
|
||||
param: [ 'param', { scope: 'worker', option: true }],
|
||||
}).extend({
|
||||
foo: async ({}, test) => await test('foo'),
|
||||
bar: async ({foo}, test) => await test(foo + '-bar'),
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
|||
import rimraf from 'rimraf';
|
||||
import { promisify } from 'util';
|
||||
import { CommonFixtures, commonFixtures } from '../config/commonFixtures';
|
||||
import { serverFixtures, ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
|
||||
import { serverFixtures, serverOptions, ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
|
||||
import { test as base, TestInfo } from './stable-test-runner';
|
||||
|
||||
const removeFolderAsync = promisify(rimraf);
|
||||
|
@ -195,32 +195,35 @@ type Fixtures = {
|
|||
runTSC: (files: Files) => Promise<TSCResult>;
|
||||
};
|
||||
|
||||
const common = base.extend<CommonFixtures>(commonFixtures as any);
|
||||
export const test = common.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures as any).extend<Fixtures>({
|
||||
writeFiles: async ({}, use, testInfo) => {
|
||||
await use(files => writeFiles(testInfo, files));
|
||||
},
|
||||
export const test = base
|
||||
.extend<CommonFixtures>(commonFixtures as any)
|
||||
// TODO: this is a hack until we roll the stable test runner.
|
||||
.extend<ServerFixtures, ServerWorkerOptions>({ ...serverOptions, ...serverFixtures } as any)
|
||||
.extend<Fixtures>({
|
||||
writeFiles: async ({}, use, testInfo) => {
|
||||
await use(files => writeFiles(testInfo, files));
|
||||
},
|
||||
|
||||
runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
await use(async (files: Files, params: Params = {}, env: Env = {}, options: RunOptions = {}) => {
|
||||
const baseDir = await writeFiles(testInfo, files);
|
||||
return await runPlaywrightTest(childProcess, baseDir, params, env, options);
|
||||
});
|
||||
},
|
||||
runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
await use(async (files: Files, params: Params = {}, env: Env = {}, options: RunOptions = {}) => {
|
||||
const baseDir = await writeFiles(testInfo, files);
|
||||
return await runPlaywrightTest(childProcess, baseDir, params, env, options);
|
||||
});
|
||||
},
|
||||
|
||||
runTSC: async ({ childProcess }, use, testInfo) => {
|
||||
await use(async files => {
|
||||
const baseDir = await writeFiles(testInfo, { 'tsconfig.json': JSON.stringify(TSCONFIG), ...files });
|
||||
const tsc = childProcess({
|
||||
command: ['npx', 'tsc', '-p', baseDir],
|
||||
cwd: baseDir,
|
||||
shell: true,
|
||||
});
|
||||
const { exitCode } = await tsc.exited;
|
||||
return { exitCode, output: tsc.output };
|
||||
runTSC: async ({ childProcess }, use, testInfo) => {
|
||||
await use(async files => {
|
||||
const baseDir = await writeFiles(testInfo, { 'tsconfig.json': JSON.stringify(TSCONFIG), ...files });
|
||||
const tsc = childProcess({
|
||||
command: ['npx', 'tsc', '-p', baseDir],
|
||||
cwd: baseDir,
|
||||
shell: true,
|
||||
});
|
||||
const { exitCode } = await tsc.exited;
|
||||
return { exitCode, output: tsc.output };
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const TSCONFIG = {
|
||||
'compilerOptions': {
|
||||
|
|
|
@ -39,34 +39,29 @@ test('test.extend should work', async ({ runInlineTest }) => {
|
|||
};
|
||||
}
|
||||
|
||||
export const base = pwt.test.declare();
|
||||
export const base = pwt.test.extend({
|
||||
suffix: ['', { scope: 'worker', option: true } ],
|
||||
baseWorker: [async ({ suffix }, run) => {
|
||||
global.logs.push('beforeAll-' + suffix);
|
||||
await run();
|
||||
global.logs.push('afterAll-' + suffix);
|
||||
if (suffix.includes('base'))
|
||||
console.log(global.logs.join('\\n'));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
baseTest: async ({ suffix, derivedWorker }, run) => {
|
||||
global.logs.push('beforeEach-' + suffix);
|
||||
await run();
|
||||
global.logs.push('afterEach-' + suffix);
|
||||
},
|
||||
});
|
||||
export const test1 = base.extend(createDerivedFixtures('e1'));
|
||||
export const test2 = base.extend(createDerivedFixtures('e2'));
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
import { base } from './helper';
|
||||
|
||||
function createBaseFixtures(suffix) {
|
||||
return {
|
||||
baseWorker: [async ({}, run) => {
|
||||
global.logs.push('beforeAll-' + suffix);
|
||||
await run();
|
||||
global.logs.push('afterAll-' + suffix);
|
||||
if (suffix.includes('base'))
|
||||
console.log(global.logs.join('\\n'));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
baseTest: async ({ derivedWorker }, run) => {
|
||||
global.logs.push('beforeEach-' + suffix);
|
||||
await run();
|
||||
global.logs.push('afterEach-' + suffix);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { projects: [
|
||||
{ define: { test: base, fixtures: createBaseFixtures('base1') } },
|
||||
{ define: { test: base, fixtures: createBaseFixtures('base2') } },
|
||||
{ use: { suffix: 'base1' } },
|
||||
{ use: { suffix: 'base2' } },
|
||||
] };
|
||||
`,
|
||||
'a.test.ts': `
|
||||
|
@ -126,53 +121,107 @@ test('test.extend should work', async ({ runInlineTest }) => {
|
|||
].join('\n'));
|
||||
});
|
||||
|
||||
test('test.declare should be inserted at the right place', async ({ runInlineTest }) => {
|
||||
const { output, passed } = await runInlineTest({
|
||||
'helper.ts': `
|
||||
const test1 = pwt.test.extend({
|
||||
foo: async ({}, run) => {
|
||||
console.log('before-foo');
|
||||
await run('foo');
|
||||
console.log('after-foo');
|
||||
},
|
||||
});
|
||||
export const test2 = test1.declare<{ bar: string }>();
|
||||
export const test3 = test2.extend({
|
||||
baz: async ({ bar }, run) => {
|
||||
console.log('before-baz');
|
||||
await run(bar + 'baz');
|
||||
console.log('after-baz');
|
||||
},
|
||||
});
|
||||
`,
|
||||
test('config should override options but not fixtures', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import { test2 } from './helper';
|
||||
const fixtures = {
|
||||
bar: async ({ foo }, run) => {
|
||||
console.log('before-bar');
|
||||
await run(foo + 'bar');
|
||||
console.log('after-bar');
|
||||
},
|
||||
};
|
||||
module.exports = {
|
||||
define: { test: test2, fixtures },
|
||||
use: { param: 'config' },
|
||||
};
|
||||
`,
|
||||
'a.test.js': `
|
||||
const { test3 } = require('./helper');
|
||||
test3('should work', async ({baz}) => {
|
||||
console.log('test-' + baz);
|
||||
const test1 = pwt.test.extend({ param: [ 'default', { option: true } ] });
|
||||
test1('default', async ({ param }) => {
|
||||
console.log('default-' + param);
|
||||
});
|
||||
|
||||
const test2 = test1.extend({
|
||||
param: 'extend',
|
||||
});
|
||||
test2('extend', async ({ param }) => {
|
||||
console.log('extend-' + param);
|
||||
});
|
||||
|
||||
const test3 = test1.extend({
|
||||
param: async ({ param }, use) => {
|
||||
await use(param + '-fixture');
|
||||
},
|
||||
});
|
||||
test3('fixture', async ({ param }) => {
|
||||
console.log('fixture-' + param);
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(passed).toBe(1);
|
||||
expect(output).toContain([
|
||||
'before-foo',
|
||||
'before-bar',
|
||||
'before-baz',
|
||||
'test-foobarbaz',
|
||||
'after-baz',
|
||||
'after-bar',
|
||||
'after-foo',
|
||||
].join('\n'));
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(3);
|
||||
expect(result.output).toContain('default-config');
|
||||
expect(result.output).toContain('extend-extend');
|
||||
expect(result.output).toContain('fixture-config-fixture');
|
||||
});
|
||||
|
||||
test('test.extend should be able to merge', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
use: { param: 'from-config' },
|
||||
};
|
||||
`,
|
||||
'a.test.js': `
|
||||
const base = pwt.test.extend({
|
||||
myFixture: 'abc',
|
||||
});
|
||||
|
||||
const test1 = base
|
||||
.extend({
|
||||
param: [ 'default', { option: true } ],
|
||||
fixture1: ({ param }, use) => use(param + '+fixture1'),
|
||||
myFixture: 'override',
|
||||
});
|
||||
|
||||
const test2 = base.extend({
|
||||
fixture2: ({}, use) => use('fixture2'),
|
||||
});
|
||||
|
||||
const test3 = test1.extendTest(test2);
|
||||
|
||||
test3('merged', async ({ param, fixture1, myFixture, fixture2 }) => {
|
||||
console.log('param-' + param);
|
||||
console.log('fixture1-' + fixture1);
|
||||
console.log('myFixture-' + myFixture);
|
||||
console.log('fixture2-' + fixture2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.output).toContain('param-from-config');
|
||||
expect(result.output).toContain('fixture1-from-config+fixture1');
|
||||
expect(result.output).toContain('myFixture-override');
|
||||
expect(result.output).toContain('fixture2-fixture2');
|
||||
});
|
||||
|
||||
test('test.extend should print nice message when used as extendTest', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const test1 = pwt.test.extend({});
|
||||
const test2 = pwt.test.extend({});
|
||||
const test3 = test1.extend(test2);
|
||||
|
||||
test3('test', () => {});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('Did you mean to call test.extendTest()?');
|
||||
});
|
||||
|
||||
test('test.extendTest should print nice message when used as extend', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
const test3 = pwt.test.extendTest({});
|
||||
test3('test', () => {});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('Did you mean to call test.extend() with fixtures instead?');
|
||||
});
|
||||
|
|
|
@ -139,7 +139,7 @@ test('should use options from the config', async ({ runInlineTest }) => {
|
|||
const result = await runInlineTest({
|
||||
'helper.ts': `
|
||||
export const test = pwt.test.extend({
|
||||
foo: 'foo',
|
||||
foo: [ 'foo', { option: true } ],
|
||||
});
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
|
|
|
@ -63,11 +63,17 @@ test('can return anything from hooks', async ({ runTSC }) => {
|
|||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('test.declare should check types', async ({ runTSC }) => {
|
||||
test('test.extend options should check types', async ({ runTSC }) => {
|
||||
const result = await runTSC({
|
||||
'helper.ts': `
|
||||
export type Params = { foo: string };
|
||||
export const test = pwt.test;
|
||||
export const test1 = test.declare<{ foo: string }>();
|
||||
export const test1 = test.extend<Params>({ foo: [ 'foo', { option: true } ] });
|
||||
export const test1b = test.extend<{ bar: string }>({ bar: [ 'bar', { option: true } ] });
|
||||
export const testerror = test.extend<{ foo: string }>({
|
||||
// @ts-expect-error
|
||||
foo: 123
|
||||
});
|
||||
export const test2 = test1.extend<{ bar: number }>({
|
||||
bar: async ({ foo }, run) => { await run(parseInt(foo)); }
|
||||
});
|
||||
|
@ -75,26 +81,31 @@ test('test.declare should check types', async ({ runTSC }) => {
|
|||
// @ts-expect-error
|
||||
bar: async ({ baz }, run) => { await run(42); }
|
||||
});
|
||||
export const test4 = test1.extendTest(test1b);
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
import { test1 } from './helper';
|
||||
const configs: pwt.Config[] = [];
|
||||
import { Params } from './helper';
|
||||
const configs: pwt.Config<Params>[] = [];
|
||||
|
||||
configs.push({});
|
||||
|
||||
configs.push({
|
||||
define: {
|
||||
test: test1,
|
||||
fixtures: { foo: 'foo' }
|
||||
},
|
||||
use: { foo: 'bar' },
|
||||
});
|
||||
|
||||
configs.push({
|
||||
// @ts-expect-error
|
||||
define: { test: {}, fixtures: {} },
|
||||
use: { foo: true },
|
||||
});
|
||||
|
||||
configs.push({
|
||||
// @ts-expect-error
|
||||
use: { unknown: true },
|
||||
});
|
||||
module.exports = configs;
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, test1, test2, test3 } from './helper';
|
||||
import { test, test1, test2, test3, test4 } from './helper';
|
||||
// @ts-expect-error
|
||||
test('my test', async ({ foo }) => {});
|
||||
test1('my test', async ({ foo }) => {});
|
||||
|
@ -103,6 +114,7 @@ test('test.declare should check types', async ({ runTSC }) => {
|
|||
test2('my test', async ({ foo, bar }) => {});
|
||||
// @ts-expect-error
|
||||
test2('my test', async ({ foo, baz }) => {});
|
||||
test4('my test', async ({ foo, bar }) => {});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
|
|
@ -35,7 +35,6 @@ export type ReportSlowTests = { max: number, threshold: number } | null;
|
|||
export type PreserveOutput = 'always' | 'never' | 'failures-only';
|
||||
export type UpdateSnapshots = 'all' | 'none' | 'missing';
|
||||
|
||||
type FixtureDefine<TestArgs extends KeyValue = {}, WorkerArgs extends KeyValue = {}> = { test: TestType<TestArgs, WorkerArgs>, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> };
|
||||
type UseOptions<TestArgs, WorkerArgs> = { [K in keyof WorkerArgs]?: WorkerArgs[K] } & { [K in keyof TestArgs]?: TestArgs[K] };
|
||||
|
||||
type ExpectSettings = {
|
||||
|
@ -62,7 +61,6 @@ interface TestProject {
|
|||
}
|
||||
|
||||
export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
|
||||
define?: FixtureDefine | FixtureDefine[];
|
||||
use?: UseOptions<TestArgs, WorkerArgs>;
|
||||
}
|
||||
|
||||
|
@ -133,7 +131,6 @@ interface TestConfig {
|
|||
|
||||
export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
|
||||
projects?: Project<TestArgs, WorkerArgs>[];
|
||||
define?: FixtureDefine | FixtureDefine[];
|
||||
use?: UseOptions<TestArgs, WorkerArgs>;
|
||||
}
|
||||
|
||||
|
@ -267,8 +264,8 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
|||
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||
step(title: string, body: () => Promise<any>): Promise<any>;
|
||||
expect: Expect;
|
||||
declare<T extends KeyValue = {}, W extends KeyValue = {}>(): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
extend<T, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
extendTest<T, W>(other: TestType<T, W>): TestType<TestArgs & T, WorkerArgs & W>;
|
||||
}
|
||||
|
||||
type KeyValue = { [key: string]: any };
|
||||
|
@ -281,9 +278,9 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
|||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test' }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
Loading…
Reference in New Issue