fix(test runner): make sure undefined options in config result in default values (#19632)

Note: this keeps existing behavior of `undefined` in `test.use()`
reverting to the config value and not to the original default value.

References #19615.
This commit is contained in:
Dmitry Gozman 2022-12-21 14:34:43 -08:00 committed by GitHub
parent eaae8ebbf3
commit b8848fb499
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 44 additions and 11 deletions

View File

@ -48,6 +48,8 @@ type FixtureRegistration = {
id: string; id: string;
// A fixture override can use the previous version of the fixture. // A fixture override can use the previous version of the fixture.
super?: FixtureRegistration; super?: FixtureRegistration;
// Whether this fixture is an option value set from the config.
fromConfig?: boolean;
}; };
class Fixture { class Fixture {
@ -188,7 +190,7 @@ export class FixturePool {
constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool, disallowWorkerFixtures?: boolean) { constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool, disallowWorkerFixtures?: boolean) {
this.registrations = new Map(parentPool ? parentPool.registrations : []); 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)) { for (const entry of Object.entries(fixtures)) {
const name = entry[0]; const name = entry[0];
let value = entry[1]; let value = entry[1];
@ -223,16 +225,16 @@ export class FixturePool {
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 }); 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 // Overriding option with "undefined" value means setting it to the default value
// from the original declaration of the option. // from the config or from the original declaration of the option.
if (fn === undefined && options.option && previous) { if (fn === undefined && options.option && previous) {
let original = previous; let original = previous;
while (original.super) while (!original.fromConfig && original.super)
original = original.super; original = original.super;
fn = original.fn; fn = original.fn;
} }
const deps = fixtureParameterNames(fn, location); 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); registrationId(registration);
this.registrations.set(name, registration); this.registrations.set(name, registration);
} }

View File

@ -435,18 +435,17 @@ class ProjectSuiteBuilder {
return testType.fixtures; return testType.fixtures;
const result: FixturesWithLocation[] = []; const result: FixturesWithLocation[] = [];
for (const f of testType.fixtures) { for (const f of testType.fixtures) {
result.push(f);
const optionsFromConfig: Fixtures = {}; const optionsFromConfig: Fixtures = {};
const originalFixtures: Fixtures = {};
for (const [key, value] of Object.entries(f.fixtures)) { for (const [key, value] of Object.entries(f.fixtures)) {
if (isFixtureOption(value) && configKeys.has(key)) if (isFixtureOption(value) && configKeys.has(key))
(optionsFromConfig as any)[key] = [(configUse as any)[key], value[1]]; (optionsFromConfig as any)[key] = [(configUse as any)[key], value[1]];
else
(originalFixtures as any)[key] = value;
} }
if (Object.entries(optionsFromConfig).length) if (Object.entries(optionsFromConfig).length) {
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 } }); // Add config options immediately after original option definition,
if (Object.entries(originalFixtures).length) // so that any test.use() override it.
result.push({ fixtures: originalFixtures, location: f.location }); result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 }, fromConfig: true });
}
} }
return result; return result;
} }

View File

@ -24,6 +24,7 @@ export type { Location } from '../types/testReporter';
export type FixturesWithLocation = { export type FixturesWithLocation = {
fixtures: Fixtures; fixtures: Fixtures;
location: Location; location: Location;
fromConfig?: boolean;
}; };
export type Annotation = { type: string, description?: string }; export type Annotation = { type: string, description?: string };

View File

@ -605,6 +605,7 @@ test('should not throw with many fixtures set to undefined', async ({ runInlineT
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { use: { module.exports = { use: {
browserName: undefined,
headless: undefined, headless: undefined,
channel: undefined, channel: undefined,
launchOptions: undefined, launchOptions: undefined,
@ -642,6 +643,7 @@ test('should not throw with many fixtures set to undefined', async ({ runInlineT
'a.spec.ts': ` 'a.spec.ts': `
const { test } = pwt; const { test } = pwt;
test.use({ test.use({
browserName: undefined,
headless: undefined, headless: undefined,
channel: undefined, channel: undefined,
launchOptions: undefined, launchOptions: undefined,
@ -676,6 +678,7 @@ test('should not throw with many fixtures set to undefined', async ({ runInlineT
contextOptions: undefined, contextOptions: undefined,
}); });
test('passes', async ({ page }) => { test('passes', async ({ page }) => {
await page.setContent('text');
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });

View File

@ -276,3 +276,31 @@ test('test.use() with undefined should not be ignored', async ({ runInlineTest }
expect(result.output).toContain('test4: option1=config'); expect(result.output).toContain('test4: option1=config');
expect(result.output).toContain('test4: option2=default'); expect(result.output).toContain('test4: option2=default');
}); });
test('undefined values in config and test.use should be reverted to default', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
use: { option1: undefined, option2: undefined },
};
`,
'a.test.js': `
const test = pwt.test.extend({
option1: [ 'default1', { option: true } ],
option2: [ 'default2', { option: true } ],
option3: [ 'default3', { option: true } ],
});
test.use({ option2: undefined, option3: undefined });
test('my test', async ({ option1, option2, option3 }) => {
console.log('option1=' + option1);
console.log('option2=' + option2);
console.log('option3=' + option3);
});
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.output).toContain('option1=default1');
expect(result.output).toContain('option2=default2');
expect(result.output).toContain('option3=default3');
});