diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index cbd951c5e6..f07f6f3592 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -25,9 +25,8 @@ import { experimentalLoaderOption, fileIsModule } from './util'; import type { TestFileFilter } from './util'; import { createTitleMatcher } from './util'; import { showHTMLReport } from './reporters/html'; -import { baseFullConfig, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; +import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; import type { TraceMode } from './common/types'; -import { builtInReporters } from './runner/reporters'; import type { ConfigCLIOverrides } from './common/ipc'; export function addTestCommands(program: Command) { @@ -151,11 +150,12 @@ async function runTests(args: string[], opts: { [key: string]: any }) { if (restartWithExperimentalTsEsm(resolvedConfigFile)) return; - const runner = new Runner(overrides); + const configLoader = new ConfigLoader(overrides); + const runner = new Runner(configLoader.fullConfig()); if (resolvedConfigFile) - await runner.loadConfigFromResolvedFile(resolvedConfigFile); + await configLoader.loadConfigFile(resolvedConfigFile); else - await runner.loadEmptyConfig(configFileOrDirectory); + await configLoader.loadEmptyConfig(configFileOrDirectory); const testFileFilters: TestFileFilter[] = args.map(arg => { const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); @@ -193,8 +193,9 @@ async function listTestFiles(opts: { [key: string]: any }) { if (restartWithExperimentalTsEsm(resolvedConfigFile)) return; - const runner = new Runner(); - await runner.loadConfigFromResolvedFile(resolvedConfigFile); + const configLoader = new ConfigLoader(); + const runner = new Runner(configLoader.fullConfig()); + await configLoader.loadConfigFile(resolvedConfigFile); const report = await runner.listTestFiles(opts.project); write(JSON.stringify(report), () => { process.exit(0); diff --git a/packages/playwright-test/src/common/DEPS.list b/packages/playwright-test/src/common/DEPS.list index 7a1a8d340d..4e8d9c6e0b 100644 --- a/packages/playwright-test/src/common/DEPS.list +++ b/packages/playwright-test/src/common/DEPS.list @@ -6,6 +6,3 @@ [transform.ts] ../third_party/tsconfig-loader.ts - -[configLoader.ts] -../runner/reporters.ts \ No newline at end of file diff --git a/packages/playwright-test/src/common/configLoader.ts b/packages/playwright-test/src/common/configLoader.ts index 82813f82ca..aeaea2f1ac 100644 --- a/packages/playwright-test/src/common/configLoader.ts +++ b/packages/playwright-test/src/common/configLoader.ts @@ -18,9 +18,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { isRegExp } from 'playwright-core/lib/utils'; -import type { Reporter } from '../../types/testReporter'; import type { ConfigCLIOverrides, SerializedConfig } from './ipc'; -import { builtInReporters, toReporters } from '../runner/reporters'; import { requireOrImport } from './transform'; import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types'; import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util'; @@ -28,14 +26,11 @@ import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util'; export const defaultTimeout = 30000; export class ConfigLoader { - private _configCLIOverrides: ConfigCLIOverrides; private _fullConfig: FullConfigInternal; - private _configDir: string = ''; - private _configFile: string | undefined; constructor(configCLIOverrides?: ConfigCLIOverrides) { - this._configCLIOverrides = configCLIOverrides || {}; this._fullConfig = { ...baseFullConfig }; + this._fullConfig._configCLIOverrides = configCLIOverrides || {}; } static async deserialize(data: SerializedConfig): Promise { @@ -48,11 +43,10 @@ export class ConfigLoader { } async loadConfigFile(file: string): Promise { - if (this._configFile) + if (this._fullConfig.configFile) throw new Error('Cannot load two config files'); - const config = await this._requireOrImportDefaultObject(file) as Config; - this._configFile = file; - await this._processConfigObject(config, path.dirname(file)); + const config = await requireOrImportDefaultObject(file) as Config; + await this._processConfigObject(config, path.dirname(file), file); return this._fullConfig; } @@ -61,35 +55,35 @@ export class ConfigLoader { return {}; } - private async _processConfigObject(config: Config, configDir: string) { + private async _processConfigObject(config: Config, configDir: string, configFile?: string) { // 1. Validate data provided in the config file. - validateConfig(this._configFile || '', config); + validateConfig(configFile || '', 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.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); - config.ignoreSnapshots = takeFirst(this._configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots); - if (this._configCLIOverrides.projects && config.projects) + const configCLIOverrides = this._fullConfig._configCLIOverrides; + config.forbidOnly = takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly); + config.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel); + config.globalTimeout = takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout); + config.maxFailures = takeFirst(configCLIOverrides.maxFailures, config.maxFailures); + config.outputDir = takeFirst(configCLIOverrides.outputDir, config.outputDir); + config.quiet = takeFirst(configCLIOverrides.quiet, config.quiet); + config.repeatEach = takeFirst(configCLIOverrides.repeatEach, config.repeatEach); + config.retries = takeFirst(configCLIOverrides.retries, config.retries); + if (configCLIOverrides.reporter) + config.reporter = toReporters(configCLIOverrides.reporter as any); + config.shard = takeFirst(configCLIOverrides.shard, config.shard); + config.timeout = takeFirst(configCLIOverrides.timeout, config.timeout); + config.updateSnapshots = takeFirst(configCLIOverrides.updateSnapshots, config.updateSnapshots); + config.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots); + if (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); + config.projects = takeFirst(configCLIOverrides.projects, config.projects as any); + config.workers = takeFirst(configCLIOverrides.workers, config.workers); + config.use = mergeObjects(config.use, configCLIOverrides.use); for (const project of config.projects || []) this._applyCLIOverridesToProject(project); // 3. Resolve config. - this._configDir = configDir; const packageJsonPath = getPackageJsonPath(configDir); const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined; const throwawayArtifactsPath = packageJsonDir || process.cwd(); @@ -109,8 +103,8 @@ export class ConfigLoader { this._fullConfig._configDir = configDir; this._fullConfig._storeDir = path.resolve(configDir, '.playwright-store'); - this._fullConfig.configFile = this._configFile; - this._fullConfig.rootDir = config.testDir || this._configDir; + this._fullConfig.configFile = configFile; + this._fullConfig.rootDir = config.testDir || configDir; this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel); @@ -169,46 +163,30 @@ export class ConfigLoader { } } - async loadGlobalHook(file: string): Promise<(config: FullConfigInternal) => any> { - return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), false); - } - - async loadReporter(file: string): Promise Reporter> { - return this._requireOrImportDefaultFunction(path.resolve(this._fullConfig.rootDir, file), true); - } - fullConfig(): FullConfigInternal { return this._fullConfig; } - serializedConfig(): SerializedConfig { - const result: SerializedConfig = { - configFile: this._configFile, - configDir: this._configDir, - configCLIOverrides: this._configCLIOverrides, - }; - return result; - } - private _applyCLIOverridesToProject(projectConfig: Project) { - projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel); - 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); - projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use); + const configCLIOverrides = this._fullConfig._configCLIOverrides; + projectConfig.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel); + projectConfig.outputDir = takeFirst(configCLIOverrides.outputDir, projectConfig.outputDir); + projectConfig.repeatEach = takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach); + projectConfig.retries = takeFirst(configCLIOverrides.retries, projectConfig.retries); + projectConfig.timeout = takeFirst(configCLIOverrides.timeout, projectConfig.timeout); + projectConfig.use = mergeObjects(projectConfig.use, configCLIOverrides.use); } private _resolveProject(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal { // Resolve all config dirs relative to configDir. if (projectConfig.testDir !== undefined) - projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir); + projectConfig.testDir = path.resolve(fullConfig._configDir, projectConfig.testDir); if (projectConfig.outputDir !== undefined) - projectConfig.outputDir = path.resolve(this._configDir, projectConfig.outputDir); + projectConfig.outputDir = path.resolve(fullConfig._configDir, projectConfig.outputDir); if (projectConfig.snapshotDir !== undefined) - projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir); + projectConfig.snapshotDir = path.resolve(fullConfig._configDir, projectConfig.snapshotDir); - const testDir = takeFirst(projectConfig.testDir, config.testDir, this._configDir); + const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig._configDir); const respectGitIgnore = !projectConfig.testDir && !config.testDir; const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); @@ -239,22 +217,13 @@ export class ConfigLoader { use: mergeObjects(config.use, projectConfig.use), }; } +} - private async _requireOrImportDefaultFunction(file: string, expectConstructor: boolean) { - let func = await requireOrImport(file); - if (func && typeof func === 'object' && ('default' in func)) - func = func['default']; - if (typeof func !== 'function') - throw errorWithFile(file, `file must export a single ${expectConstructor ? 'class' : 'function'}.`); - return func; - } - - private async _requireOrImportDefaultObject(file: string) { - let object = await requireOrImport(file); - if (object && typeof object === 'object' && ('default' in object)) - object = object['default']; - return object; - } +async function requireOrImportDefaultObject(file: string) { + let object = await requireOrImport(file); + if (object && typeof object === 'object' && ('default' in object)) + object = object['default']; + return object; } function takeFirst(...args: (T | undefined)[]): T { @@ -462,6 +431,7 @@ export const baseFullConfig: FullConfigInternal = { _webServers: [], _globalOutputDir: path.resolve(process.cwd()), _configDir: '', + _configCLIOverrides: {}, _storeDir: '', _maxConcurrentTestGroups: 0, _ignoreSnapshots: false, @@ -513,3 +483,14 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null return configFile!; } } + +export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html'] as const; +export type BuiltInReporter = typeof builtInReporters[number]; + +export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | undefined): ReporterDescription[] | undefined { + if (!reporters) + return; + if (typeof reporters === 'string') + return [[reporters]]; + return reporters; +} diff --git a/packages/playwright-test/src/common/globals.ts b/packages/playwright-test/src/common/globals.ts index fb5119d613..d39c58f373 100644 --- a/packages/playwright-test/src/common/globals.ts +++ b/packages/playwright-test/src/common/globals.ts @@ -32,3 +32,13 @@ export function setCurrentlyLoadingFileSuite(suite: Suite | undefined) { export function currentlyLoadingFileSuite() { return currentFileSuite; } + +export function currentExpectTimeout(options: { timeout?: number }) { + const testInfo = currentTestInfo(); + if (options.timeout !== undefined) + return options.timeout; + let defaultExpectTimeout = testInfo?.project._expect?.timeout; + if (typeof defaultExpectTimeout === 'undefined') + defaultExpectTimeout = 5000; + return defaultExpectTimeout; +} diff --git a/packages/playwright-test/src/common/ipc.ts b/packages/playwright-test/src/common/ipc.ts index 0bbef0fe77..69111868b4 100644 --- a/packages/playwright-test/src/common/ipc.ts +++ b/packages/playwright-test/src/common/ipc.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TestInfoError, TestStatus } from './types'; +import type { FullConfigInternal, TestInfoError, TestStatus } from './types'; export type ConfigCLIOverrides = { forbidOnly?: boolean; @@ -120,3 +120,12 @@ export type TestOutputPayload = { export type TeardownErrorsPayload = { fatalErrors: TestInfoError[]; }; + +export function serializeConfig(config: FullConfigInternal): SerializedConfig { + const result: SerializedConfig = { + configFile: config.configFile, + configDir: config._configDir, + configCLIOverrides: config._configCLIOverrides, + }; + return result; +} diff --git a/packages/playwright-test/src/common/types.ts b/packages/playwright-test/src/common/types.ts index 2711a6ea3d..cc539db0ed 100644 --- a/packages/playwright-test/src/common/types.ts +++ b/packages/playwright-test/src/common/types.ts @@ -16,6 +16,7 @@ import type { Fixtures, TestInfoError, Project } from '../../types/test'; import type { Location } from '../../types/testReporter'; +import type { ConfigCLIOverrides } from './ipc'; import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types'; export * from '../../types/test'; export type { Location } from '../../types/testReporter'; @@ -44,6 +45,7 @@ export interface TestStepInternal { export interface FullConfigInternal extends FullConfigPublic { _globalOutputDir: string; _configDir: string; + _configCLIOverrides: ConfigCLIOverrides; _storeDir: string; _maxConcurrentTestGroups: number; _ignoreSnapshots: boolean; diff --git a/packages/playwright-test/src/loader/loaderMain.ts b/packages/playwright-test/src/loader/loaderMain.ts index 844578c8b7..07792377f9 100644 --- a/packages/playwright-test/src/loader/loaderMain.ts +++ b/packages/playwright-test/src/loader/loaderMain.ts @@ -19,26 +19,27 @@ import { ConfigLoader } from '../common/configLoader'; import { ProcessRunner } from '../common/process'; import { loadTestFilesInProcess } from '../common/testLoader'; import type { LoadError } from '../common/fixtures'; +import type { FullConfigInternal } from '../common/types'; export class LoaderMain extends ProcessRunner { - private _config: SerializedConfig; - private _configLoaderPromise: Promise | undefined; + private _serializedConfig: SerializedConfig; + private _configPromise: Promise | undefined; - constructor(config: SerializedConfig) { + constructor(serializedConfig: SerializedConfig) { super(); - this._config = config; + this._serializedConfig = serializedConfig; } - private _configLoader(): Promise { - if (!this._configLoaderPromise) - this._configLoaderPromise = ConfigLoader.deserialize(this._config); - return this._configLoaderPromise; + private _config(): Promise { + if (!this._configPromise) + this._configPromise = ConfigLoader.deserialize(this._serializedConfig).then(configLoader => configLoader.fullConfig()); + return this._configPromise; } async loadTestFiles(params: { files: string[] }) { const loadErrors: LoadError[] = []; - const configLoader = await this._configLoader(); - const rootSuite = await loadTestFilesInProcess(configLoader.fullConfig(), params.files, loadErrors); + const config = await this._config(); + const rootSuite = await loadTestFilesInProcess(config, params.files, loadErrors); return { rootSuite: rootSuite._deepSerialize(), loadErrors }; } } diff --git a/packages/playwright-test/src/matchers/expect.ts b/packages/playwright-test/src/matchers/expect.ts index 070fe5217c..f484add649 100644 --- a/packages/playwright-test/src/matchers/expect.ts +++ b/packages/playwright-test/src/matchers/expect.ts @@ -42,8 +42,8 @@ import { } from './matchers'; import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot'; import type { Expect } from '../common/types'; -import { currentTestInfo } from '../common/globals'; -import { serializeError, captureStackTrace, currentExpectTimeout } from '../util'; +import { currentTestInfo, currentExpectTimeout } from '../common/globals'; +import { serializeError, captureStackTrace } from '../util'; import { expect as expectLibrary, INVERTED_COLOR, diff --git a/packages/playwright-test/src/matchers/toBeTruthy.ts b/packages/playwright-test/src/matchers/toBeTruthy.ts index 6ba77fa6e4..dfc1b617b8 100644 --- a/packages/playwright-test/src/matchers/toBeTruthy.ts +++ b/packages/playwright-test/src/matchers/toBeTruthy.ts @@ -16,8 +16,9 @@ import type { Expect } from '../common/types'; import type { ParsedStackTrace } from '../util'; -import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util'; +import { expectTypes, callLogText, captureStackTrace } from '../util'; import { matcherHint } from './matcherHint'; +import { currentExpectTimeout } from '../common/globals'; export async function toBeTruthy( this: ReturnType, diff --git a/packages/playwright-test/src/matchers/toEqual.ts b/packages/playwright-test/src/matchers/toEqual.ts index 7df1fccc37..300dc20b58 100644 --- a/packages/playwright-test/src/matchers/toEqual.ts +++ b/packages/playwright-test/src/matchers/toEqual.ts @@ -16,10 +16,11 @@ import type { Expect } from '../common/types'; import { expectTypes } from '../util'; -import { callLogText, currentExpectTimeout } from '../util'; +import { callLogText } from '../util'; import type { ParsedStackTrace } from 'playwright-core/lib/utils'; import { captureStackTrace } from 'playwright-core/lib/utils'; import { matcherHint } from './matcherHint'; +import { currentExpectTimeout } from '../common/globals'; // Omit colon and one or more spaces, so can call getLabelPrinter. const EXPECTED_LABEL = 'Expected'; diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index 53173e6592..7f40079f4e 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -18,13 +18,13 @@ import type { Locator, Page } from 'playwright-core'; import type { Page as PageEx } from 'playwright-core/lib/client/page'; import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator'; import type { Expect } from '../common/types'; -import { currentTestInfo } from '../common/globals'; +import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; import { getComparator } from 'playwright-core/lib/utils'; import type { PageScreenshotOptions } from 'playwright-core/types/types'; import { addSuffixToFilePath, serializeError, sanitizeForFilePath, - trimLongString, callLogText, currentExpectTimeout, + trimLongString, callLogText, expectTypes, captureStackTrace } from '../util'; import { colors } from 'playwright-core/lib/utilsBundle'; import fs from 'fs'; diff --git a/packages/playwright-test/src/matchers/toMatchText.ts b/packages/playwright-test/src/matchers/toMatchText.ts index e0e0de879f..fad6f41da1 100644 --- a/packages/playwright-test/src/matchers/toMatchText.ts +++ b/packages/playwright-test/src/matchers/toMatchText.ts @@ -19,12 +19,13 @@ import type { ExpectedTextValue } from '@protocol/channels'; import { isRegExp, isString } from 'playwright-core/lib/utils'; import type { Expect } from '../common/types'; import type { ParsedStackTrace } from '../util'; -import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util'; +import { expectTypes, callLogText, captureStackTrace } from '../util'; import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; import { matcherHint } from './matcherHint'; +import { currentExpectTimeout } from '../common/globals'; export async function toMatchText( this: ReturnType, diff --git a/packages/playwright-test/src/runner/dispatcher.ts b/packages/playwright-test/src/runner/dispatcher.ts index c3a759b23c..995467c790 100644 --- a/packages/playwright-test/src/runner/dispatcher.ts +++ b/packages/playwright-test/src/runner/dispatcher.ts @@ -15,14 +15,15 @@ */ import type { TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, RunPayload, SerializedConfig } from '../common/ipc'; +import { serializeConfig } from '../common/ipc'; import type { TestResult, Reporter, TestStep, TestError } from '../../types/testReporter'; import type { Suite } from '../common/test'; -import type { ConfigLoader } from '../common/configLoader'; import type { ProcessExitData } from './processHost'; import type { TestCase } from '../common/test'; import { ManualPromise } from 'playwright-core/lib/utils'; import { WorkerHost } from './workerHost'; import type { TestGroup } from './testGroups'; +import type { FullConfigInternal } from '../common/types'; type TestResultData = { result: TestResult; @@ -42,13 +43,13 @@ export class Dispatcher { private _isStopped = false; private _testById = new Map(); - private _configLoader: ConfigLoader; + private _config: FullConfigInternal; private _reporter: Reporter; private _hasWorkerErrors = false; private _failureCount = 0; - constructor(configLoader: ConfigLoader, testGroups: TestGroup[], reporter: Reporter) { - this._configLoader = configLoader; + constructor(config: FullConfigInternal, testGroups: TestGroup[], reporter: Reporter) { + this._config = config; this._reporter = reporter; this._queue = testGroups; for (const group of testGroups) { @@ -125,7 +126,7 @@ export class Dispatcher { // 2. Start the worker if it is down. if (!worker) { - worker = this._createWorker(job, index, this._configLoader.serializedConfig()); + worker = this._createWorker(job, index, serializeConfig(this._config)); this._workerSlots[index].worker = worker; worker.on('exit', () => this._workerSlots[index].worker = undefined); await worker.start(); @@ -169,7 +170,7 @@ export class Dispatcher { async run() { this._workerSlots = []; // 1. Allocate workers. - for (let i = 0; i < this._configLoader.fullConfig().workers; i++) + for (let i = 0; i < this._config.workers; i++) this._workerSlots.push({ busy: false }); // 2. Schedule enough jobs. for (let i = 0; i < this._workerSlots.length; i++) @@ -488,7 +489,7 @@ export class Dispatcher { } private _hasReachedMaxFailures() { - const maxFailures = this._configLoader.fullConfig().maxFailures; + const maxFailures = this._config.maxFailures; return maxFailures > 0 && this._failureCount >= maxFailures; } @@ -496,7 +497,7 @@ export class Dispatcher { if (result.status !== 'skipped' && result.status !== test.expectedStatus) ++this._failureCount; this._reporter.onTestEnd?.(test, result); - const maxFailures = this._configLoader.fullConfig().maxFailures; + const maxFailures = this._config.maxFailures; if (maxFailures && this._failureCount === maxFailures) this.stop().catch(e => {}); } diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index e1cbedb454..cf190d002f 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -15,8 +15,7 @@ */ import path from 'path'; -import type { TestError } from '../../types/testReporter'; -import type { ConfigLoader } from '../common/configLoader'; +import type { Reporter, TestError } from '../../types/testReporter'; import type { LoadError } from '../common/fixtures'; import { LoaderHost } from './loaderHost'; import type { Multiplexer } from '../reporters/multiplexer'; @@ -24,9 +23,12 @@ import { createRootSuite, filterOnly, filterSuite } from '../common/suiteUtils'; import type { Suite, TestCase } from '../common/test'; import { loadTestFilesInProcess } from '../common/testLoader'; import type { FullConfigInternal } from '../common/types'; +import { errorWithFile } from '../util'; import type { Matcher, TestFileFilter } from '../util'; import { createFileMatcher } from '../util'; import { collectFilesForProjects, collectProjects } from './projectUtils'; +import { requireOrImport } from '../common/transform'; +import { serializeConfig } from '../common/ipc'; type LoadOptions = { listOnly: boolean; @@ -36,8 +38,7 @@ type LoadOptions = { passWithNoTests?: boolean; }; -export async function loadAllTests(configLoader: ConfigLoader, reporter: Multiplexer, options: LoadOptions, errors: TestError[]): Promise { - const config = configLoader.fullConfig(); +export async function loadAllTests(config: FullConfigInternal, reporter: Multiplexer, options: LoadOptions, errors: TestError[]): Promise { const projects = collectProjects(config, options.projectFilter); const filesByProject = await collectFilesForProjects(projects, options.testFileFilters); const allTestFiles = new Set(); @@ -45,7 +46,7 @@ export async function loadAllTests(configLoader: ConfigLoader, reporter: Multipl files.forEach(file => allTestFiles.add(file)); // Load all tests. - const preprocessRoot = await loadTests(configLoader, reporter, allTestFiles, errors); + const preprocessRoot = await loadTests(config, reporter, allTestFiles, errors); // Complain about duplicate titles. errors.push(...createDuplicateTitlesErrors(config, preprocessRoot)); @@ -67,10 +68,10 @@ export async function loadAllTests(configLoader: ConfigLoader, reporter: Multipl return await createRootSuite(preprocessRoot, options.testTitleMatcher, filesByProject); } -async function loadTests(configLoader: ConfigLoader, reporter: Multiplexer, testFiles: Set, errors: TestError[]): Promise { +async function loadTests(config: FullConfigInternal, reporter: Multiplexer, testFiles: Set, errors: TestError[]): Promise { if (process.env.PW_TEST_OOP_LOADER) { const loaderHost = new LoaderHost(); - await loaderHost.start(configLoader.serializedConfig()); + await loaderHost.start(serializeConfig(config)); try { return await loaderHost.loadTestFiles([...testFiles], reporter); } finally { @@ -79,7 +80,7 @@ async function loadTests(configLoader: ConfigLoader, reporter: Multiplexer, test } const loadErrors: LoadError[] = []; try { - return await loadTestFilesInProcess(configLoader.fullConfig(), [...testFiles], loadErrors); + return await loadTestFilesInProcess(config, [...testFiles], loadErrors); } finally { errors.push(...loadErrors); } @@ -140,3 +141,20 @@ function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) { return ''; return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`; } + +async function requireOrImportDefaultFunction(file: string, expectConstructor: boolean) { + let func = await requireOrImport(file); + if (func && typeof func === 'object' && ('default' in func)) + func = func['default']; + if (typeof func !== 'function') + throw errorWithFile(file, `file must export a single ${expectConstructor ? 'class' : 'function'}.`); + return func; +} + +export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfigInternal) => any> { + return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), false); +} + +export function loadReporter(config: FullConfigInternal, file: string): Promise Reporter> { + return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), true); +} diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts index dc1e4bfb18..17c092dea3 100644 --- a/packages/playwright-test/src/runner/reporters.ts +++ b/packages/playwright-test/src/runner/reporters.ts @@ -16,7 +16,6 @@ import path from 'path'; import type { Reporter, TestError } from '../../types/testReporter'; -import type { ConfigLoader } from '../common/configLoader'; import { formatError } from '../reporters/base'; import DotReporter from '../reporters/dot'; import EmptyReporter from '../reporters/empty'; @@ -28,9 +27,11 @@ import LineReporter from '../reporters/line'; import ListReporter from '../reporters/list'; import { Multiplexer } from '../reporters/multiplexer'; import type { Suite } from '../common/test'; -import type { FullConfigInternal, ReporterDescription } from '../common/types'; +import type { FullConfigInternal } from '../common/types'; +import { loadReporter } from './loadUtils'; +import type { BuiltInReporter } from '../common/configLoader'; -export async function createReporter(configLoader: ConfigLoader, list: boolean) { +export async function createReporter(config: FullConfigInternal, list: boolean) { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { dot: list ? ListModeReporter : DotReporter, line: list ? ListModeReporter : LineReporter, @@ -42,17 +43,17 @@ export async function createReporter(configLoader: ConfigLoader, list: boolean) html: HtmlReporter, }; const reporters: Reporter[] = []; - for (const r of configLoader.fullConfig().reporter) { + for (const r of config.reporter) { const [name, arg] = r; if (name in defaultReporters) { reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg)); } else { - const reporterConstructor = await configLoader.loadReporter(name); + const reporterConstructor = await loadReporter(config, name); reporters.push(new reporterConstructor(arg)); } } if (process.env.PW_TEST_REPORTER) { - const reporterConstructor = await configLoader.loadReporter(process.env.PW_TEST_REPORTER); + const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER); reporters.push(new reporterConstructor()); } @@ -98,14 +99,3 @@ export class ListModeReporter implements Reporter { console.error('\n' + formatError(this.config, error, false).message); } } - -export function toReporters(reporters: BuiltInReporter | ReporterDescription[] | undefined): ReporterDescription[] | undefined { - if (!reporters) - return; - if (typeof reporters === 'string') - return [[reporters]]; - return reporters; -} - -export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html'] as const; -export type BuiltInReporter = typeof builtInReporters[number]; diff --git a/packages/playwright-test/src/runner/runner.ts b/packages/playwright-test/src/runner/runner.ts index 40bc913cce..73f5ce091a 100644 --- a/packages/playwright-test/src/runner/runner.ts +++ b/packages/playwright-test/src/runner/runner.ts @@ -17,7 +17,6 @@ import { monotonicTime } from 'playwright-core/lib/utils'; import type { FullResult } from '../../types/testReporter'; -import { ConfigLoader } from '../common/configLoader'; import type { TestRunnerPlugin } from '../plugins'; import { setRunnerToAddPluginsTo } from '../plugins'; import { dockerPlugin } from '../plugins/dockerPlugin'; @@ -26,9 +25,8 @@ import { collectFilesForProjects, collectProjects } from './projectUtils'; import { createReporter } from './reporters'; import { createTaskRunner } from './tasks'; import type { TaskRunnerState } from './tasks'; -import type { Config, FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/types'; import type { Matcher, TestFileFilter } from '../util'; -import type { ConfigCLIOverrides } from '../common/ipc'; export type RunOptions = { listOnly: boolean; @@ -39,11 +37,11 @@ export type RunOptions = { }; export class Runner { - private _configLoader: ConfigLoader; + private _config: FullConfigInternal; private _plugins: TestRunnerPlugin[] = []; - constructor(configCLIOverrides?: ConfigCLIOverrides) { - this._configLoader = new ConfigLoader(configCLIOverrides); + constructor(config: FullConfigInternal) { + this._config = config; setRunnerToAddPluginsTo(this); } @@ -51,16 +49,8 @@ export class Runner { this._plugins.push(plugin); } - async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise { - return await this._configLoader.loadConfigFile(resolvedConfigFile); - } - - loadEmptyConfig(configFileOrDirectory: string): Promise { - return this._configLoader.loadEmptyConfig(configFileOrDirectory); - } - async listTestFiles(projectNames: string[] | undefined): Promise { - const projects = collectProjects(this._configLoader.fullConfig(), projectNames); + const projects = collectProjects(this._config, projectNames); const filesByProject = await collectFilesForProjects(projects, []); const report: any = { projects: [] @@ -75,7 +65,7 @@ export class Runner { } async runAllTests(options: RunOptions): Promise { - const config = this._configLoader.fullConfig(); + const config = this._config; const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0; // Legacy webServer support. @@ -83,12 +73,11 @@ export class Runner { // Docker support. this._plugins.push(dockerPlugin); - const reporter = await createReporter(this._configLoader, options.listOnly); + const reporter = await createReporter(config, options.listOnly); const taskRunner = createTaskRunner(config, reporter, this._plugins, options); const context: TaskRunnerState = { config, - configLoader: this._configLoader, options, reporter, }; diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index 56e2a1ff30..3df8be66a4 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -18,7 +18,6 @@ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; import { colors, rimraf } from 'playwright-core/lib/utilsBundle'; -import type { ConfigLoader } from '../common/configLoader'; import { Dispatcher } from './dispatcher'; import type { TestRunnerPlugin } from '../plugins'; import type { Multiplexer } from '../reporters/multiplexer'; @@ -28,7 +27,7 @@ import type { Task } from './taskRunner'; import { TaskRunner } from './taskRunner'; import type { Suite } from '../common/test'; import type { FullConfigInternal } from '../common/types'; -import { loadAllTests } from './loadUtils'; +import { loadAllTests, loadGlobalHook } from './loadUtils'; import type { Matcher, TestFileFilter } from '../util'; const removeFolderAsync = promisify(rimraf); @@ -46,7 +45,6 @@ export type TaskRunnerState = { options: TaskRunnerOptions; reporter: Multiplexer; config: FullConfigInternal; - configLoader: ConfigLoader; rootSuite?: Suite; testGroups?: TestGroup[]; dispatcher?: Dispatcher; @@ -90,10 +88,10 @@ export function createPluginSetupTask(plugin: TestRunnerPlugin): Task { - return async ({ config, configLoader }) => { - const setupHook = config.globalSetup ? await configLoader.loadGlobalHook(config.globalSetup) : undefined; - const teardownHook = config.globalTeardown ? await configLoader.loadGlobalHook(config.globalTeardown) : undefined; - const globalSetupResult = setupHook ? await setupHook(configLoader.fullConfig()) : undefined; + return async ({ config }) => { + const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined; + const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined; + const globalSetupResult = setupHook ? await setupHook(config) : undefined; return async () => { if (typeof globalSetupResult === 'function') await globalSetupResult(); @@ -104,7 +102,7 @@ export function createGlobalSetupTask(): Task { export function createSetupWorkersTask(): Task { return async params => { - const { config, configLoader, testGroups, reporter } = params; + const { config, testGroups, reporter } = params; if (config._ignoreSnapshots) { reporter.onStdOut(colors.dim([ 'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:', @@ -114,7 +112,7 @@ export function createSetupWorkersTask(): Task { ].join('\n'))); } - const dispatcher = new Dispatcher(configLoader, testGroups!, reporter); + const dispatcher = new Dispatcher(config, testGroups!, reporter); params.dispatcher = dispatcher; return async () => { await dispatcher.stop(); @@ -123,8 +121,7 @@ export function createSetupWorkersTask(): Task { } export function createRemoveOutputDirsTask(): Task { - return async ({ options, configLoader }) => { - const config = configLoader.fullConfig(); + return async ({ config, options }) => { const outputDirs = new Set(); for (const p of config.projects) { if (!options.projectFilter || options.projectFilter.includes(p.name)) @@ -147,8 +144,8 @@ export function createRemoveOutputDirsTask(): Task { function createLoadTask(): Task { return async (context, errors) => { - const { config, reporter, options, configLoader } = context; - const rootSuite = await loadAllTests(configLoader, reporter, options, errors); + const { config, reporter, options } = context; + const rootSuite = await loadAllTests(config, reporter, options, errors); const testGroups = options.listOnly ? [] : createTestGroups(rootSuite.suites, config.workers); context.rootSuite = rootSuite; diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 920fac627a..2bf07fa623 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -23,7 +23,6 @@ import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle'; import type { TestInfoError, Location } from './common/types'; import { calculateSha1, isRegExp, isString, captureStackTrace as coreCaptureStackTrace } from 'playwright-core/lib/utils'; import { isInternalFileName } from 'playwright-core/lib/utils'; -import { currentTestInfo } from './common/globals'; import type { ParsedStackTrace } from 'playwright-core/lib/utils'; export type { ParsedStackTrace }; @@ -31,7 +30,7 @@ export type { ParsedStackTrace }; const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core')); const EXPECT_PATH = require.resolve('./common/expectBundle'); const EXPECT_PATH_IMPL = require.resolve('./common/expectBundleImpl'); -const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '../..'); +const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); function filterStackTrace(e: Error) { if (process.env.PWDEBUGIMPL) @@ -243,16 +242,6 @@ Call log: `; } -export function currentExpectTimeout(options: { timeout?: number }) { - const testInfo = currentTestInfo(); - if (options.timeout !== undefined) - return options.timeout; - let defaultExpectTimeout = testInfo?.project._expect?.timeout; - if (typeof defaultExpectTimeout === 'undefined') - defaultExpectTimeout = 5000; - return defaultExpectTimeout; -} - const folderToPackageJsonPath = new Map(); export function getPackageJsonPath(folderPath: string): string { diff --git a/packages/playwright-test/src/worker/workerMain.ts b/packages/playwright-test/src/worker/workerMain.ts index 63bdd945f5..3ef25eca9d 100644 --- a/packages/playwright-test/src/worker/workerMain.ts +++ b/packages/playwright-test/src/worker/workerMain.ts @@ -21,7 +21,7 @@ import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerI import { setCurrentTestInfo } from '../common/globals'; import { ConfigLoader } from '../common/configLoader'; import type { Suite, TestCase } from '../common/test'; -import type { Annotation, FullProjectInternal, TestInfoError } from '../common/types'; +import type { Annotation, FullConfigInternal, FullProjectInternal, TestInfoError } from '../common/types'; import { FixtureRunner } from '../common/fixtures'; import { ManualPromise } from 'playwright-core/lib/utils'; import { TestInfoImpl } from '../common/testInfo'; @@ -36,7 +36,7 @@ const removeFolderAsync = util.promisify(rimraf); export class WorkerMain extends ProcessRunner { private _params: WorkerInitParams; - private _configLoader!: ConfigLoader; + private _config!: FullConfigInternal; private _testLoader!: TestLoader; private _project!: FullProjectInternal; private _poolBuilder!: PoolBuilder; @@ -190,12 +190,13 @@ export class WorkerMain extends ProcessRunner { } private async _loadIfNeeded() { - if (this._configLoader) + if (this._config) return; - this._configLoader = await ConfigLoader.deserialize(this._params.config); - this._testLoader = new TestLoader(this._configLoader.fullConfig()); - this._project = this._configLoader.fullConfig().projects.find(p => p._id === this._params.projectId)!; + const configLoader = await ConfigLoader.deserialize(this._params.config); + this._config = configLoader.fullConfig(); + this._testLoader = new TestLoader(this._config); + this._project = this._config.projects.find(p => p._id === this._params.projectId)!; this._poolBuilder = PoolBuilder.createForWorker(this._project); } @@ -251,7 +252,7 @@ export class WorkerMain extends ProcessRunner { } private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) { - const testInfo = new TestInfoImpl(this._configLoader.fullConfig(), this._project, this._params, test, retry, + const testInfo = new TestInfoImpl(this._config, this._project, this._params, test, retry, stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload), stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload)); @@ -485,8 +486,8 @@ export class WorkerMain extends ProcessRunner { setCurrentTestInfo(null); this.dispatchEvent('testEnd', buildTestEndPayload(testInfo)); - const preserveOutput = this._configLoader.fullConfig().preserveOutput === 'always' || - (this._configLoader.fullConfig().preserveOutput === 'failures-only' && testInfo._isFailure()); + const preserveOutput = this._config.preserveOutput === 'always' || + (this._config.preserveOutput === 'failures-only' && testInfo._isFailure()); if (!preserveOutput) await removeFolderAsync(testInfo.outputDir).catch(e => {}); }