diff --git a/docs/src/test-advanced-js.md b/docs/src/test-advanced-js.md index 6460ac91f2..5f7c3fde3e 100644 --- a/docs/src/test-advanced-js.md +++ b/docs/src/test-advanced-js.md @@ -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({ - // Default value for the version. - version: '1.0', +export const test = base.extend({ + // 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({ }); ``` -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 = { - timeout: 20000, projects: [ { name: 'v1', @@ -489,7 +487,7 @@ const config: PlaywrightTestConfig = { 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 diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index c61d7aaa70..40f5713e56 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -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({ + // 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 = { + 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. diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index f5ad1355f0..e62819fd4e 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -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({ - // 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({ + // 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!'; diff --git a/docs/src/test-parameterize-js.md b/docs/src/test-parameterize-js.md index 777f06b57f..0cb34e56cb 100644 --- a/docs/src/test-parameterize-js.md +++ b/docs/src/test-parameterize-js.md @@ -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. @@ -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({ - // 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 = { - 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({ + // 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); + }, +}); +``` diff --git a/packages/playwright-test/src/fixtures.ts b/packages/playwright-test/src/fixtures.ts index 59bbcf607a..ce2d087cc9 100644 --- a/packages/playwright-test/src/fixtures.ts +++ b/packages/playwright-test/src/fixtures.ts @@ -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; @@ -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' diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index da2e15c609..aab953ef1e 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -31,16 +31,16 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { _setupContextOptionsAndArtifacts: void; _contextFactory: (options?: BrowserContextOptions) => Promise; }; -type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { +type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _browserType: BrowserType; _browserOptions: LaunchOptions; _artifactsDir: () => string; _snapshotSuffix: string; }; -export const test = _baseTest.extend({ - defaultBrowserType: [ 'chromium', { scope: 'worker' } ], - browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ], +export const test = _baseTest.extend({ + 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({ 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({ _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, diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index e281981bfb..293a2c4641 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -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, diff --git a/packages/playwright-test/src/project.ts b/packages/playwright-test/src/project.ts index ce3e0de962..a2a2749973 100644 --- a/packages/playwright-test/src/project.ts +++ b/packages/playwright-test/src/project.ts @@ -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, Fixtures>(); private testTypePools = new Map(); private testPools = new Map(); 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: ``, - 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 }; }); } } diff --git a/packages/playwright-test/src/testType.ts b/packages/playwright-test/src/testType.ts index 75fb3c851f..c3bd30e578 100644 --- a/packages/playwright-test/src/testType.ts +++ b/packages/playwright-test/src/testType.ts @@ -22,20 +22,17 @@ import { Fixtures, FixturesWithLocation, Location, TestType } from './types'; import { errorWithLocation, serializeError } from './util'; const countByFile = new Map(); - -export class DeclaredFixtures { - testType!: TestTypeImpl; - location!: Location; -} +const testTypeSymbol = Symbol('testType'); export class TestTypeImpl { - readonly fixtures: (FixturesWithLocation | DeclaredFixtures)[]; + readonly fixtures: FixturesWithLocation[]; readonly test: TestType; - 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) { + 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; } } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 0d0174b1d5..ea3a1408bd 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -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 = { test: TestType, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> }; type UseOptions = { [K in keyof WorkerArgs]?: WorkerArgs[K] } & { [K in keyof TestArgs]?: TestArgs[K] }; type ExpectSettings = { @@ -319,7 +318,6 @@ interface TestProject { * */ export interface Project 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 extends TestConfig { * Playwright Test supports running multiple test projects at the same time. See [TestProject] for more information. */ projects?: Project[]; - 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(): TestType; + /** + * 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({ + * // 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 = { + * 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(fixtures: Fixtures): TestType; + extendTest(other: TestType): TestType; } type KeyValue = { [key: string]: any }; @@ -2589,9 +2652,9 @@ export type Fixtures | [TestFixtureValue, { scope: 'test' }]; } & { - [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean }]; + [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean }]; } & { - [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean }]; + [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; diff --git a/tests/config/baseTest.ts b/tests/config/baseTest.ts index 0df963aca2..84f27a63bc 100644 --- a/tests/config/baseTest.ts +++ b/tests/config/baseTest.ts @@ -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) - .extend(serverFixtures as any) - .extend<{}, BaseTestWorkerFixtures>({ + .extendTest(serverTest) + .extend<{}, { _snapshotSuffix: string }>({ _snapshotSuffix: ['', { scope: 'worker' }], }); diff --git a/tests/config/coverageFixtures.ts b/tests/config/coverageFixtures.ts index 920ab55e47..3ecc2351d2 100644 --- a/tests/config/coverageFixtures.ts +++ b/tests/config/coverageFixtures.ts @@ -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 } ], -}; +}); diff --git a/tests/config/default.config.ts b/tests/config/default.config.ts index a1a26ee348..e3d9b5c106 100644 --- a/tests/config/default.config.ts +++ b/tests/config/default.config.ts @@ -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 = { +const config: Config = { testDir, outputDir, expect: { diff --git a/tests/config/platformFixtures.ts b/tests/config/platformFixtures.ts index 2d2b01cf3a..96afd4ae2b 100644 --- a/tests/config/platformFixtures.ts +++ b/tests/config/platformFixtures.ts @@ -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' } ], -}; +}); diff --git a/tests/config/serverFixtures.ts b/tests/config/serverFixtures.ts index 4ad5d9216b..cc87258418 100644 --- a/tests/config/serverFixtures.ts +++ b/tests/config/serverFixtures.ts @@ -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 = { - loopback: [ undefined, { scope: 'worker' } ], +export const serverFixtures: Fixtures = { + 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); diff --git a/tests/config/testModeFixtures.ts b/tests/config/testModeFixtures.ts index 6f3f2ce91c..f196320ab6 100644 --- a/tests/config/testModeFixtures.ts +++ b/tests/config/testModeFixtures.ts @@ -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' } ], -}; +}); diff --git a/tests/page/pageTest.ts b/tests/page/pageTest.ts index 3028c7051d..9c4c5d7ba3 100644 --- a/tests/page/pageTest.ts +++ b/tests/page/pageTest.ts @@ -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 = browserTest; +let impl: TestType = browserTest; if (process.env.PWPAGE_IMPL === 'android') impl = androidTest; diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts index d844351638..7ba0045950 100644 --- a/tests/playwright-test/config.spec.ts +++ b/tests/playwright-test/config.spec.ts @@ -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'); diff --git a/tests/playwright-test/fixtures.spec.ts b/tests/playwright-test/fixtures.spec.ts index 2728b325ac..59c8cd1bb7 100644 --- a/tests/playwright-test/fixtures.spec.ts +++ b/tests/playwright-test/fixtures.spec.ts @@ -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'), }); diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 420475742a..d360bf668b 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -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; }; -const common = base.extend(commonFixtures as any); -export const test = common.extend(serverFixtures as any).extend({ - writeFiles: async ({}, use, testInfo) => { - await use(files => writeFiles(testInfo, files)); - }, +export const test = base + .extend(commonFixtures as any) + // TODO: this is a hack until we roll the stable test runner. + .extend({ ...serverOptions, ...serverFixtures } as any) + .extend({ + 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': { diff --git a/tests/playwright-test/test-extend.spec.ts b/tests/playwright-test/test-extend.spec.ts index c6ee17ed50..3b9a377b74 100644 --- a/tests/playwright-test/test-extend.spec.ts +++ b/tests/playwright-test/test-extend.spec.ts @@ -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?'); }); diff --git a/tests/playwright-test/test-use.spec.ts b/tests/playwright-test/test-use.spec.ts index 53df09d122..1892676167 100644 --- a/tests/playwright-test/test-use.spec.ts +++ b/tests/playwright-test/test-use.spec.ts @@ -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': ` diff --git a/tests/playwright-test/types-2.spec.ts b/tests/playwright-test/types-2.spec.ts index c62b335eb3..0d1b6965f0 100644 --- a/tests/playwright-test/types-2.spec.ts +++ b/tests/playwright-test/types-2.spec.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({ 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[] = []; + 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); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 3d9f0c8389..882035a79a 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -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 = { test: TestType, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> }; type UseOptions = { [K in keyof WorkerArgs]?: WorkerArgs[K] } & { [K in keyof TestArgs]?: TestArgs[K] }; type ExpectSettings = { @@ -62,7 +61,6 @@ interface TestProject { } export interface Project extends TestProject { - define?: FixtureDefine | FixtureDefine[]; use?: UseOptions; } @@ -133,7 +131,6 @@ interface TestConfig { export interface Config extends TestConfig { projects?: Project[]; - define?: FixtureDefine | FixtureDefine[]; use?: UseOptions; } @@ -267,8 +264,8 @@ export interface TestType): void; step(title: string, body: () => Promise): Promise; expect: Expect; - declare(): TestType; extend(fixtures: Fixtures): TestType; + extendTest(other: TestType): TestType; } type KeyValue = { [key: string]: any }; @@ -281,9 +278,9 @@ export type Fixtures | [TestFixtureValue, { scope: 'test' }]; } & { - [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean }]; + [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean }]; } & { - [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean }]; + [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit';