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;
// 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;
};
class Fixture {
@ -188,7 +190,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];
@ -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 });
// 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) {
let original = previous;
while (original.super)
while (!original.fromConfig && original.super)
original = original.super;
fn = original.fn;
}
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);
}

View File

@ -435,18 +435,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;
}

View File

@ -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 };

View File

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