chore: pass config w/ cli overrides to plugins (#13835)

This commit is contained in:
Pavel Feldman 2022-04-29 12:32:39 -08:00 committed by GitHub
parent dc3f2d26c4
commit 46acf84c68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 55 deletions

View File

@ -20,8 +20,8 @@ import type { Command } from 'playwright-core/lib/utilsBundle';
import fs from 'fs'; import fs from 'fs';
import url from 'url'; import url from 'url';
import path from 'path'; import path from 'path';
import type { Config } from './types';
import { Runner, builtInReporters, kDefaultConfigFiles } from './runner'; import { Runner, builtInReporters, kDefaultConfigFiles } from './runner';
import type { ConfigCLIOverrides } from './runner';
import { stopProfiling, startProfiling } from './profiler'; import { stopProfiling, startProfiling } from './profiler';
import type { FilePatternFilter } from './util'; import type { FilePatternFilter } from './util';
import { showHTMLReport } from './reporters/html'; import { showHTMLReport } from './reporters/html';
@ -132,9 +132,10 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
return; return;
const runner = new Runner(overrides); const runner = new Runner(overrides);
const config = resolvedConfigFile ? await runner.loadConfigFromResolvedFile(resolvedConfigFile) : await runner.loadEmptyConfig(configFileOrDirectory); if (resolvedConfigFile)
if (('projects' in config) && opts.browser) await runner.loadConfigFromResolvedFile(resolvedConfigFile);
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); else
await runner.loadEmptyConfig(configFileOrDirectory);
const filePatternFilter: FilePatternFilter[] = args.map(arg => { const filePatternFilter: FilePatternFilter[] = args.map(arg => {
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
@ -182,7 +183,7 @@ function forceRegExp(pattern: string): RegExp {
return new RegExp(pattern, 'gi'); return new RegExp(pattern, 'gi');
} }
function overridesFromOptions(options: { [key: string]: any }): Config { function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
return { return {
forbidOnly: options.forbidOnly ? true : undefined, forbidOnly: options.forbidOnly ? true : undefined,

View File

@ -15,10 +15,11 @@
*/ */
import type { TestError } from '../types/testReporter'; import type { TestError } from '../types/testReporter';
import type { Config, TestStatus } from './types'; import type { ConfigCLIOverrides } from './runner';
import type { TestStatus } from './types';
export type SerializedLoaderData = { export type SerializedLoaderData = {
overrides: Config; overrides: ConfigCLIOverrides;
configFile: { file: string } | { configDir: string }; configFile: { file: string } | { configDir: string };
}; };
export type WorkerInitParams = { export type WorkerInitParams = {

View File

@ -26,10 +26,10 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import { ProjectImpl } from './project'; import { ProjectImpl } from './project';
import type { BuiltInReporter } from './runner'; import type { BuiltInReporter, ConfigCLIOverrides } from './runner';
import type { Reporter } from '../types/testReporter'; import type { Reporter } from '../types/testReporter';
import { builtInReporters } from './runner'; import { builtInReporters } from './runner';
import { deepCopy, isRegExp } from 'playwright-core/lib/utils'; import { isRegExp } from 'playwright-core/lib/utils';
import { serializeError } from './util'; import { serializeError } from './util';
import { _legacyWebServer } from './plugins/webServerPlugin'; import { _legacyWebServer } from './plugins/webServerPlugin';
import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform'; import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform';
@ -41,14 +41,14 @@ export const defaultTimeout = 30000;
const cachedFileSuites = new Map<string, Suite>(); const cachedFileSuites = new Map<string, Suite>();
export class Loader { export class Loader {
private _configOverrides: Config; private _configCLIOverrides: ConfigCLIOverrides;
private _fullConfig: FullConfigInternal; private _fullConfig: FullConfigInternal;
private _configDir: string = ''; private _configDir: string = '';
private _configFile: string | undefined; private _configFile: string | undefined;
private _projects: ProjectImpl[] = []; private _projects: ProjectImpl[] = [];
constructor(configOverrides?: Config) { constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._configOverrides = configOverrides || {}; this._configCLIOverrides = configCLIOverrides || {};
this._fullConfig = { ...baseFullConfig }; this._fullConfig = { ...baseFullConfig };
} }
@ -61,17 +61,15 @@ export class Loader {
return loader; return loader;
} }
async loadConfigFile(file: string): Promise<Config> { async loadConfigFile(file: string): Promise<FullConfigInternal> {
if (this._configFile) if (this._configFile)
throw new Error('Cannot load two config files'); throw new Error('Cannot load two config files');
let config = await this._requireOrImport(file) as Config; let config = await this._requireOrImport(file) as Config;
if (config && typeof config === 'object' && ('default' in config)) if (config && typeof config === 'object' && ('default' in config))
config = (config as any)['default']; config = (config as any)['default'];
this._configFile = file; this._configFile = file;
const rawConfig = deepCopy({ ...config, plugins: [] });
rawConfig.plugins = config.plugins?.slice() || [] as any;
await this._processConfigObject(config, path.dirname(file)); await this._processConfigObject(config, path.dirname(file));
return rawConfig; return this._fullConfig;
} }
async loadEmptyConfig(configDir: string): Promise<Config> { async loadEmptyConfig(configDir: string): Promise<Config> {
@ -85,14 +83,40 @@ export class Loader {
config.plugins.push(_legacyWebServer(config.webServer)); config.plugins.push(_legacyWebServer(config.webServer));
} }
// 1. Validate data provided in the config file.
validateConfig(this._configFile || '<default config>', config);
// 2. Override settings from CLI.
config.forbidOnly = takeFirst(this._configCLIOverrides.forbidOnly, config.forbidOnly);
config.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, config.fullyParallel);
config.globalTimeout = takeFirst(this._configCLIOverrides.globalTimeout, config.globalTimeout);
config.grep = takeFirst(this._configCLIOverrides.grep, config.grep);
config.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, config.grepInvert);
config.maxFailures = takeFirst(this._configCLIOverrides.maxFailures, config.maxFailures);
config.outputDir = takeFirst(this._configCLIOverrides.outputDir, config.outputDir);
config.quiet = takeFirst(this._configCLIOverrides.quiet, config.quiet);
config.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, config.repeatEach);
config.retries = takeFirst(this._configCLIOverrides.retries, config.retries);
if (this._configCLIOverrides.reporter)
config.reporter = toReporters(this._configCLIOverrides.reporter as any);
config.shard = takeFirst(this._configCLIOverrides.shard, config.shard);
config.timeout = takeFirst(this._configCLIOverrides.timeout, config.timeout);
config.updateSnapshots = takeFirst(this._configCLIOverrides.updateSnapshots, config.updateSnapshots);
if (this._configCLIOverrides.projects && config.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
config.projects = takeFirst(this._configCLIOverrides.projects, config.projects as any);
config.workers = takeFirst(this._configCLIOverrides.workers, config.workers);
config.use = mergeObjects(config.use, this._configCLIOverrides.use);
// 3. Run configure plugins phase.
for (const plugin of config.plugins || []) for (const plugin of config.plugins || [])
await plugin.configure?.(config, configDir); await plugin.configure?.(config, configDir);
// 4. Resolve config.
this._configDir = configDir; this._configDir = configDir;
const packageJsonPath = getPackageJsonPath(configDir); const packageJsonPath = getPackageJsonPath(configDir);
const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined; const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined;
const throwawayArtifactsPath = packageJsonDir || process.cwd(); const throwawayArtifactsPath = packageJsonDir || process.cwd();
validateConfig(this._configFile || '<default config>', config);
// Resolve script hooks relative to the root dir. // Resolve script hooks relative to the root dir.
if (config.globalSetup) if (config.globalSetup)
@ -114,25 +138,25 @@ export class Loader {
this._fullConfig._configDir = configDir; this._fullConfig._configDir = configDir;
this._fullConfig.rootDir = config.testDir || this._configDir; this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir);
this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, config.forbidOnly, baseFullConfig.forbidOnly); this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(this._configOverrides.fullyParallel, config.fullyParallel, baseFullConfig.fullyParallel); this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel);
this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, config.globalSetup, baseFullConfig.globalSetup); this._fullConfig.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup);
this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, config.globalTeardown, baseFullConfig.globalTeardown); this._fullConfig.globalTeardown = takeFirst(config.globalTeardown, baseFullConfig.globalTeardown);
this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, config.globalTimeout, baseFullConfig.globalTimeout); this._fullConfig.globalTimeout = takeFirst(config.globalTimeout, baseFullConfig.globalTimeout);
this._fullConfig.grep = takeFirst(this._configOverrides.grep, config.grep, baseFullConfig.grep); this._fullConfig.grep = takeFirst(config.grep, baseFullConfig.grep);
this._fullConfig.grepInvert = takeFirst(this._configOverrides.grepInvert, config.grepInvert, baseFullConfig.grepInvert); this._fullConfig.grepInvert = takeFirst(config.grepInvert, baseFullConfig.grepInvert);
this._fullConfig.maxFailures = takeFirst(this._configOverrides.maxFailures, config.maxFailures, baseFullConfig.maxFailures); this._fullConfig.maxFailures = takeFirst(config.maxFailures, baseFullConfig.maxFailures);
this._fullConfig.preserveOutput = takeFirst(this._configOverrides.preserveOutput, config.preserveOutput, baseFullConfig.preserveOutput); this._fullConfig.preserveOutput = takeFirst(config.preserveOutput, baseFullConfig.preserveOutput);
this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter as any), resolveReporters(config.reporter, configDir), baseFullConfig.reporter); this._fullConfig.reporter = takeFirst(resolveReporters(config.reporter, configDir), baseFullConfig.reporter);
this._fullConfig.reportSlowTests = takeFirst(this._configOverrides.reportSlowTests, config.reportSlowTests, baseFullConfig.reportSlowTests); this._fullConfig.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests);
this._fullConfig.quiet = takeFirst(this._configOverrides.quiet, config.quiet, baseFullConfig.quiet); this._fullConfig.quiet = takeFirst(config.quiet, baseFullConfig.quiet);
this._fullConfig.shard = takeFirst(this._configOverrides.shard, config.shard, baseFullConfig.shard); this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard);
this._fullConfig.updateSnapshots = takeFirst(this._configOverrides.updateSnapshots, config.updateSnapshots, baseFullConfig.updateSnapshots); this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig.workers = takeFirst(this._configOverrides.workers, config.workers, baseFullConfig.workers); this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(this._configOverrides.webServer, config.webServer, baseFullConfig.webServer); this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
this._fullConfig._plugins = takeFirst(this._configOverrides.plugins, config.plugins, baseFullConfig._plugins); this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins);
const projects: Project[] = this._configOverrides.projects || config.projects || [config]; const projects: Project[] = this._configCLIOverrides.projects || config.projects || [config];
for (const project of projects) for (const project of projects)
this._addProject(config, project, throwawayArtifactsPath); this._addProject(config, project, throwawayArtifactsPath);
this._fullConfig.projects = this._projects.map(p => p.config); this._fullConfig.projects = this._projects.map(p => p.config);
@ -211,7 +235,7 @@ export class Loader {
serialize(): SerializedLoaderData { serialize(): SerializedLoaderData {
return { return {
configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir }, configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir },
overrides: this._configOverrides, overrides: this._configCLIOverrides,
}; };
} }
@ -226,29 +250,37 @@ export class Loader {
if (projectConfig.snapshotDir !== undefined) if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir); projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir);
const testDir = takeFirst(this._configOverrides.testDir, projectConfig.testDir, config.testDir, this._configDir); projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.grep = takeFirst(this._configCLIOverrides.grep, projectConfig.grep);
projectConfig.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, projectConfig.grepInvert);
projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir);
projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach);
projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries);
projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout);
const outputDir = takeFirst(this._configOverrides.outputDir, projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); const testDir = takeFirst(projectConfig.testDir, config.testDir, this._configDir);
const snapshotDir = takeFirst(this._configOverrides.snapshotDir, projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(this._configOverrides.name, projectConfig.name, config.name, ''); const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
const screenshotsDir = takeFirst((this._configOverrides as any).screenshotsDir, (projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name)); const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
const name = takeFirst(projectConfig.name, config.name, '');
const screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
const fullProject: FullProjectInternal = { const fullProject: FullProjectInternal = {
_fullyParallel: takeFirst(this._configOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined), _fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
_expect: takeFirst(this._configOverrides.expect, projectConfig.expect, config.expect, undefined), _expect: takeFirst(projectConfig.expect, config.expect, undefined),
grep: takeFirst(this._configOverrides.grep, projectConfig.grep, config.grep, baseFullConfig.grep), grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep),
grepInvert: takeFirst(this._configOverrides.grepInvert, projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert), grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert),
outputDir, outputDir,
repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, config.repeatEach, 1), repeatEach: takeFirst(projectConfig.repeatEach, config.repeatEach, 1),
retries: takeFirst(this._configOverrides.retries, projectConfig.retries, config.retries, 0), retries: takeFirst(projectConfig.retries, config.retries, 0),
metadata: takeFirst(this._configOverrides.metadata, projectConfig.metadata, config.metadata, undefined), metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name, name,
testDir, testDir,
snapshotDir, snapshotDir,
_screenshotsDir: screenshotsDir, _screenshotsDir: screenshotsDir,
testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, config.testIgnore, []), testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []),
testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).*'), testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).*'),
timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout), timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout),
use: mergeObjects(mergeObjects(config.use, projectConfig.use), this._configOverrides.use), use: mergeObjects(config.use, projectConfig.use),
}; };
this._projects.push(new ProjectImpl(fullProject, this._projects.length)); this._projects.push(new ProjectImpl(fullProject, this._projects.length));
} }

