feat(test runner): test.reset() to reset options to default/config value (#18561)
This commit is contained in:
		
							parent
							
								
									7a9f1b5ee4
								
							
						
					
					
						commit
						6fef227f43
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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); | ||||
|       } | ||||
|  |  | |||
|  | @ -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; | ||||
|   } | ||||
|  |  | |||
|  | @ -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<T>(location: Location, title: string, body: () => Promise<T>): Promise<T> { | ||||
|     const testInfo = currentTestInfo(); | ||||
|     if (!testInfo) | ||||
|  |  | |||
|  | @ -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 }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -2519,6 +2519,29 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue | |||
|    * @param options An object with local options. | ||||
|    */ | ||||
|   use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): 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<TestArgs & WorkerArgs>): void; | ||||
|   /** | ||||
|    * Declares a test step. | ||||
|    * | ||||
|  | @ -2643,6 +2666,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend | |||
| } & { | ||||
|   [K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; | ||||
| }; | ||||
| type ResetOptions<T extends KeyValue> = { [K in keyof T]?: 'config' | 'default' }; | ||||
| 
 | ||||
| type BrowserName = 'chromium' | 'firefox' | 'webkit'; | ||||
| type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>; | ||||
|  |  | |||
|  | @ -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'); | ||||
| }); | ||||
|  |  | |||
|  | @ -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"'); | ||||
| }); | ||||
|  |  | |||
|  | @ -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); | ||||
|       }); | ||||
|  |  | |||
|  | @ -151,6 +151,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue | |||
|   beforeAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void; | ||||
|   afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void; | ||||
|   use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; | ||||
|   reset(options: ResetOptions<TestArgs & WorkerArgs>): void; | ||||
|   step<T>(title: string, body: () => Promise<T>): Promise<T>; | ||||
|   expect: Expect; | ||||
|   extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>; | ||||
|  | @ -171,6 +172,7 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend | |||
| } & { | ||||
|   [K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }]; | ||||
| }; | ||||
| type ResetOptions<T extends KeyValue> = { [K in keyof T]?: 'config' | 'default' }; | ||||
| 
 | ||||
| type BrowserName = 'chromium' | 'firefox' | 'webkit'; | ||||
| type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue