diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index 0b841dd2b6..16085e74d7 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -17,6 +17,7 @@ import * as crypto from 'crypto'; import type stream from 'stream'; import * as URL from 'url'; +import v8 from 'v8'; type NameValue = { name: string, @@ -214,3 +215,7 @@ export function streamToString(stream: stream.Readable): Promise { } export const isLikelyNpxGlobal = () => process.argv.length >= 2 && process.argv[1].includes('_npx'); + +export function deepCopy(obj: T): T { + return v8.deserialize(v8.serialize(obj)); +} diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index 8346b5210d..6ff0a76c0e 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -20,18 +20,12 @@ import type { Command } from 'playwright-core/lib/utilsBundle'; import fs from 'fs'; import url from 'url'; import path from 'path'; -import os from 'os'; import type { Config } from './types'; -import type { BuiltInReporter } from './runner'; import { Runner, builtInReporters, kDefaultConfigFiles } from './runner'; import { stopProfiling, startProfiling } from './profiler'; import type { FilePatternFilter } from './util'; import { showHTMLReport } from './reporters/html'; -import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform'; -import { fileIsModule } from './loader'; - -const defaultTimeout = 30000; -const defaultReporter: BuiltInReporter = process.env.CI ? 'dot' : 'list'; +import { baseFullConfig, defaultTimeout, fileIsModule } from './loader'; export function addTestCommand(program: Command) { const command = program.command('test [test-filter...]'); @@ -51,7 +45,7 @@ export function addTestCommand(program: Command) { command.option('--output ', `Folder for output artifacts (default: "test-results")`); command.option('--quiet', `Suppress stdio`); command.option('--repeat-each ', `Run each test N times (default: 1)`); - command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`); + command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${baseFullConfig.reporter[0]}")`); command.option('--retries ', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`); command.option('--shard ', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`); command.option('--project ', `Only run tests from the specified list of projects (default: run all projects)`); @@ -108,24 +102,13 @@ Examples: async function runTests(args: string[], opts: { [key: string]: any }) { await startProfiling(); - const cpus = os.cpus().length; - const workers = hostPlatform.startsWith('mac') && hostPlatform.endsWith('arm64') ? cpus : Math.ceil(cpus / 2); - - const defaultConfig: Config = { - preserveOutput: 'always', - reporter: [ [defaultReporter] ], - reportSlowTests: { max: 5, threshold: 15000 }, - timeout: defaultTimeout, - updateSnapshots: 'missing', - workers, - }; - + const overrides = overridesFromOptions(opts); if (opts.browser) { const browserOpt = opts.browser.toLowerCase(); if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt)) throw new Error(`Unsupported browser "${opts.browser}", must be one of "all", "chromium", "firefox" or "webkit"`); const browserNames = browserOpt === 'all' ? ['chromium', 'firefox', 'webkit'] : [browserOpt]; - defaultConfig.projects = browserNames.map(browserName => { + overrides.projects = browserNames.map(browserName => { return { name: browserName, use: { browserName }, @@ -133,7 +116,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) { }); } - const overrides = overridesFromOptions(opts); if (opts.headed || opts.debug) overrides.use = { headless: false }; if (opts.debug) { @@ -149,7 +131,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { if (restartWithExperimentalTsEsm(resolvedConfigFile)) return; - const runner = new Runner(overrides, { defaultConfig }); + const runner = new Runner(overrides); const config = resolvedConfigFile ? await runner.loadConfigFromResolvedFile(resolvedConfigFile) : await runner.loadEmptyConfig(configFileOrDirectory); if (('projects' in config) && opts.browser) throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); @@ -185,7 +167,7 @@ async function listTestFiles(opts: { [key: string]: any }) { if (restartWithExperimentalTsEsm(resolvedConfigFile)) return; - const runner = new Runner({}, { defaultConfig: {} }); + const runner = new Runner(); await runner.loadConfigFromResolvedFile(resolvedConfigFile); const report = await runner.listTestFiles(resolvedConfigFile, opts.project); write(JSON.stringify(report), () => { diff --git a/packages/playwright-test/src/ipc.ts b/packages/playwright-test/src/ipc.ts index 90881567cd..87c970305c 100644 --- a/packages/playwright-test/src/ipc.ts +++ b/packages/playwright-test/src/ipc.ts @@ -18,7 +18,6 @@ import type { TestError } from '../types/testReporter'; import type { Config, TestStatus } from './types'; export type SerializedLoaderData = { - defaultConfig: Config; overrides: Config; configFile: { file: string } | { configDir: string }; }; diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index d7c051bee5..de09f3b545 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -24,34 +24,36 @@ import type { SerializedLoaderData } from './ipc'; import * as path from 'path'; import * as url from 'url'; import * as fs from 'fs'; +import * as os from 'os'; import { ProjectImpl } from './project'; import type { BuiltInReporter } from './runner'; import type { Reporter } from '../types/testReporter'; import { builtInReporters } from './runner'; -import { isRegExp } from 'playwright-core/lib/utils'; +import { deepCopy, isRegExp } from 'playwright-core/lib/utils'; import { serializeError } from './util'; import { _legacyWebServer } from './plugins/webServerPlugin'; +import { hostPlatform } from 'playwright-core/lib/utils/hostPlatform'; + +export const defaultTimeout = 30000; // To allow multiple loaders in the same process without clearing require cache, // we make these maps global. const cachedFileSuites = new Map(); export class Loader { - private _defaultConfig: Config; private _configOverrides: Config; private _fullConfig: FullConfigInternal; private _configDir: string = ''; private _configFile: string | undefined; private _projects: ProjectImpl[] = []; - constructor(defaultConfig: Config, configOverrides: Config) { - this._defaultConfig = defaultConfig; - this._configOverrides = configOverrides; + constructor(configOverrides?: Config) { + this._configOverrides = configOverrides || {}; this._fullConfig = { ...baseFullConfig }; } static async deserialize(data: SerializedLoaderData): Promise { - const loader = new Loader(data.defaultConfig, data.overrides); + const loader = new Loader(data.overrides); if ('file' in data.configFile) await loader.loadConfigFile(data.configFile.file); else @@ -62,11 +64,12 @@ export class Loader { async loadConfigFile(file: string): Promise { if (this._configFile) throw new Error('Cannot load two config files'); - let config = await this._requireOrImport(file); + let config = await this._requireOrImport(file) as Config; if (config && typeof config === 'object' && ('default' in config)) - config = config['default']; + config = (config as any)['default']; this._configFile = file; - const rawConfig = { ...config }; + const rawConfig = deepCopy({ ...config, plugins: [] }); + rawConfig.plugins = config.plugins?.slice() || [] as any; await this._processConfigObject(config, path.dirname(file)); return rawConfig; } @@ -108,9 +111,6 @@ export class Loader { if (config.webServer) config.webServer.cwd = config.webServer.cwd ? path.resolve(configDir, config.webServer.cwd) : configDir; - const configUse = mergeObjects(this._defaultConfig.use, config.use); - config = mergeObjects(mergeObjects(this._defaultConfig, config), { use: configUse }); - this._fullConfig._configDir = configDir; this._fullConfig.rootDir = config.testDir || this._configDir; this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); @@ -132,7 +132,7 @@ export class Loader { this._fullConfig.webServer = takeFirst(this._configOverrides.webServer, config.webServer, baseFullConfig.webServer); this._fullConfig._plugins = takeFirst(this._configOverrides.plugins, config.plugins, baseFullConfig._plugins); - const projects: Project[] = ('projects' in config) && config.projects !== undefined ? config.projects : [config]; + const projects: Project[] = this._configOverrides.projects || config.projects || [config]; for (const project of projects) this._addProject(config, project, throwawayArtifactsPath); this._fullConfig.projects = this._projects.map(p => p.config); @@ -210,7 +210,6 @@ export class Loader { serialize(): SerializedLoaderData { return { - defaultConfig: this._defaultConfig, configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir }, overrides: this._configOverrides, }; @@ -248,7 +247,7 @@ export class Loader { _screenshotsDir: screenshotsDir, testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, config.testIgnore, []), testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).*'), - timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, config.timeout, 10000), + timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout), use: mergeObjects(mergeObjects(config.use, projectConfig.use), this._configOverrides.use), }; this._projects.push(new ProjectImpl(fullProject, this._projects.length)); @@ -467,7 +466,10 @@ function validateProject(file: string, project: Project, title: string) { } } -const baseFullConfig: FullConfigInternal = { +const cpus = os.cpus().length; +const workers = hostPlatform.startsWith('mac') && hostPlatform.endsWith('arm64') ? cpus : Math.ceil(cpus / 2); + +export const baseFullConfig: FullConfigInternal = { forbidOnly: false, fullyParallel: false, globalSetup: null, @@ -478,14 +480,14 @@ const baseFullConfig: FullConfigInternal = { maxFailures: 0, preserveOutput: 'always', projects: [], - reporter: [ ['list'] ], - reportSlowTests: null, + reporter: [ [process.env.CI ? 'dot' : 'list'] ], + reportSlowTests: { max: 5, threshold: 15000 }, rootDir: path.resolve(process.cwd()), quiet: false, shard: null, updateSnapshots: 'missing', version: require('../package.json').version, - workers: 1, + workers, webServer: null, _globalOutputDir: path.resolve(process.cwd()), _configDir: '', diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 6b88c5ad87..4572faeb9c 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -60,8 +60,8 @@ export class Runner { private _reporter!: Reporter; private _globalInfo: GlobalInfoImpl; - constructor(configOverrides: Config, options: { defaultConfig?: Config } = {}) { - this._loader = new Loader(options.defaultConfig || {}, configOverrides); + constructor(configOverrides?: Config) { + this._loader = new Loader(configOverrides); this._globalInfo = new GlobalInfoImpl(this._loader.fullConfig()); }