diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 0afca6474a..82a467c29a 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1077,6 +1077,51 @@ Test function that takes one or two arguments: an object with fixtures and optio +## method: Test.reset +* since: v1.28 + +Resets options that were set up in the configuration file or with [`method: Test.use`] to their default or config-specified value. + +```js tab=js-js +const { test, expect } = require('@playwright/test'); + +test.reset({ + // Reset storage state to the default empty value. + storageStage: 'default', + + // Reset locale to the value specified in the config file. + locale: 'config', +}); + +test('example', async ({ page }) => { + // ... +}); +``` + +```js tab=js-ts +import { test, expect } from '@playwright/test'; + +test.reset({ + // Reset storage state to the default empty value. + storageStage: 'default', + + // Reset locale to the value specified in the config file. + locale: 'config', +}); + +test('example', async ({ page }) => { + // ... +}); +``` + +### param: Test.reset.fixtures +* since: v1.28 +- `options` <[Object]> + +An object with options set to either `'config'` or `'default'`. + + + ## method: Test.setTimeout * since: v1.10 diff --git a/packages/playwright-test/src/fixtures.ts b/packages/playwright-test/src/fixtures.ts index 2aa919ed5a..5d486f6032 100644 --- a/packages/playwright-test/src/fixtures.ts +++ b/packages/playwright-test/src/fixtures.ts @@ -48,8 +48,13 @@ type FixtureRegistration = { id: string; // A fixture override can use the previous version of the fixture. super?: FixtureRegistration; + // Whether this fixture is an option value set from the config. + fromConfig?: boolean; }; +export const kResetToConfig = Symbol('reset-to-config'); +export const kResetToDefault = Symbol('reset-to-default'); + class Fixture { runner: FixtureRunner; registration: FixtureRegistration; @@ -188,7 +193,7 @@ export class FixturePool { constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool, disallowWorkerFixtures?: boolean) { this.registrations = new Map(parentPool ? parentPool.registrations : []); - for (const { fixtures, location } of fixturesList) { + for (const { fixtures, location, fromConfig } of fixturesList) { for (const entry of Object.entries(fixtures)) { const name = entry[0]; let value = entry[1]; @@ -222,17 +227,30 @@ export class FixturePool { if (options.scope === 'worker' && disallowWorkerFixtures) throw errorWithLocations(`Cannot use({ ${name} }) in a describe group, because it forces a new worker.\nMake it top-level in the test file or put in the configuration file.`, { location, name }); - // Overriding option with "undefined" value means setting it to the default value - // from the original declaration of the option. - if (fn === undefined && options.option && previous) { - let original = previous; - while (original.super) - original = original.super; - fn = original.fn; + if (fn === undefined && options.option) { + // Overriding option with "undefined" value means setting it to the config value. + fn = kResetToConfig; + } + if (fn === kResetToConfig || fn === kResetToDefault) { + // Find the target fixture to copy the reset value from. + // It is either the original definition, or "fromConfig" one. + // + // Note that "reset to config" behaves like "reset to default" + // if no value is set in the config. + let targetFixture = previous; + while (targetFixture && targetFixture.super) { + if (fn === kResetToConfig && targetFixture.fromConfig) + break; + targetFixture = targetFixture.super; + } + if (targetFixture) + fn = targetFixture.fn; + else + fn = undefined; } const deps = fixtureParameterNames(fn, location); - const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, deps, super: previous }; + const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, deps, super: previous, fromConfig }; registrationId(registration); this.registrations.set(name, registration); } diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index 18f3aafb1f..eb3a8bc214 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -440,18 +440,17 @@ class ProjectSuiteBuilder { return testType.fixtures; const result: FixturesWithLocation[] = []; for (const f of testType.fixtures) { + result.push(f); const optionsFromConfig: Fixtures = {}; - const originalFixtures: Fixtures = {}; for (const [key, value] of Object.entries(f.fixtures)) { if (isFixtureOption(value) && configKeys.has(key)) (optionsFromConfig as any)[key] = [(configUse as any)[key], value[1]]; - else - (originalFixtures as any)[key] = value; } - if (Object.entries(optionsFromConfig).length) - result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 } }); - if (Object.entries(originalFixtures).length) - result.push({ fixtures: originalFixtures, location: f.location }); + if (Object.entries(optionsFromConfig).length) { + // Add config options immediately after original option definition, + // so that any test.use() override it. + result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 }, fromConfig: true }); + } } return result; } diff --git a/packages/playwright-test/src/testType.ts b/packages/playwright-test/src/testType.ts index 2dc003f378..f8db73b7c0 100644 --- a/packages/playwright-test/src/testType.ts +++ b/packages/playwright-test/src/testType.ts @@ -15,6 +15,7 @@ */ import { expect } from './expect'; +import { kResetToConfig, kResetToDefault } from './fixtures'; import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals'; import { TestCase, Suite } from './test'; import { wrapFunctionWithLocation } from './transform'; @@ -54,6 +55,7 @@ export class TestTypeImpl { test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this)); test.step = wrapFunctionWithLocation(this._step.bind(this)); test.use = wrapFunctionWithLocation(this._use.bind(this)); + test.reset = wrapFunctionWithLocation(this._reset.bind(this)); test.extend = wrapFunctionWithLocation(this._extend.bind(this)); test._extendTest = wrapFunctionWithLocation(this._extendTest.bind(this)); test.info = () => { @@ -206,6 +208,20 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } + private _reset(location: Location, resets: Fixtures) { + const suite = this._ensureCurrentSuite(location, `test.reset()`); + const fixtures: any = {}; + for (const [key, value] of Object.entries(resets)) { + if (value === 'config') + fixtures[key] = kResetToConfig; + else if (value === 'default') + fixtures[key] = kResetToDefault; + else + throw errorWithLocation(location, `test.reset() supports "config" or "default", got unexpected value "${value}"`); + } + suite._use.push({ fixtures, location }); + } + private async _step(location: Location, title: string, body: () => Promise): Promise { const testInfo = currentTestInfo(); if (!testInfo) diff --git a/packages/playwright-test/src/types.ts b/packages/playwright-test/src/types.ts index c17cec7de0..b383bacf90 100644 --- a/packages/playwright-test/src/types.ts +++ b/packages/playwright-test/src/types.ts @@ -24,6 +24,7 @@ export type { Location } from '../types/testReporter'; export type FixturesWithLocation = { fixtures: Fixtures; location: Location; + fromConfig?: boolean; }; export type Annotation = { type: string, description?: string }; diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index db7912006a..68f76bb1af 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -2519,6 +2519,29 @@ export interface TestType): void; + /** + * Resets options that were set up in the configuration file or with + * [test.use(options)](https://playwright.dev/docs/api/class-test#test-use) to their default or config-specified value. + * + * ```js + * import { test, expect } from '@playwright/test'; + * + * test.reset({ + * // Reset storage state to the default empty value. + * storageStage: 'default', + * + * // Reset locale to the value specified in the config file. + * locale: 'config', + * }); + * + * test('example', async ({ page }) => { + * // ... + * }); + * ``` + * + * @param options An object with options set to either `'config'` or `'default'`. + */ + reset(options: ResetOptions): void; /** * Declares a test step. * @@ -2643,6 +2666,7 @@ export type Fixtures | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; }; +type ResetOptions = { [K in keyof T]?: 'config' | 'default' }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; type BrowserChannel = Exclude; diff --git a/tests/playwright-test/test-extend.spec.ts b/tests/playwright-test/test-extend.spec.ts index 8fd3b0af5a..85704980f2 100644 --- a/tests/playwright-test/test-extend.spec.ts +++ b/tests/playwright-test/test-extend.spec.ts @@ -225,54 +225,3 @@ test('test._extendTest should print nice message when used as extend', async ({ expect(result.passed).toBe(0); expect(result.output).toContain('Did you mean to call test.extend() with fixtures instead?'); }); - -test('test.use() with undefined should not be ignored', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - module.exports = { - use: { option1: 'config' }, - }; - `, - 'a.test.js': ` - const test = pwt.test.extend({ - option1: [ 'default', { option: true } ], - option2: [ 'default', { option: true } ], - }); - test('test1', async ({ option1, option2 }) => { - console.log('test1: option1=' + option1); - console.log('test1: option2=' + option2); - }); - - test.describe('', () => { - test.use({ option1: 'foo', option2: 'foo' }); - test('test2', async ({ option1, option2 }) => { - console.log('test2: option1=' + option1); - console.log('test2: option2=' + option2); - }); - - test.describe('', () => { - test.use({ option1: undefined, option2: undefined }); - test('test3', async ({ option1, option2 }) => { - console.log('test3: option1=' + option1); - console.log('test3: option2=' + option2); - }); - }); - }); - - test.extend({ option1: undefined, option2: undefined })('test4', async ({ option1, option2 }) => { - console.log('test4: option1=' + option1); - console.log('test4: option2=' + option2); - }); - `, - }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(4); - expect(result.output).toContain('test1: option1=config'); - expect(result.output).toContain('test1: option2=default'); - expect(result.output).toContain('test2: option1=foo'); - expect(result.output).toContain('test2: option2=foo'); - expect(result.output).toContain('test3: option1=config'); - expect(result.output).toContain('test3: option2=default'); - expect(result.output).toContain('test4: option1=config'); - expect(result.output).toContain('test4: option2=default'); -}); diff --git a/tests/playwright-test/test-use.spec.ts b/tests/playwright-test/test-use.spec.ts index 9a058c3b7b..1698449ec7 100644 --- a/tests/playwright-test/test-use.spec.ts +++ b/tests/playwright-test/test-use.spec.ts @@ -179,3 +179,109 @@ test('test.use() should throw if called from beforeAll ', async ({ runInlineTest expect(result.output).toContain('Playwright Test did not expect test.use() to be called here'); }); +test('test.use() with undefined should not be ignored', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { option1: 'config' }, + }; + `, + 'a.test.js': ` + const test = pwt.test.extend({ + option1: [ 'default', { option: true } ], + option2: [ 'default', { option: true } ], + }); + test('test1', async ({ option1, option2 }) => { + console.log('test1: option1=' + option1); + console.log('test1: option2=' + option2); + }); + + test.describe('', () => { + test.use({ option1: 'foo', option2: 'foo' }); + test('test2', async ({ option1, option2 }) => { + console.log('test2: option1=' + option1); + console.log('test2: option2=' + option2); + }); + + test.describe('', () => { + test.use({ option1: undefined, option2: undefined }); + test('test3', async ({ option1, option2 }) => { + console.log('test3: option1=' + option1); + console.log('test3: option2=' + option2); + }); + }); + }); + + test.extend({ option1: undefined, option2: undefined })('test4', async ({ option1, option2 }) => { + console.log('test4: option1=' + option1); + console.log('test4: option2=' + option2); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(4); + expect(result.output).toContain('test1: option1=config'); + expect(result.output).toContain('test1: option2=default'); + expect(result.output).toContain('test2: option1=foo'); + expect(result.output).toContain('test2: option2=foo'); + expect(result.output).toContain('test3: option1=config'); + expect(result.output).toContain('test3: option2=default'); + expect(result.output).toContain('test4: option1=config'); + expect(result.output).toContain('test4: option2=default'); +}); + +test('test.reset() should reset to default or config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { option1: 'config-value' }, + }; + `, + 'a.test.js': ` + const test = pwt.test.extend({ + option1: [ 'default-value', { option: true } ], + option2: [ 'default-value', { option: true } ], + }); + + test.use({ option1: 'use-value', option2: 'use-value' }); + test('test1', async ({ option1, option2 }) => { + console.log('test1: option1=' + option1); + console.log('test1: option2=' + option2); + }); + + test.describe(() => { + test.reset({ option1: 'default', option2: 'default' }); + test('test2', async ({ option1, option2 }) => { + console.log('test2: option1=' + option1); + console.log('test2: option2=' + option2); + }); + }); + + test.describe(() => { + test.reset({ option1: 'config', option2: 'config' }); + test('test3', async ({ option1, option2 }) => { + console.log('test3: option1=' + option1); + console.log('test3: option2=' + option2); + }); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.output).toContain('test1: option1=use-value'); + expect(result.output).toContain('test1: option2=use-value'); + expect(result.output).toContain('test2: option1=default-value'); + expect(result.output).toContain('test2: option2=default-value'); + expect(result.output).toContain('test3: option1=config-value'); + expect(result.output).toContain('test3: option2=default-value'); +}); + +test('test.reset() throws for unsupported value', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + pwt.test.reset({ option: 'foo' }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('test.reset() supports "config" or "default", got unexpected value "foo"'); +}); diff --git a/tests/playwright-test/types.spec.ts b/tests/playwright-test/types.spec.ts index 67d9adb715..1149d1fc84 100644 --- a/tests/playwright-test/types.spec.ts +++ b/tests/playwright-test/types.spec.ts @@ -136,6 +136,13 @@ test('should check types of fixtures', async ({ runTSC }) => { // @ts-expect-error test.use({ baz: 'baz' }); + test.reset({ foo: 'default' }); + test.reset({ foo: 'config' }); + // @ts-expect-error + test.reset({ unknown: 'config' }); + // @ts-expect-error + test.reset({ foo: 'unknown' }); + test('my test', async ({ foo, bar }) => { bar += parseInt(foo); }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 982be88d1c..c2e7a6811a 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -151,6 +151,7 @@ export interface TestType Promise | any): void; afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; + reset(options: ResetOptions): void; step(title: string, body: () => Promise): Promise; expect: Expect; extend(fixtures: Fixtures): TestType; @@ -171,6 +172,7 @@ export type Fixtures | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; }; +type ResetOptions = { [K in keyof T]?: 'config' | 'default' }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; type BrowserChannel = Exclude;