View File

@ -55,17 +55,37 @@ type RunOptions = {
projectFilter?: string[]; projectFilter?: string[];
}; };
export type ConfigCLIOverrides = {
forbidOnly?: boolean;
fullyParallel?: boolean;
globalTimeout?: number;
grep?: RegExp;
grepInvert?: RegExp;
maxFailures?: number;
outputDir?: string;
quiet?: boolean;
repeatEach?: number;
retries?: number;
reporter?: string;
shard?: { current: number, total: number };
timeout?: number;
updateSnapshots?: 'all'|'none'|'missing';
workers?: number;
projects?: { name: string, use?: any }[],
use?: any;
};
export class Runner { export class Runner {
private _loader: Loader; private _loader: Loader;
private _reporter!: Reporter; private _reporter!: Reporter;
private _globalInfo: GlobalInfoImpl; private _globalInfo: GlobalInfoImpl;
constructor(configOverrides?: Config) { constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._loader = new Loader(configOverrides); this._loader = new Loader(configCLIOverrides);
this._globalInfo = new GlobalInfoImpl(this._loader.fullConfig()); this._globalInfo = new GlobalInfoImpl(this._loader.fullConfig());
} }
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<Config> { async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
return await this._loader.loadConfigFile(resolvedConfigFile); return await this._loader.loadConfigFile(resolvedConfigFile);
} }