diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index 46c577547f..51b1bc2610 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -23,10 +23,12 @@ import { Runner } from './runner/runner'; import { stopProfiling, startProfiling } from 'playwright-core/lib/utils'; import { experimentalLoaderOption, fileIsModule } from './util'; import { showHTMLReport } from './reporters/html'; -import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; -import type { TraceMode } from './common/types'; +import { ConfigLoader, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; import type { ConfigCLIOverrides } from './common/ipc'; import type { FullResult } from '../reporter'; +import type { TraceMode } from '../types/test'; +import { baseFullConfig, builtInReporters, defaultTimeout } from './common/config'; +import type { FullConfigInternal } from './common/config'; export function addTestCommands(program: Command) { addTestCommand(program); @@ -127,20 +129,18 @@ async function runTests(args: string[], opts: { [key: string]: any }) { return; const configLoader = new ConfigLoader(overrides); + let config: FullConfigInternal; if (resolvedConfigFile) - await configLoader.loadConfigFile(resolvedConfigFile); + config = await configLoader.loadConfigFile(resolvedConfigFile, opts.deps === false); else - await configLoader.loadEmptyConfig(configFileOrDirectory); - if (opts.deps === false) - configLoader.ignoreProjectDependencies(); + config = await configLoader.loadEmptyConfig(configFileOrDirectory); - const config = configLoader.fullConfig(); - config._internal.cliArgs = args; - config._internal.cliGrep = opts.grep as string | undefined; - config._internal.cliGrepInvert = opts.grepInvert as string | undefined; - config._internal.listOnly = !!opts.list; - config._internal.cliProjectFilter = opts.project || undefined; - config._internal.passWithNoTests = !!opts.passWithNoTests; + config.cliArgs = args; + config.cliGrep = opts.grep as string | undefined; + config.cliGrepInvert = opts.grepInvert as string | undefined; + config.listOnly = !!opts.list; + config.cliProjectFilter = opts.project || undefined; + config.passWithNoTests = !!opts.passWithNoTests; const runner = new Runner(config); let status: FullResult['status']; @@ -166,8 +166,8 @@ async function listTestFiles(opts: { [key: string]: any }) { return; const configLoader = new ConfigLoader(); - const runner = new Runner(configLoader.fullConfig()); - await configLoader.loadConfigFile(resolvedConfigFile); + const config = await configLoader.loadConfigFile(resolvedConfigFile); + const runner = new Runner(config); const report = await runner.listTestFiles(opts.project); write(JSON.stringify(report), () => { process.exit(0); diff --git a/packages/playwright-test/src/common/config.ts b/packages/playwright-test/src/common/config.ts new file mode 100644 index 0000000000..b6ad4ed3f7 --- /dev/null +++ b/packages/playwright-test/src/common/config.ts @@ -0,0 +1,255 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import os from 'os'; +import type { Config, Fixtures, Project, ReporterDescription } from '../../types/test'; +import type { Location } from '../../types/testReporter'; +import type { TestRunnerPluginRegistration } from '../plugins'; +import type { Matcher } from '../util'; +import { mergeObjects } from '../util'; +import type { ConfigCLIOverrides } from './ipc'; +import type { FullConfig, FullProject } from '../../types/test'; + +export type FixturesWithLocation = { + fixtures: Fixtures; + location: Location; +}; +export type Annotation = { type: string, description?: string }; + +export const defaultTimeout = 30000; + +export class FullConfigInternal { + readonly config: FullConfig; + globalOutputDir = path.resolve(process.cwd()); + configDir = ''; + configCLIOverrides: ConfigCLIOverrides = {}; + storeDir = ''; + maxConcurrentTestGroups = 0; + ignoreSnapshots = false; + webServers: Exclude[] = []; + plugins: TestRunnerPluginRegistration[] = []; + listOnly = false; + cliArgs: string[] = []; + cliGrep: string | undefined; + cliGrepInvert: string | undefined; + cliProjectFilter?: string[]; + testIdMatcher?: Matcher; + passWithNoTests?: boolean; + defineConfigWasUsed = false; + projects: FullProjectInternal[] = []; + + static from(config: FullConfig): FullConfigInternal { + return (config as any)[configInternalSymbol]; + } + + constructor(configDir: string, configFile: string | undefined, config: Config, throwawayArtifactsPath: string) { + this.configDir = configDir; + this.config = { ...baseFullConfig }; + (this.config as any)[configInternalSymbol] = this; + this.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright'); + this.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, path.resolve(process.cwd())); + this.ignoreSnapshots = takeFirst(config.ignoreSnapshots, false); + this.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p })); + + this.config.configFile = configFile; + this.config.rootDir = config.testDir || configDir; + this.config.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); + this.config.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel); + this.config.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup); + this.config.globalTeardown = takeFirst(config.globalTeardown, baseFullConfig.globalTeardown); + this.config.globalTimeout = takeFirst(config.globalTimeout, baseFullConfig.globalTimeout); + this.config.grep = takeFirst(config.grep, baseFullConfig.grep); + this.config.grepInvert = takeFirst(config.grepInvert, baseFullConfig.grepInvert); + this.config.maxFailures = takeFirst(config.maxFailures, baseFullConfig.maxFailures); + this.config.preserveOutput = takeFirst(config.preserveOutput, baseFullConfig.preserveOutput); + this.config.reporter = takeFirst(resolveReporters(config.reporter, configDir), baseFullConfig.reporter); + this.config.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests); + this.config.quiet = takeFirst(config.quiet, baseFullConfig.quiet); + this.config.shard = takeFirst(config.shard, baseFullConfig.shard); + this.config.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); + + const workers = takeFirst(config.workers, '50%'); + if (typeof workers === 'string') { + if (workers.endsWith('%')) { + const cpus = os.cpus().length; + this.config.workers = Math.max(1, Math.floor(cpus * (parseInt(workers, 10) / 100))); + } else { + this.config.workers = parseInt(workers, 10); + } + } else { + this.config.workers = workers; + } + + const webServers = takeFirst(config.webServer, baseFullConfig.webServer); + if (Array.isArray(webServers)) { // multiple web server mode + // Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type. + this.config.webServer = null; + this.webServers = webServers; + } else if (webServers) { // legacy singleton mode + this.config.webServer = webServers; + this.webServers = [webServers]; + } + this.config.metadata = takeFirst(config.metadata, baseFullConfig.metadata); + this.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath)); + resolveProjectDependencies(this.projects); + this._assignUniqueProjectIds(this.projects); + this.config.projects = this.projects.map(p => p.project); + } + + private _assignUniqueProjectIds(projects: FullProjectInternal[]) { + const usedNames = new Set(); + for (const p of projects) { + const name = p.project.name || ''; + for (let i = 0; i < projects.length; ++i) { + const candidate = name + (i ? i : ''); + if (usedNames.has(candidate)) + continue; + p.id = candidate; + usedNames.add(candidate); + break; + } + } + } + + private _resolveProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal { + // Resolve all config dirs relative to configDir. + if (projectConfig.testDir !== undefined) + projectConfig.testDir = path.resolve(this.configDir, projectConfig.testDir); + if (projectConfig.outputDir !== undefined) + projectConfig.outputDir = path.resolve(this.configDir, projectConfig.outputDir); + if (projectConfig.snapshotDir !== undefined) + projectConfig.snapshotDir = path.resolve(this.configDir, projectConfig.snapshotDir); + return new FullProjectInternal(config, this, projectConfig, throwawayArtifactsPath); + } +} + +export class FullProjectInternal { + readonly project: FullProject; + id = ''; + fullConfig: FullConfigInternal; + fullyParallel: boolean; + expect: Project['expect']; + respectGitIgnore: boolean; + deps: FullProjectInternal[] = []; + snapshotPathTemplate: string; + + static from(project: FullProject): FullProjectInternal { + return (project as any)[projectInternalSymbol]; + } + + constructor(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string) { + this.fullConfig = fullConfig; + + const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig.configDir); + + const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); + const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir); + const name = takeFirst(projectConfig.name, config.name, ''); + + const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; + this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); + + this.project = { + grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), + grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert), + outputDir, + repeatEach: takeFirst(projectConfig.repeatEach, config.repeatEach, 1), + retries: takeFirst(projectConfig.retries, config.retries, 0), + metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), + name, + testDir, + snapshotDir, + testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []), + testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).?(m)[jt]s?(x)'), + timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout), + use: mergeObjects(config.use, projectConfig.use), + dependencies: projectConfig.dependencies || [], + }; + (this.project as any)[projectInternalSymbol] = this; + this.fullyParallel = takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined); + this.expect = takeFirst(projectConfig.expect, config.expect, {}); + this.respectGitIgnore = !projectConfig.testDir && !config.testDir; + } +} + +export const baseFullConfig: FullConfig = { + forbidOnly: false, + fullyParallel: false, + globalSetup: null, + globalTeardown: null, + globalTimeout: 0, + grep: /.*/, + grepInvert: null, + maxFailures: 0, + metadata: {}, + preserveOutput: 'always', + projects: [], + 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: 0, + webServer: null, +}; + +export function takeFirst(...args: (T | undefined)[]): T { + for (const arg of args) { + if (arg !== undefined) + return arg; + } + return undefined as any as T; +} + +function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[] | undefined { + return toReporters(reporters as any)?.map(([id, arg]) => { + if (builtInReporters.includes(id as any)) + return [id, arg]; + return [require.resolve(id, { paths: [rootDir] }), arg]; + }); +} + +function resolveProjectDependencies(projects: FullProjectInternal[]) { + for (const project of projects) { + for (const dependencyName of project.project.dependencies) { + const dependencies = projects.filter(p => p.project.name === dependencyName); + if (!dependencies.length) + throw new Error(`Project '${project.project.name}' depends on unknown project '${dependencyName}'`); + if (dependencies.length > 1) + throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`); + project.deps.push(...dependencies); + } + } +} + +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]; + +export type ContextReuseMode = 'none' | 'force' | 'when-possible'; + +const configInternalSymbol = Symbol('configInternalSymbol'); +const projectInternalSymbol = Symbol('projectInternalSymbol'); diff --git a/packages/playwright-test/src/common/configLoader.ts b/packages/playwright-test/src/common/configLoader.ts index 8f05fa31a5..db488838ff 100644 --- a/packages/playwright-test/src/common/configLoader.ts +++ b/packages/playwright-test/src/common/configLoader.ts @@ -15,16 +15,14 @@ */ import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; import { isRegExp } from 'playwright-core/lib/utils'; import type { ConfigCLIOverrides, SerializedConfig } from './ipc'; import { requireOrImport } from './transform'; -import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types'; +import type { Config, Project } from '../../types/test'; import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util'; import { setCurrentConfig } from './globals'; - -export const defaultTimeout = 30000; +import { FullConfigInternal, takeFirst, toReporters } from './config'; const kDefineConfigWasUsed = Symbol('defineConfigWasUsed'); export const defineConfig = (config: any) => { @@ -33,62 +31,64 @@ export const defineConfig = (config: any) => { }; export class ConfigLoader { - private _fullConfig: FullConfigInternal; + private _configCLIOverrides: ConfigCLIOverrides; + private _fullConfig: FullConfigInternal | undefined; constructor(configCLIOverrides?: ConfigCLIOverrides) { - this._fullConfig = { ...baseFullConfig }; - this._fullConfig._internal.configCLIOverrides = configCLIOverrides || {}; + this._configCLIOverrides = configCLIOverrides || {}; } - static async deserialize(data: SerializedConfig): Promise { + static async deserialize(data: SerializedConfig): Promise { const loader = new ConfigLoader(data.configCLIOverrides); if (data.configFile) - await loader.loadConfigFile(data.configFile); - else - await loader.loadEmptyConfig(data.configDir); - return loader; + return await loader.loadConfigFile(data.configFile); + return await loader.loadEmptyConfig(data.configDir); } - async loadConfigFile(file: string): Promise { - if (this._fullConfig.configFile) + async loadConfigFile(file: string, ignoreProjectDependencies = false): Promise { + if (this._fullConfig) throw new Error('Cannot load two config files'); const config = await requireOrImportDefaultObject(file) as Config; - await this._processConfigObject(config, path.dirname(file), file); - setCurrentConfig(this._fullConfig); - return this._fullConfig; + const fullConfig = await this._loadConfig(config, path.dirname(file), file); + setCurrentConfig(fullConfig); + if (ignoreProjectDependencies) { + for (const project of fullConfig.projects) + project.deps = []; + } + this._fullConfig = fullConfig; + return fullConfig; } - async loadEmptyConfig(configDir: string): Promise { - await this._processConfigObject({}, configDir); - setCurrentConfig(this._fullConfig); - return {}; + async loadEmptyConfig(configDir: string): Promise { + const fullConfig = await this._loadConfig({}, configDir); + setCurrentConfig(fullConfig); + return fullConfig; } - private async _processConfigObject(config: Config, configDir: string, configFile?: string) { + private async _loadConfig(config: Config, configDir: string, configFile?: string): Promise { // 1. Validate data provided in the config file. validateConfig(configFile || '', config); // 2. Override settings from CLI. - const configCLIOverrides = this._fullConfig._internal.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) + 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) throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); - config.projects = takeFirst(configCLIOverrides.projects, config.projects as any); - config.workers = takeFirst(configCLIOverrides.workers, config.workers); - config.use = mergeObjects(config.use, configCLIOverrides.use); + 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); for (const project of config.projects || []) this._applyCLIOverridesToProject(project); @@ -110,134 +110,19 @@ export class ConfigLoader { if (config.snapshotDir !== undefined) config.snapshotDir = path.resolve(configDir, config.snapshotDir); - this._fullConfig._internal.configDir = configDir; - this._fullConfig._internal.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright'); - this._fullConfig.configFile = configFile; - this._fullConfig.rootDir = config.testDir || configDir; - this._fullConfig._internal.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._internal.globalOutputDir); - this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); - this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel); - this._fullConfig.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup); - this._fullConfig.globalTeardown = takeFirst(config.globalTeardown, baseFullConfig.globalTeardown); - this._fullConfig.globalTimeout = takeFirst(config.globalTimeout, baseFullConfig.globalTimeout); - this._fullConfig.grep = takeFirst(config.grep, baseFullConfig.grep); - this._fullConfig.grepInvert = takeFirst(config.grepInvert, baseFullConfig.grepInvert); - this._fullConfig.maxFailures = takeFirst(config.maxFailures, baseFullConfig.maxFailures); - this._fullConfig.preserveOutput = takeFirst(config.preserveOutput, baseFullConfig.preserveOutput); - this._fullConfig.reporter = takeFirst(resolveReporters(config.reporter, configDir), baseFullConfig.reporter); - this._fullConfig.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests); - this._fullConfig.quiet = takeFirst(config.quiet, baseFullConfig.quiet); - this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard); - this._fullConfig._internal.ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._internal.ignoreSnapshots); - this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); - this._fullConfig._internal.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p })); - this._fullConfig._internal.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed]; - - const workers = takeFirst(config.workers, '50%'); - if (typeof workers === 'string') { - if (workers.endsWith('%')) { - const cpus = os.cpus().length; - this._fullConfig.workers = Math.max(1, Math.floor(cpus * (parseInt(workers, 10) / 100))); - } else { - this._fullConfig.workers = parseInt(workers, 10); - } - } else { - this._fullConfig.workers = workers; - } - - const webServers = takeFirst(config.webServer, baseFullConfig.webServer); - if (Array.isArray(webServers)) { // multiple web server mode - // Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type. - this._fullConfig.webServer = null; - this._fullConfig._internal.webServers = webServers; - } else if (webServers) { // legacy singleton mode - this._fullConfig.webServer = webServers; - this._fullConfig._internal.webServers = [webServers]; - } - this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata); - this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath)); - - resolveProjectDependencies(this._fullConfig.projects); - this._assignUniqueProjectIds(this._fullConfig.projects); - } - - ignoreProjectDependencies() { - for (const project of this._fullConfig.projects) - project._internal.deps = []; - } - - private _assignUniqueProjectIds(projects: FullProjectInternal[]) { - const usedNames = new Set(); - for (const p of projects) { - const name = p.name || ''; - for (let i = 0; i < projects.length; ++i) { - const candidate = name + (i ? i : ''); - if (usedNames.has(candidate)) - continue; - p._internal.id = candidate; - usedNames.add(candidate); - break; - } - } - } - - fullConfig(): FullConfigInternal { - return this._fullConfig; + const fullConfig = new FullConfigInternal(configDir, configFile, config, throwawayArtifactsPath); + fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed]; + fullConfig.configCLIOverrides = this._configCLIOverrides; + return fullConfig; } private _applyCLIOverridesToProject(projectConfig: Project) { - const configCLIOverrides = this._fullConfig._internal.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(fullConfig._internal.configDir, projectConfig.testDir); - if (projectConfig.outputDir !== undefined) - projectConfig.outputDir = path.resolve(fullConfig._internal.configDir, projectConfig.outputDir); - if (projectConfig.snapshotDir !== undefined) - projectConfig.snapshotDir = path.resolve(fullConfig._internal.configDir, projectConfig.snapshotDir); - - const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig._internal.configDir); - const respectGitIgnore = !projectConfig.testDir && !config.testDir; - - const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); - const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir); - const name = takeFirst(projectConfig.name, config.name, ''); - - const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; - const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); - return { - _internal: { - id: '', - fullConfig: fullConfig, - fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), - expect: takeFirst(projectConfig.expect, config.expect, {}), - deps: [], - respectGitIgnore: respectGitIgnore, - }, - grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), - grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert), - outputDir, - repeatEach: takeFirst(projectConfig.repeatEach, config.repeatEach, 1), - retries: takeFirst(projectConfig.retries, config.retries, 0), - metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), - name, - testDir, - snapshotDir, - snapshotPathTemplate, - testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []), - testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).?(m)[jt]s?(x)'), - timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout), - use: mergeObjects(config.use, projectConfig.use), - dependencies: projectConfig.dependencies || [], - }; + 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); } } @@ -248,14 +133,6 @@ async function requireOrImportDefaultObject(file: string) { return object; } -function takeFirst(...args: (T | undefined)[]): T { - for (const arg of args) { - if (arg !== undefined) - return arg; - } - return undefined as any as T; -} - function validateConfig(file: string, config: Config) { if (typeof config !== 'object' || !config) throw errorWithFile(file, `Configuration file must export a single object`); @@ -428,53 +305,6 @@ function validateProject(file: string, project: Project, title: string) { } } -export const baseFullConfig: FullConfigInternal = { - forbidOnly: false, - fullyParallel: false, - globalSetup: null, - globalTeardown: null, - globalTimeout: 0, - grep: /.*/, - grepInvert: null, - maxFailures: 0, - metadata: {}, - preserveOutput: 'always', - projects: [], - reporter: [[process.env.CI ? 'dot' : 'list']], - reportSlowTests: { max: 5, threshold: 15000 }, - configFile: '', - rootDir: path.resolve(process.cwd()), - quiet: false, - shard: null, - updateSnapshots: 'missing', - version: require('../../package.json').version, - workers: 0, - webServer: null, - _internal: { - webServers: [], - globalOutputDir: path.resolve(process.cwd()), - configDir: '', - configCLIOverrides: {}, - storeDir: '', - maxConcurrentTestGroups: 0, - ignoreSnapshots: false, - plugins: [], - cliArgs: [], - cliGrep: undefined, - cliGrepInvert: undefined, - listOnly: false, - defineConfigWasUsed: false, - } -}; - -function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined { - return toReporters(reporters as any)?.map(([id, arg]) => { - if (builtInReporters.includes(id as any)) - return [id, arg]; - return [require.resolve(id, { paths: [rootDir] }), arg]; - }); -} - function resolveScript(id: string, rootDir: string) { const localPath = path.resolve(rootDir, id); if (fs.existsSync(localPath)) @@ -482,19 +312,6 @@ function resolveScript(id: string, rootDir: string) { return require.resolve(id, { paths: [rootDir] }); } -function resolveProjectDependencies(projects: FullProjectInternal[]) { - for (const project of projects) { - for (const dependencyName of project.dependencies) { - const dependencies = projects.filter(p => p.name === dependencyName); - if (!dependencies.length) - throw new Error(`Project '${project.name}' depends on unknown project '${dependencyName}'`); - if (dependencies.length > 1) - throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`); - project._internal.deps.push(...dependencies); - } - } -} - export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs']; export function resolveConfigFile(configFileOrDirectory: string): string | null { @@ -526,14 +343,3 @@ 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/fixtures.ts b/packages/playwright-test/src/common/fixtures.ts index 716b224d9a..4fd298e7cc 100644 --- a/packages/playwright-test/src/common/fixtures.ts +++ b/packages/playwright-test/src/common/fixtures.ts @@ -16,7 +16,9 @@ import { formatLocation } from '../util'; import * as crypto from 'crypto'; -import type { Fixtures, FixturesWithLocation, Location } from './types'; +import type { Fixtures } from '../../types/test'; +import type { Location } from '../../types/testReporter'; +import type { FixturesWithLocation } from './config'; export type FixtureScope = 'test' | 'worker'; type FixtureAuto = boolean | 'all-hooks-included'; diff --git a/packages/playwright-test/src/common/globals.ts b/packages/playwright-test/src/common/globals.ts index 2bee5c5a41..fa4821a87b 100644 --- a/packages/playwright-test/src/common/globals.ts +++ b/packages/playwright-test/src/common/globals.ts @@ -16,7 +16,8 @@ import type { TestInfoImpl } from '../worker/testInfo'; import type { Suite } from './test'; -import type { FullConfigInternal } from './types'; +import { FullProjectInternal } from './config'; +import type { FullConfigInternal } from './config'; let currentTestInfoValue: TestInfoImpl | null = null; export function setCurrentTestInfo(testInfo: TestInfoImpl | null) { @@ -38,7 +39,7 @@ export function currentExpectTimeout(options: { timeout?: number }) { const testInfo = currentTestInfo(); if (options.timeout !== undefined) return options.timeout; - let defaultExpectTimeout = testInfo?.project._internal.expect?.timeout; + let defaultExpectTimeout = testInfo?.project ? FullProjectInternal.from(testInfo.project).expect?.timeout : undefined; 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 f209e389cb..5e9d6fe1d4 100644 --- a/packages/playwright-test/src/common/ipc.ts +++ b/packages/playwright-test/src/common/ipc.ts @@ -15,7 +15,8 @@ */ import { serializeCompilationCache } from './compilationCache'; -import type { FullConfigInternal, TestInfoError, TestStatus } from './types'; +import type { FullConfigInternal } from './config'; +import type { TestInfoError, TestStatus } from '../../types/test'; export type ConfigCLIOverrides = { forbidOnly?: boolean; @@ -126,9 +127,9 @@ export type EnvProducedPayload = [string, string | null][]; export function serializeConfig(config: FullConfigInternal): SerializedConfig { const result: SerializedConfig = { - configFile: config.configFile, - configDir: config._internal.configDir, - configCLIOverrides: config._internal.configCLIOverrides, + configFile: config.config.configFile, + configDir: config.configDir, + configCLIOverrides: config.configCLIOverrides, compilationCache: serializeCompilationCache(), }; return result; diff --git a/packages/playwright-test/src/common/poolBuilder.ts b/packages/playwright-test/src/common/poolBuilder.ts index 382255bbf8..326f24843b 100644 --- a/packages/playwright-test/src/common/poolBuilder.ts +++ b/packages/playwright-test/src/common/poolBuilder.ts @@ -18,7 +18,7 @@ import { FixturePool } from './fixtures'; import type { LoadError } from './fixtures'; import type { Suite, TestCase } from './test'; import type { TestTypeImpl } from './testType'; -import type { FullProjectInternal } from './types'; +import type { FullProjectInternal } from './config'; import { formatLocation } from '../util'; import type { TestError } from '../../reporter'; @@ -74,8 +74,8 @@ export class PoolBuilder { private _buildTestTypePool(testType: TestTypeImpl, testErrors?: TestError[]): FixturePool { if (!this._testTypePools.has(testType)) { const optionOverrides = { - overrides: this._project?.use ?? {}, - location: { file: `project#${this._project?._internal.id}`, line: 1, column: 1 } + overrides: this._project?.project?.use ?? {}, + location: { file: `project#${this._project?.id}`, line: 1, column: 1 } }; const pool = new FixturePool(testType.fixtures, e => this._handleLoadError(e, testErrors), undefined, undefined, optionOverrides); this._testTypePools.set(testType, pool); diff --git a/packages/playwright-test/src/common/process.ts b/packages/playwright-test/src/common/process.ts index 8119555202..8b0cd4222b 100644 --- a/packages/playwright-test/src/common/process.ts +++ b/packages/playwright-test/src/common/process.ts @@ -17,7 +17,7 @@ import type { WriteStream } from 'tty'; import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc'; import { startProfiling, stopProfiling } from 'playwright-core/lib/utils'; -import type { TestInfoError } from './types'; +import type { TestInfoError } from '../../types/test'; import { serializeError } from '../util'; export type ProtocolRequest = { diff --git a/packages/playwright-test/src/common/suiteUtils.ts b/packages/playwright-test/src/common/suiteUtils.ts index 839e780a04..0247d2a8e4 100644 --- a/packages/playwright-test/src/common/suiteUtils.ts +++ b/packages/playwright-test/src/common/suiteUtils.ts @@ -17,7 +17,7 @@ import path from 'path'; import { calculateSha1 } from 'playwright-core/lib/utils'; import type { Suite, TestCase } from './test'; -import type { FullProjectInternal } from './types'; +import type { FullProjectInternal } from './config'; import type { Matcher, TestFileFilter } from '../util'; import { createFileMatcher } from '../util'; @@ -41,7 +41,7 @@ export function filterTestsRemoveEmptySuites(suite: Suite, filter: (test: TestCa } export function buildFileSuiteForProject(project: FullProjectInternal, suite: Suite, repeatEachIndex: number): Suite { - const relativeFile = path.relative(project.testDir, suite.location!.file).split(path.sep).join('/'); + const relativeFile = path.relative(project.project.testDir, suite.location!.file).split(path.sep).join('/'); const fileId = calculateSha1(relativeFile).slice(0, 20); // Clone suite. @@ -54,11 +54,11 @@ export function buildFileSuiteForProject(project: FullProjectInternal, suite: Su const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : ''; // At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles. - const testIdExpression = `[project=${project._internal.id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`; + const testIdExpression = `[project=${project.id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`; const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20); test.id = testId; test.repeatEachIndex = repeatEachIndex; - test._projectId = project._internal.id; + test._projectId = project.id; // Inherit properties from parent suites. let inheritedRetries: number | undefined; @@ -70,8 +70,8 @@ export function buildFileSuiteForProject(project: FullProjectInternal, suite: Su if (inheritedTimeout === undefined && parentSuite._timeout !== undefined) inheritedTimeout = parentSuite._timeout; } - test.retries = inheritedRetries ?? project.retries; - test.timeout = inheritedTimeout ?? project.timeout; + test.retries = inheritedRetries ?? project.project.retries; + test.timeout = inheritedTimeout ?? project.project.timeout; // Skip annotations imply skipped expectedStatus. if (test._staticAnnotations.some(a => a.type === 'skip' || a.type === 'fixme')) @@ -79,7 +79,7 @@ export function buildFileSuiteForProject(project: FullProjectInternal, suite: Su // We only compute / set digest in the runner. if (test._poolDigest) - test._workerHash = `${project._internal.id}-${test._poolDigest}-${repeatEachIndex}`; + test._workerHash = `${project.id}-${test._poolDigest}-${repeatEachIndex}`; }); return result; diff --git a/packages/playwright-test/src/common/test.ts b/packages/playwright-test/src/common/test.ts index b8aaa313c4..fe79121074 100644 --- a/packages/playwright-test/src/common/test.ts +++ b/packages/playwright-test/src/common/test.ts @@ -19,7 +19,9 @@ import type * as reporterTypes from '../../types/testReporter'; import type { SuitePrivate } from '../../types/reporterPrivate'; import type { TestTypeImpl } from './testType'; import { rootTestType } from './testType'; -import type { Annotation, FixturesWithLocation, FullProject, FullProjectInternal, Location } from './types'; +import type { Annotation, FixturesWithLocation, FullProjectInternal } from './config'; +import type { FullProject } from '../../types/test'; +import type { Location } from '../../types/testReporter'; class Base { title: string; @@ -197,7 +199,7 @@ export class Suite extends Base implements SuitePrivate { } project(): FullProject | undefined { - return this._projectConfig || this.parent?.project(); + return this._projectConfig?.project || this.parent?.project(); } } diff --git a/packages/playwright-test/src/common/testType.ts b/packages/playwright-test/src/common/testType.ts index ef02c0fc49..12b630f6de 100644 --- a/packages/playwright-test/src/common/testType.ts +++ b/packages/playwright-test/src/common/testType.ts @@ -18,7 +18,9 @@ import { expect } from '../matchers/expect'; import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals'; import { TestCase, Suite } from './test'; import { wrapFunctionWithLocation } from './transform'; -import type { Fixtures, FixturesWithLocation, Location, TestType } from './types'; +import type { FixturesWithLocation } from './config'; +import type { Fixtures, TestType } from '../../types/test'; +import type { Location } from '../../types/testReporter'; const testTypeSymbol = Symbol('testType'); diff --git a/packages/playwright-test/src/common/transform.ts b/packages/playwright-test/src/common/transform.ts index 7bedda954f..cdef485841 100644 --- a/packages/playwright-test/src/common/transform.ts +++ b/packages/playwright-test/src/common/transform.ts @@ -18,7 +18,7 @@ import path from 'path'; import fs from 'fs'; import { sourceMapSupport, pirates } from '../utilsBundle'; import url from 'url'; -import type { Location } from './types'; +import type { Location } from '../../types/testReporter'; import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader'; import { tsConfigLoader } from '../third_party/tsconfig-loader'; import Module from 'module'; diff --git a/packages/playwright-test/src/common/types.ts b/packages/playwright-test/src/common/types.ts deleted file mode 100644 index e3af8e82ab..0000000000 --- a/packages/playwright-test/src/common/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Fixtures, Project } from '../../types/test'; -import type { Location } from '../../types/testReporter'; -import type { TestRunnerPluginRegistration } from '../plugins'; -import type { Matcher } from '../util'; -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'; - -export type FixturesWithLocation = { - fixtures: Fixtures; - location: Location; -}; -export type Annotation = { type: string, description?: string }; - -type ConfigInternal = { - globalOutputDir: string; - configDir: string; - configCLIOverrides: ConfigCLIOverrides; - storeDir: string; - maxConcurrentTestGroups: number; - ignoreSnapshots: boolean; - webServers: Exclude[]; - plugins: TestRunnerPluginRegistration[]; - listOnly: boolean; - cliArgs: string[]; - cliGrep: string | undefined; - cliGrepInvert: string | undefined; - cliProjectFilter?: string[]; - testIdMatcher?: Matcher; - passWithNoTests?: boolean; - defineConfigWasUsed: boolean; -}; - -/** - * FullConfigInternal allows the plumbing of configuration details throughout the Test Runner without - * increasing the surface area of the public API type called FullConfig. - */ -export interface FullConfigInternal extends FullConfigPublic { - _internal: ConfigInternal; - - // Overrides the public field. - projects: FullProjectInternal[]; -} - -type ProjectInternal = { - id: string; - fullConfig: FullConfigInternal; - fullyParallel: boolean; - expect: Project['expect']; - respectGitIgnore: boolean; - deps: FullProjectInternal[]; -}; - -/** - * FullProjectInternal allows the plumbing of configuration details throughout the Test Runner without - * increasing the surface area of the public API type called FullProject. - */ -export interface FullProjectInternal extends FullProjectPublic { - _internal: ProjectInternal; - snapshotPathTemplate: string; -} - -export type ContextReuseMode = 'none' | 'force' | 'when-possible'; diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index a65e7cf3ff..c4de2b927f 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -22,7 +22,7 @@ import { createGuid, debugMode, addInternalStackPrefix, mergeTraceFiles, saveTra import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; import type { TestInfoImpl } from './worker/testInfo'; import { rootTestType } from './common/testType'; -import { type ContextReuseMode } from './common/types'; +import { type ContextReuseMode } from './common/config'; import { artifactsFolderName } from './isomorphic/folders'; export { expect } from './matchers/expect'; export { store as _store } from './store'; diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts index 8d9dd86bfe..081ac7757a 100644 --- a/packages/playwright-test/src/isomorphic/teleReceiver.ts +++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts @@ -15,7 +15,8 @@ */ import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStatus, TestStep } from '../../types/testReporter'; -import type { Annotation, FullProject, Metadata } from '../common/types'; +import type { Annotation } from '../common/config'; +import type { FullProject, Metadata } from '../../types/test'; import type * as reporterTypes from '../../types/testReporter'; import type { SuitePrivate } from '../../types/reporterPrivate'; diff --git a/packages/playwright-test/src/loader/loaderMain.ts b/packages/playwright-test/src/loader/loaderMain.ts index 21d07b3ecc..01ac96dc4b 100644 --- a/packages/playwright-test/src/loader/loaderMain.ts +++ b/packages/playwright-test/src/loader/loaderMain.ts @@ -17,7 +17,7 @@ import type { SerializedConfig } from '../common/ipc'; import { ConfigLoader } from '../common/configLoader'; import { ProcessRunner } from '../common/process'; -import type { FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/config'; import { loadTestFile } from '../common/testLoader'; import type { TestError } from '../../reporter'; import { addToCompilationCache, serializeCompilationCache } from '../common/compilationCache'; @@ -36,14 +36,14 @@ export class LoaderMain extends ProcessRunner { private _config(): Promise { if (!this._configPromise) - this._configPromise = ConfigLoader.deserialize(this._serializedConfig).then(configLoader => configLoader.fullConfig()); + this._configPromise = ConfigLoader.deserialize(this._serializedConfig); return this._configPromise; } async loadTestFile(params: { file: string }) { const testErrors: TestError[] = []; const config = await this._config(); - const fileSuite = await loadTestFile(params.file, config.rootDir, testErrors); + const fileSuite = await loadTestFile(params.file, config.config.rootDir, testErrors); this._poolBuilder.buildPools(fileSuite); return { fileSuite: fileSuite._deepSerialize(), testErrors }; } diff --git a/packages/playwright-test/src/matchers/expect.ts b/packages/playwright-test/src/matchers/expect.ts index 34fc603943..8bf73728cb 100644 --- a/packages/playwright-test/src/matchers/expect.ts +++ b/packages/playwright-test/src/matchers/expect.ts @@ -47,7 +47,7 @@ import { toPass } from './matchers'; import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot'; -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util'; import { diff --git a/packages/playwright-test/src/matchers/matcherHint.ts b/packages/playwright-test/src/matchers/matcherHint.ts index 5ab053532f..2c1a23c5cd 100644 --- a/packages/playwright-test/src/matchers/matcherHint.ts +++ b/packages/playwright-test/src/matchers/matcherHint.ts @@ -15,7 +15,7 @@ */ import { colors } from 'playwright-core/lib/utilsBundle'; -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; export function matcherHint(state: ReturnType, matcherName: string, a: any, b: any, matcherOptions: any, timeout?: number) { const message = state.utils.matcherHint(matcherName, a, b, matcherOptions); diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index 25f6635204..5f4c56724d 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -17,7 +17,7 @@ import type { Locator, Page, APIResponse } from 'playwright-core'; import type { FrameExpectOptions } from 'playwright-core/lib/client/types'; import { colors } from 'playwright-core/lib/utilsBundle'; -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; import { expectTypes, callLogText } from '../util'; import { currentTestInfo } from '../common/globals'; import type { TestInfoErrorState } from '../worker/testInfo'; diff --git a/packages/playwright-test/src/matchers/toBeTruthy.ts b/packages/playwright-test/src/matchers/toBeTruthy.ts index da89f93a0e..d83d3ba70d 100644 --- a/packages/playwright-test/src/matchers/toBeTruthy.ts +++ b/packages/playwright-test/src/matchers/toBeTruthy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; import { expectTypes, callLogText } from '../util'; import { matcherHint } from './matcherHint'; import { currentExpectTimeout } from '../common/globals'; diff --git a/packages/playwright-test/src/matchers/toEqual.ts b/packages/playwright-test/src/matchers/toEqual.ts index 216c1d6c56..d09ca058fe 100644 --- a/packages/playwright-test/src/matchers/toEqual.ts +++ b/packages/playwright-test/src/matchers/toEqual.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; import { expectTypes } from '../util'; import { callLogText } from '../util'; import { matcherHint } from './matcherHint'; diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index 00696d628b..05eff09ca1 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -17,7 +17,7 @@ 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 type { Expect } from '../../types/test'; import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; import { getComparator } from 'playwright-core/lib/utils'; @@ -253,12 +253,12 @@ export function toMatchSnapshot( if (received instanceof Promise) throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.'); - if (testInfo.config._internal.ignoreSnapshots) + if (testInfo._configInternal.ignoreSnapshots) return { pass: !this.isNot, message: () => '' }; const helper = new SnapshotHelper( testInfo, testInfo.snapshotPath.bind(testInfo), determineFileExtension(received), - testInfo.project._internal.expect?.toMatchSnapshot || {}, + testInfo._projectInternal.expect?.toMatchSnapshot || {}, nameOrOptions, optOptions); if (this.isNot) { @@ -298,10 +298,10 @@ export async function toHaveScreenshot( if (!testInfo) throw new Error(`toHaveScreenshot() must be called during the test`); - if (testInfo.config._internal.ignoreSnapshots) + if (testInfo._configInternal.ignoreSnapshots) return { pass: !this.isNot, message: () => '' }; - const config = (testInfo.project._internal.expect as any)?.toHaveScreenshot; + const config = (testInfo._projectInternal.expect as any)?.toHaveScreenshot; const snapshotPathResolver = testInfo.snapshotPath.bind(testInfo); const helper = new SnapshotHelper( testInfo, snapshotPathResolver, 'png', diff --git a/packages/playwright-test/src/matchers/toMatchText.ts b/packages/playwright-test/src/matchers/toMatchText.ts index c051dbd0b1..5a302027d3 100644 --- a/packages/playwright-test/src/matchers/toMatchText.ts +++ b/packages/playwright-test/src/matchers/toMatchText.ts @@ -17,7 +17,7 @@ import type { ExpectedTextValue } from '@protocol/channels'; import { isRegExp, isString } from 'playwright-core/lib/utils'; -import type { Expect } from '../common/types'; +import type { Expect } from '../../types/test'; import { expectTypes, callLogText } from '../util'; import { printReceivedStringContainExpectedResult, diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index 6cec8d9445..609f7bd38f 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext, ContextReuseMode, FullConfigInternal } from './common/types'; +import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from '../types/test'; import type { Component, JsxComponent, MountOptions } from '../types/experimentalComponent'; +import type { ContextReuseMode, FullConfigInternal } from './common/config'; let boundCallbacksForMount: Function[] = []; @@ -38,7 +39,7 @@ export const fixtures: Fixtures< _ctWorker: [{ context: undefined, hash: '' }, { scope: 'worker' }], page: async ({ page }, use, info) => { - if (!(info.config as FullConfigInternal)._internal.defineConfigWasUsed) + if (!((info as any)._configInternal as FullConfigInternal).defineConfigWasUsed) throw new Error('Component testing requires the use of the defineConfig() in your playwright-ct.config.{ts,js}: https://aka.ms/playwright/ct-define-config'); await (page as any)._wrapApiCall(async () => { await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => { diff --git a/packages/playwright-test/src/plugins/index.ts b/packages/playwright-test/src/plugins/index.ts index 1a5a663264..1e700ef493 100644 --- a/packages/playwright-test/src/plugins/index.ts +++ b/packages/playwright-test/src/plugins/index.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import type { Suite } from '../../types/testReporter'; -import type { FullConfig } from '../common/types'; +import type { FullConfig, Suite } from '../../types/testReporter'; import type { Multiplexer } from '../reporters/multiplexer'; export interface TestRunnerPlugin { diff --git a/packages/playwright-test/src/plugins/vitePlugin.ts b/packages/playwright-test/src/plugins/vitePlugin.ts index 06be18e288..5c23e0e94d 100644 --- a/packages/playwright-test/src/plugins/vitePlugin.ts +++ b/packages/playwright-test/src/plugins/vitePlugin.ts @@ -23,11 +23,10 @@ import { parse, traverse, types as t } from '../common/babelBundle'; import { stoppable } from '../utilsBundle'; import type { ComponentInfo } from '../common/tsxTransform'; import { collectComponentUsages, componentInfo } from '../common/tsxTransform'; -import type { FullConfig } from '../common/types'; import { assert, calculateSha1 } from 'playwright-core/lib/utils'; import type { AddressInfo } from 'net'; import { getPlaywrightVersion } from 'playwright-core/lib/utils'; -import type { PlaywrightTestConfig as BasePlaywrightTestConfig } from '@playwright/test'; +import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from '@playwright/test'; import type { PluginContext } from 'rollup'; import { setExternalDependencies } from '../common/compilationCache'; diff --git a/packages/playwright-test/src/plugins/webServerPlugin.ts b/packages/playwright-test/src/plugins/webServerPlugin.ts index ab7fe766a2..987f1a57b4 100644 --- a/packages/playwright-test/src/plugins/webServerPlugin.ts +++ b/packages/playwright-test/src/plugins/webServerPlugin.ts @@ -21,7 +21,7 @@ import { raceAgainstTimeout, launchProcess, httpRequest } from 'playwright-core/ import type { FullConfig } from '../../types/testReporter'; import type { TestRunnerPlugin } from '.'; -import type { FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/config'; import { envWithoutExperimentalLoaderOptions } from '../util'; import type { Multiplexer } from '../reporters/multiplexer'; @@ -204,9 +204,9 @@ export const webServer = (options: WebServerPluginOptions): TestRunnerPlugin => }; export const webServerPluginsForConfig = (config: FullConfigInternal): TestRunnerPlugin[] => { - const shouldSetBaseUrl = !!config.webServer; + const shouldSetBaseUrl = !!config.config.webServer; const webServerPlugins = []; - for (const webServerConfig of config._internal.webServers) { + for (const webServerConfig of config.webServers) { if ((!webServerConfig.port && !webServerConfig.url) || (webServerConfig.port && webServerConfig.url)) throw new Error(`Exactly one of 'port' or 'url' is required in config.webServer.`); diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 0164ab1394..53b554469e 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -19,9 +19,9 @@ import fs from 'fs'; import path from 'path'; import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location, Reporter } from '../../types/testReporter'; import type { SuitePrivate } from '../../types/reporterPrivate'; -import type { FullConfigInternal } from '../common/types'; import { codeFrameColumns } from '../common/babelBundle'; import { monotonicTime } from 'playwright-core/lib/utils'; +import { FullConfigInternal } from '../common/config'; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export const kOutputSymbol = Symbol('output'); @@ -49,7 +49,7 @@ type TestSummary = { export class BaseReporter implements Reporter { duration = 0; - config!: FullConfigInternal; + config!: FullConfig; suite!: Suite; totalTestCount = 0; result!: FullResult; @@ -66,7 +66,7 @@ export class BaseReporter implements Reporter { onBegin(config: FullConfig, suite: Suite) { this.monotonicStartTime = monotonicTime(); - this.config = config as FullConfigInternal; + this.config = config; this.suite = suite; this.totalTestCount = suite.allTests().length; } @@ -122,7 +122,7 @@ export class BaseReporter implements Reporter { } protected generateStartingMessage() { - const jobs = Math.min(this.config.workers, this.config._internal.maxConcurrentTestGroups); + const jobs = Math.min(this.config.workers, FullConfigInternal.from(this.config).maxConcurrentTestGroups); const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : ''; if (!this.totalTestCount) return ''; diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index a2ad7136f6..b31a4c1962 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -26,7 +26,7 @@ import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResul import RawReporter from './raw'; import { stripAnsiEscapes } from './base'; import { getPackageJsonPath, sanitizeForFilePath } from '../util'; -import type { Metadata } from '../common/types'; +import type { Metadata } from '../../types/test'; import type { ZipFile } from 'playwright-core/lib/zipBundle'; import { yazl } from 'playwright-core/lib/zipBundle'; import { mime } from 'playwright-core/lib/utilsBundle'; diff --git a/packages/playwright-test/src/reporters/json.ts b/packages/playwright-test/src/reporters/json.ts index 2921760747..33fd3a58cd 100644 --- a/packages/playwright-test/src/reporters/json.ts +++ b/packages/playwright-test/src/reporters/json.ts @@ -20,7 +20,7 @@ import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, Full import { formatError, prepareErrorStack } from './base'; import { MultiMap } from 'playwright-core/lib/utils'; import { assert } from 'playwright-core/lib/utils'; -import type { FullProjectInternal } from '../common/types'; +import { FullProjectInternal } from '../common/config'; export function toPosixPath(aPath: string): string { return aPath.split(path.sep).join(path.posix.sep); @@ -64,7 +64,7 @@ class JSONReporter implements Reporter { repeatEach: project.repeatEach, retries: project.retries, metadata: project.metadata, - id: (project as FullProjectInternal)._internal.id, + id: FullProjectInternal.from(project).id, name: project.name, testDir: toPosixPath(project.testDir), testIgnore: serializePatterns(project.testIgnore), @@ -81,7 +81,7 @@ class JSONReporter implements Reporter { private _mergeSuites(suites: Suite[]): JSONReportSuite[] { const fileSuites = new MultiMap(); for (const projectSuite of suites) { - const projectId = (projectSuite.project() as FullProjectInternal)._internal.id; + const projectId = FullProjectInternal.from(projectSuite.project()!).id; const projectName = projectSuite.project()!.name; for (const fileSuite of projectSuite.suites) { const file = fileSuite.location!.file; diff --git a/packages/playwright-test/src/reporters/multiplexer.ts b/packages/playwright-test/src/reporters/multiplexer.ts index dab2d80dc8..314f5a778f 100644 --- a/packages/playwright-test/src/reporters/multiplexer.ts +++ b/packages/playwright-test/src/reporters/multiplexer.ts @@ -16,6 +16,7 @@ import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter } from '../../types/testReporter'; import { Suite } from '../common/test'; +import type { FullConfigInternal } from '../common/config'; import { addSnippetToError } from './base'; type StdIOChunk = { @@ -27,7 +28,7 @@ type StdIOChunk = { export class Multiplexer { private _reporters: Reporter[]; private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; - private _config!: FullConfig; + private _config!: FullConfigInternal; constructor(reporters: Reporter[]) { this._reporters = reporters; @@ -37,7 +38,7 @@ export class Multiplexer { return this._reporters.some(r => r.printsToStdio ? r.printsToStdio() : true); } - onConfigure(config: FullConfig) { + onConfigure(config: FullConfigInternal) { this._config = config; } @@ -92,7 +93,7 @@ export class Multiplexer { async onExit(result: FullResult) { if (this._deferred) { // onBegin was not reported, emit it. - this.onBegin(this._config, new Suite('', 'root')); + this.onBegin(this._config.config, new Suite('', 'root')); } for (const reporter of this._reporters) @@ -107,7 +108,7 @@ export class Multiplexer { this._deferred.push({ error }); return; } - addSnippetToError(this._config, error); + addSnippetToError(this._config.config, error); for (const reporter of this._reporters) wrap(() => reporter.onError?.(error)); } @@ -125,12 +126,12 @@ export class Multiplexer { private _addSnippetToTestErrors(test: TestCase, result: TestResult) { for (const error of result.errors) - addSnippetToError(this._config, error, test.location.file); + addSnippetToError(this._config.config, error, test.location.file); } private _addSnippetToStepError(test: TestCase, step: TestStep) { if (step.error) - addSnippetToError(this._config, step.error, test.location.file); + addSnippetToError(this._config.config, step.error, test.location.file); } } diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index d366794335..24f8357139 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -23,7 +23,7 @@ import { formatResultFailure } from './base'; import { toPosixPath, serializePatterns } from './json'; import { MultiMap } from 'playwright-core/lib/utils'; import { codeFrameColumns } from '../common/babelBundle'; -import type { Metadata } from '../common/types'; +import type { Metadata } from '../../types/test'; import type { SuitePrivate } from '../../types/reporterPrivate'; export type JsonLocation = Location; diff --git a/packages/playwright-test/src/reporters/teleEmitter.ts b/packages/playwright-test/src/reporters/teleEmitter.ts index cc34209d64..19afc54ded 100644 --- a/packages/playwright-test/src/reporters/teleEmitter.ts +++ b/packages/playwright-test/src/reporters/teleEmitter.ts @@ -18,7 +18,7 @@ import type { FullConfig, FullResult, Reporter, TestError, TestResult, TestStep, import type { Suite, TestCase } from '../common/test'; import type { JsonConfig, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; import type { SuitePrivate } from '../../types/reporterPrivate'; -import type { FullConfigInternal, FullProjectInternal } from '../common/types'; +import { FullConfigInternal, FullProjectInternal } from '../common/config'; import { createGuid } from 'playwright-core/lib/utils'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import path from 'path'; @@ -123,14 +123,14 @@ export class TeleReporterEmitter implements Reporter { return { rootDir: config.rootDir, configFile: this._relativePath(config.configFile), - listOnly: (config as FullConfigInternal)._internal.listOnly, + listOnly: FullConfigInternal.from(config).listOnly, }; } private _serializeProject(suite: Suite): JsonProject { const project = suite.project()!; const report: JsonProject = { - id: (project as FullProjectInternal)._internal.id, + id: FullProjectInternal.from(project).id, metadata: project.metadata, name: project.name, outputDir: this._relativePath(project.outputDir), diff --git a/packages/playwright-test/src/runner/dispatcher.ts b/packages/playwright-test/src/runner/dispatcher.ts index 1b6f65de8d..17dbc1aae8 100644 --- a/packages/playwright-test/src/runner/dispatcher.ts +++ b/packages/playwright-test/src/runner/dispatcher.ts @@ -23,7 +23,7 @@ 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'; +import type { FullConfigInternal } from '../common/config'; import type { Multiplexer } from '../reporters/multiplexer'; type TestResultData = { @@ -180,7 +180,7 @@ export class Dispatcher { this._isStopped = false; this._workerSlots = []; // 1. Allocate workers. - for (let i = 0; i < this._config.workers; i++) + for (let i = 0; i < this._config.config.workers; i++) this._workerSlots.push({ busy: false }); // 2. Schedule enough jobs. for (let i = 0; i < this._workerSlots.length; i++) @@ -504,7 +504,7 @@ export class Dispatcher { } private _hasReachedMaxFailures() { - const maxFailures = this._config.maxFailures; + const maxFailures = this._config.config.maxFailures; return maxFailures > 0 && this._failureCount >= maxFailures; } @@ -512,7 +512,7 @@ export class Dispatcher { if (result.status !== 'skipped' && result.status !== test.expectedStatus) ++this._failureCount; this._reporter.onTestEnd?.(test, result); - const maxFailures = this._config.maxFailures; + const maxFailures = this._config.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 35201e1496..c5eee1310d 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -15,11 +15,12 @@ */ import path from 'path'; -import type { Reporter, TestError } from '../../types/testReporter'; +import type { FullConfig, Reporter, TestError } from '../../types/testReporter'; import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost'; import { Suite } from '../common/test'; import type { TestCase } from '../common/test'; -import type { FullConfigInternal, FullProjectInternal } from '../common/types'; +import type { FullProjectInternal } from '../common/config'; +import type { FullConfigInternal } from '../common/config'; import { createFileMatcherFromArguments, createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util'; import type { Matcher, TestFileFilter } from '../util'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; @@ -35,11 +36,11 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, additionalFi const config = testRun.config; const fsCache = new Map(); const sourceMapCache = new Map(); - const cliFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : null; + const cliFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : null; // First collect all files for the projects in the command line, don't apply any file filters. const allFilesForProject = new Map(); - for (const project of filterProjects(config.projects, config._internal.cliProjectFilter)) { + for (const project of filterProjects(config.projects, config.cliProjectFilter)) { const files = await collectFilesForProject(project, fsCache); allFilesForProject.set(project, files); } @@ -100,8 +101,8 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' | for (const file of allTestFiles) { for (const dependency of dependenciesForTestFile(file)) { if (allTestFiles.has(dependency)) { - const importer = path.relative(config.rootDir, file); - const importee = path.relative(config.rootDir, dependency); + const importer = path.relative(config.config.rootDir, file); + const importee = path.relative(config.config.rootDir, dependency); errors.push({ message: `Error: test file "${importer}" should not import test file "${importee}"`, location: { file, line: 1, column: 1 }, @@ -125,20 +126,20 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho // First add top-level projects, so that we can filterOnly and shard just top-level. { // Interpret cli parameters. - const cliFileFilters = createFileFiltersFromArguments(config._internal.cliArgs); - const grepMatcher = config._internal.cliGrep ? createTitleMatcher(forceRegExp(config._internal.cliGrep)) : () => true; - const grepInvertMatcher = config._internal.cliGrepInvert ? createTitleMatcher(forceRegExp(config._internal.cliGrepInvert)) : () => false; + const cliFileFilters = createFileFiltersFromArguments(config.cliArgs); + const grepMatcher = config.cliGrep ? createTitleMatcher(forceRegExp(config.cliGrep)) : () => true; + const grepInvertMatcher = config.cliGrepInvert ? createTitleMatcher(forceRegExp(config.cliGrepInvert)) : () => false; const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); // Clone file suites for top-level projects. for (const [project, fileSuites] of testRun.projectSuites) { if (testRun.projectType.get(project) === 'top-level') - rootSuite._addSuite(await createProjectSuite(fileSuites, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher })); + rootSuite._addSuite(await createProjectSuite(fileSuites, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config.testIdMatcher })); } } // Complain about only. - if (config.forbidOnly) { + if (config.config.forbidOnly) { const onlyTestsAndSuites = rootSuite._getOnlyItems(); if (onlyTestsAndSuites.length > 0) errors.push(...createForbidOnlyErrors(onlyTestsAndSuites)); @@ -149,14 +150,14 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho filterOnly(rootSuite); // Shard only the top-level projects. - if (config.shard) { + if (config.config.shard) { // Create test groups for top-level projects. const testGroups: TestGroup[] = []; for (const projectSuite of rootSuite.suites) - testGroups.push(...createTestGroups(projectSuite, config.workers)); + testGroups.push(...createTestGroups(projectSuite, config.config.workers)); // Shard test groups. - const testGroupsInThisShard = filterForShard(config.shard, testGroups); + const testGroupsInThisShard = filterForShard(config.config.shard, testGroups); const testsInThisShard = new Set(); for (const group of testGroupsInThisShard) { for (const test of group.tests) @@ -171,7 +172,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho { // Filtering only and sharding might have reduced the number of top-level projects. // Build the project closure to only include dependencies that are still needed. - const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal))); + const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite._projectConfig!))); // Clone file suites for dependency projects. for (const [project, fileSuites] of testRun.projectSuites) { @@ -184,12 +185,12 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho } async function createProjectSuite(fileSuites: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }): Promise { - const projectSuite = new Suite(project.name, 'project'); + const projectSuite = new Suite(project.project.name, 'project'); projectSuite._projectConfig = project; - if (project._internal.fullyParallel) + if (project.fullyParallel) projectSuite._parallelMode = 'parallel'; for (const fileSuite of fileSuites) { - for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) { + for (let repeatEachIndex = 0; repeatEachIndex < project.project.repeatEach; repeatEachIndex++) { const builtSuite = buildFileSuiteForProject(project, fileSuite, repeatEachIndex); projectSuite._addSuite(builtSuite); } @@ -198,8 +199,8 @@ async function createProjectSuite(fileSuites: Suite[], project: FullProjectInter filterByFocusedLine(projectSuite, options.cliFileFilters); filterByTestIds(projectSuite, options.testIdMatcher); - const grepMatcher = createTitleMatcher(project.grep); - const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null; + const grepMatcher = createTitleMatcher(project.project.grep); + const grepInvertMatcher = project.project.grepInvert ? createTitleMatcher(project.project.grepInvert) : null; const titleMatcher = (test: TestCase) => { const grepTitle = test.titlePath().join(' '); @@ -234,7 +235,7 @@ function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuite: Suit const existingTest = testsByFullTitle.get(fullTitle); if (existingTest) { const error: TestError = { - message: `Error: duplicate test title "${fullTitle}", first declared in ${buildItemLocation(config.rootDir, existingTest)}`, + message: `Error: duplicate test title "${fullTitle}", first declared in ${buildItemLocation(config.config.rootDir, existingTest)}`, location: test.location, }; errors.push(error); @@ -259,12 +260,12 @@ async function requireOrImportDefaultFunction(file: string, expectConstructor: b return func; } -export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfigInternal) => any> { - return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), false); +export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfig) => any> { + return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false); } export function loadReporter(config: FullConfigInternal, file: string): Promise Reporter> { - return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), true); + return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), true); } function sourceMapSources(file: string, cache: Map): string[] { diff --git a/packages/playwright-test/src/runner/loaderHost.ts b/packages/playwright-test/src/runner/loaderHost.ts index 86f13cc785..29a697c286 100644 --- a/packages/playwright-test/src/runner/loaderHost.ts +++ b/packages/playwright-test/src/runner/loaderHost.ts @@ -19,7 +19,7 @@ import { serializeConfig } from '../common/ipc'; import { ProcessHost } from './processHost'; import { Suite } from '../common/test'; import { loadTestFile } from '../common/testLoader'; -import type { FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/config'; import { PoolBuilder } from '../common/poolBuilder'; import { addToCompilationCache } from '../common/compilationCache'; @@ -33,7 +33,7 @@ export class InProcessLoaderHost { } async loadTestFile(file: string, testErrors: TestError[]): Promise { - const result = await loadTestFile(file, this._config.rootDir, testErrors); + const result = await loadTestFile(file, this._config.config.rootDir, testErrors); this._poolBuilder.buildPools(result, testErrors); return result; } diff --git a/packages/playwright-test/src/runner/projectUtils.ts b/packages/playwright-test/src/runner/projectUtils.ts index 547906065c..664eadcbc5 100644 --- a/packages/playwright-test/src/runner/projectUtils.ts +++ b/packages/playwright-test/src/runner/projectUtils.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { minimatch } from 'playwright-core/lib/utilsBundle'; import { promisify } from 'util'; -import type { FullProjectInternal } from '../common/types'; +import type { FullProjectInternal } from '../common/config'; import { createFileMatcher } from '../util'; const readFileAsync = promisify(fs.readFile); @@ -35,12 +35,12 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s unknownProjects.set(name, n); }); const result = projects.filter(project => { - const name = project.name.toLocaleLowerCase(); + const name = project.project.name.toLocaleLowerCase(); unknownProjects.delete(name); return projectsToFind.has(name); }); if (unknownProjects.size) { - const names = projects.map(p => p.name).filter(name => !!name); + const names = projects.map(p => p.project.name).filter(name => !!name); if (!names.length) throw new Error(`No named projects are specified in the configuration file`); const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', '); @@ -58,7 +58,7 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map()): Promise { const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx']; const testFileExtension = (file: string) => extensions.includes(path.extname(file)); - const allFiles = await cachedCollectFiles(project.testDir, project._internal.respectGitIgnore, fsCache); - const testMatch = createFileMatcher(project.testMatch); - const testIgnore = createFileMatcher(project.testIgnore); + const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache); + const testMatch = createFileMatcher(project.project.testMatch); + const testIgnore = createFileMatcher(project.project.testIgnore); const testFiles = allFiles.filter(file => { if (!testFileExtension(file)) return false; diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts index 3cf8013b3f..108d1ef7b0 100644 --- a/packages/playwright-test/src/runner/reporters.ts +++ b/packages/playwright-test/src/runner/reporters.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import type { Reporter, TestError } from '../../types/testReporter'; +import type { FullConfig, Reporter, TestError } from '../../types/testReporter'; import { formatError } from '../reporters/base'; import DotReporter from '../reporters/dot'; import EmptyReporter from '../reporters/empty'; @@ -27,9 +27,8 @@ import LineReporter from '../reporters/line'; import ListReporter from '../reporters/list'; import { Multiplexer } from '../reporters/multiplexer'; import type { Suite } from '../common/test'; -import type { FullConfigInternal } from '../common/types'; +import type { BuiltInReporter, FullConfigInternal } from '../common/config'; import { loadReporter } from './loadUtils'; -import type { BuiltInReporter } from '../common/configLoader'; export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run' | 'ui', additionalReporters: Reporter[] = []): Promise { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { @@ -46,9 +45,9 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' | if (mode === 'watch') { reporters.push(new ListReporter()); } else { - for (const r of config.reporter) { + for (const r of config.config.reporter) { const [name, arg] = r; - const options = { ...arg, configDir: config._internal.configDir }; + const options = { ...arg, configDir: config.configDir }; if (name in defaultReporters) { reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options)); } else { @@ -79,9 +78,9 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' | } export class ListModeReporter implements Reporter { - private config!: FullConfigInternal; + private config!: FullConfig; - onBegin(config: FullConfigInternal, suite: Suite): void { + onBegin(config: FullConfig, suite: Suite): void { this.config = config; // eslint-disable-next-line no-console console.log(`Listing tests:`); diff --git a/packages/playwright-test/src/runner/runner.ts b/packages/playwright-test/src/runner/runner.ts index d5c0d5b5e8..c9225de18b 100644 --- a/packages/playwright-test/src/runner/runner.ts +++ b/packages/playwright-test/src/runner/runner.ts @@ -21,7 +21,7 @@ import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import { collectFilesForProject, filterProjects } from './projectUtils'; import { createReporter } from './reporters'; import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; -import type { FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/config'; import { colors } from 'playwright-core/lib/utilsBundle'; import { runWatchModeLoop } from './watchMode'; import { runUIMode } from './uiMode'; @@ -49,11 +49,11 @@ export class Runner { async runAllTests(): Promise { const config = this._config; - const listOnly = config._internal.listOnly; - const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0; + const listOnly = config.listOnly; + const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0; // Legacy webServer support. - webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p })); + webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); const reporter = await createReporter(config, listOnly ? 'list' : 'run'); const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process') @@ -62,7 +62,7 @@ export class Runner { const testRun = new TestRun(config, reporter); reporter.onConfigure(config); - if (!listOnly && config._internal.ignoreSnapshots) { + if (!listOnly && config.ignoreSnapshots) { reporter.onStdOut(colors.dim([ 'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:', '- expect().toMatchSnapshot()', @@ -89,13 +89,13 @@ export class Runner { async watchAllTests(): Promise { const config = this._config; - webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p })); + webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); return await runWatchModeLoop(config); } async uiAllTests(): Promise { const config = this._config; - webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p })); + webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); return await runUIMode(config); } } diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index e9b831b29d..40bbcafd6c 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -24,7 +24,7 @@ import type { Multiplexer } from '../reporters/multiplexer'; import { createTestGroups, type TestGroup } from '../runner/testGroups'; import type { Task } from './taskRunner'; import { TaskRunner } from './taskRunner'; -import type { FullConfigInternal, FullProjectInternal } from '../common/types'; +import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils'; import type { Matcher } from '../util'; import type { Suite } from '../common/test'; @@ -60,7 +60,7 @@ export class TestRun { } export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.globalTimeout); + const taskRunner = new TaskRunner(reporter, config.config.globalTimeout); addGlobalSetupTasks(taskRunner, config); taskRunner.addTask('load tests', createLoadTask('in-process', true)); addRunTasks(taskRunner, config); @@ -81,9 +81,9 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: M } function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfigInternal) { - for (const plugin of config._internal.plugins) + for (const plugin of config.plugins) taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); - if (config.globalSetup || config.globalTeardown) + if (config.config.globalSetup || config.config.globalTeardown) taskRunner.addTask('global setup', createGlobalSetupTask()); taskRunner.addTask('clear output', createRemoveOutputDirsTask()); } @@ -91,10 +91,10 @@ function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfig function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal) { taskRunner.addTask('create phases', createPhasesTask()); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { - reporter.onBegin?.(config, rootSuite!); + reporter.onBegin?.(config.config, rootSuite!); return () => reporter.onEnd(); }); - for (const plugin of config._internal.plugins) + for (const plugin of config.plugins) taskRunner.addTask('plugin begin', createPluginBeginTask(plugin)); taskRunner.addTask('start workers', createWorkersTask()); taskRunner.addTask('test suite', createRunTestsTask()); @@ -102,10 +102,10 @@ function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal } export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.globalTimeout); + const taskRunner = new TaskRunner(reporter, config.config.globalTimeout); taskRunner.addTask('load tests', createLoadTask(mode, false)); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { - reporter.onBegin?.(config, rootSuite!); + reporter.onBegin?.(config.config, rootSuite!); return () => reporter.onEnd(); }); return taskRunner; @@ -117,7 +117,7 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task plugin.instance?.teardown?.(); }; } @@ -131,13 +131,13 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task { 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; + const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; + const teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; + const globalSetupResult = setupHook ? await setupHook(config.config) : undefined; return async () => { if (typeof globalSetupResult === 'function') await globalSetupResult(); - await teardownHook?.(config); + await teardownHook?.(config.config); }; }; } @@ -146,8 +146,8 @@ function createRemoveOutputDirsTask(): Task { return async ({ config }) => { const outputDirs = new Set(); for (const p of config.projects) { - if (!config._internal.cliProjectFilter || config._internal.cliProjectFilter.includes(p.name)) - outputDirs.add(p.outputDir); + if (!config.cliProjectFilter || config.cliProjectFilter.includes(p.project.name)) + outputDirs.add(p.project.outputDir); } await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async (error: any) => { @@ -170,24 +170,24 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: await loadFileSuites(testRun, mode, errors); testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly); // Fail when no tests. - if (!testRun.rootSuite.allTests().length && !testRun.config._internal.passWithNoTests && !testRun.config.shard) + if (!testRun.rootSuite.allTests().length && !testRun.config.passWithNoTests && !testRun.config.config.shard) throw new Error(`No tests found`); }; } function createPhasesTask(): Task { return async testRun => { - testRun.config._internal.maxConcurrentTestGroups = 0; + testRun.config.maxConcurrentTestGroups = 0; const processed = new Set(); - const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite])); + const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._projectConfig!, suite])); for (let i = 0; i < projectToSuite.size; i++) { // Find all projects that have all their dependencies processed by previous phases. const phaseProjects: FullProjectInternal[] = []; for (const project of projectToSuite.keys()) { if (processed.has(project)) continue; - if (project._internal.deps.find(p => !processed.has(p))) + if (project.deps.find(p => !processed.has(p))) continue; phaseProjects.push(project); } @@ -201,12 +201,12 @@ function createPhasesTask(): Task { testRun.phases.push(phase); for (const project of phaseProjects) { const projectSuite = projectToSuite.get(project)!; - const testGroups = createTestGroups(projectSuite, testRun.config.workers); + const testGroups = createTestGroups(projectSuite, testRun.config.config.workers); phase.projects.push({ project, projectSuite, testGroups }); testGroupsInPhase += testGroups.length; } - debug('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); - testRun.config._internal.maxConcurrentTestGroups = Math.max(testRun.config._internal.maxConcurrentTestGroups, testGroupsInPhase); + debug('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); + testRun.config.maxConcurrentTestGroups = Math.max(testRun.config.maxConcurrentTestGroups, testGroupsInPhase); } } }; @@ -235,11 +235,11 @@ function createRunTestsTask(): Task { for (const { project, testGroups } of projects) { // Inherit extra enviroment variables from dependencies. let extraEnv: Record = {}; - for (const dep of project._internal.deps) - extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep._internal.id) }; - extraEnvByProjectId.set(project._internal.id, extraEnv); + for (const dep of project.deps) + extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) }; + extraEnvByProjectId.set(project.id, extraEnv); - const hasFailedDeps = project._internal.deps.some(p => !successfulProjects.has(p)); + const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p)); if (!hasFailedDeps) { phaseTestGroups.push(...testGroups); } else { @@ -263,7 +263,7 @@ function createRunTestsTask(): Task { // projects failed. if (!dispatcher.hasWorkerErrors()) { for (const { project, projectSuite } of projects) { - const hasFailedDeps = project._internal.deps.some(p => !successfulProjects.has(p)); + const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p)); if (!hasFailedDeps && !projectSuite.allTests().some(test => !test.ok())) successfulProjects.add(project); } diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index fd40afeb23..9c182df3fb 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -19,7 +19,7 @@ import type { Page } from 'playwright-core/lib/server/page'; import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils'; import type { FullResult } from '../../reporter'; import { clearCompilationCache, collectAffectedTestFiles, dependenciesForTestFile } from '../common/compilationCache'; -import type { FullConfigInternal } from '../common/types'; +import type { FullConfigInternal } from '../common/config'; import { Multiplexer } from '../reporters/multiplexer'; import { TeleReporterEmitter } from '../reporters/teleEmitter'; import { createReporter } from './reporters'; @@ -42,20 +42,20 @@ class UIMode { constructor(config: FullConfigInternal) { this._config = config; process.env.PW_LIVE_TRACE_STACKS = '1'; - config._internal.configCLIOverrides.forbidOnly = false; - config._internal.configCLIOverrides.globalTimeout = 0; - config._internal.configCLIOverrides.repeatEach = 0; - config._internal.configCLIOverrides.shard = undefined; - config._internal.configCLIOverrides.updateSnapshots = undefined; - config._internal.listOnly = false; - config._internal.passWithNoTests = true; + config.configCLIOverrides.forbidOnly = false; + config.configCLIOverrides.globalTimeout = 0; + config.configCLIOverrides.repeatEach = 0; + config.configCLIOverrides.shard = undefined; + config.configCLIOverrides.updateSnapshots = undefined; + config.listOnly = false; + config.passWithNoTests = true; for (const project of config.projects) - project._internal.deps = []; + project.deps = []; for (const p of config.projects) - p.retries = 0; - config._internal.configCLIOverrides.use = config._internal.configCLIOverrides.use || {}; - config._internal.configCLIOverrides.use.trace = { mode: 'on', sources: false }; + p.project.retries = 0; + config.configCLIOverrides.use = config.configCLIOverrides.use || {}; + config.configCLIOverrides.use.trace = { mode: 'on', sources: false }; this._originalStdoutWrite = process.stdout.write; this._originalStderrWrite = process.stderr.write; @@ -148,8 +148,8 @@ class UIMode { private async _listTests() { const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); const reporter = new Multiplexer([listReporter]); - this._config._internal.listOnly = true; - this._config._internal.testIdMatcher = undefined; + this._config.listOnly = true; + this._config.testIdMatcher = undefined; const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process'); const testRun = new TestRun(this._config, reporter); clearCompilationCache(); @@ -159,7 +159,7 @@ class UIMode { const projectDirs = new Set(); for (const p of this._config.projects) - projectDirs.add(p.testDir); + projectDirs.add(p.project.testDir); this._globalWatcher.update([...projectDirs], false); } @@ -167,8 +167,8 @@ class UIMode { await this._stopTests(); const testIdSet = testIds ? new Set(testIds) : null; - this._config._internal.listOnly = false; - this._config._internal.testIdMatcher = id => !testIdSet || testIdSet.has(id); + this._config.listOnly = false; + this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); const reporter = await createReporter(this._config, 'ui', [runReporter]); @@ -180,7 +180,7 @@ class UIMode { const run = taskRunner.run(testRun, 0, stop).then(async status => { await reporter.onExit({ status }); this._testRun = undefined; - this._config._internal.testIdMatcher = undefined; + this._config.testIdMatcher = undefined; return status; }); this._testRun = { run, stop }; diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts index 23ceea813a..ae931b55ce 100644 --- a/packages/playwright-test/src/runner/watchMode.ts +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -16,7 +16,7 @@ import readline from 'readline'; import { createGuid, ManualPromise } from 'playwright-core/lib/utils'; -import type { FullConfigInternal, FullProjectInternal } from '../common/types'; +import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import { Multiplexer } from '../reporters/multiplexer'; import { createFileMatcher, createFileMatcherFromArguments } from '../util'; import type { Matcher } from '../util'; @@ -40,15 +40,15 @@ class FSWatcher { private _timer: NodeJS.Timeout | undefined; async update(config: FullConfigInternal) { - const commandLineFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true; - const projects = filterProjects(config.projects, config._internal.cliProjectFilter); + const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true; + const projects = filterProjects(config.projects, config.cliProjectFilter); const projectClosure = buildProjectsClosure(projects); const projectFilters = new Map(); for (const [project, type] of projectClosure) { - const testMatch = createFileMatcher(project.testMatch); - const testIgnore = createFileMatcher(project.testIgnore); + const testMatch = createFileMatcher(project.project.testMatch); + const testIgnore = createFileMatcher(project.project.testIgnore); projectFilters.set(project, file => { - if (!file.startsWith(project.testDir) || !testMatch(file) || testIgnore(file)) + if (!file.startsWith(project.project.testDir) || !testMatch(file) || testIgnore(file)) return false; return type === 'dependency' || commandLineFileMatcher(file); }); @@ -59,7 +59,7 @@ class FSWatcher { if (this._watcher) await this._watcher.close(); - this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => { + this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.project.testDir), { ignoreInitial: true }).on('all', async (event, file) => { if (event !== 'add' && event !== 'change') return; @@ -108,9 +108,9 @@ class FSWatcher { export async function runWatchModeLoop(config: FullConfigInternal): Promise { // Reset the settings that don't apply to watch. - config._internal.passWithNoTests = true; + config.passWithNoTests = true; for (const p of config.projects) - p.retries = 0; + p.project.retries = 0; // Perform global setup. const reporter = await createReporter(config, 'watch'); @@ -123,7 +123,7 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise(); - const originalWorkers = config.workers; + const originalWorkers = config.config.workers; const fsWatcher = new FSWatcher(); await fsWatcher.update(config); @@ -165,11 +165,11 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise ({ name: p.name })), + choices: config.projects.map(p => ({ name: p.project.name })), }).catch(() => ({ projectNames: null })); if (!projectNames) continue; - config._internal.cliProjectFilter = projectNames.length ? projectNames : undefined; + config.cliProjectFilter = projectNames.length ? projectNames : undefined; await fsWatcher.update(config); await runTests(config, failedTestIdCollector); lastRun = { type: 'regular' }; @@ -185,9 +185,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise failedTestIdCollector.has(id); + config.testIdMatcher = id => failedTestIdCollector.has(id); const failedTestIds = new Set(failedTestIdCollector); await runTests(config, failedTestIdCollector, { title: 'running failed tests' }); - config._internal.testIdMatcher = undefined; + config.testIdMatcher = undefined; lastRun = { type: 'failed', failedTestIds }; continue; } @@ -228,9 +228,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise lastRun.failedTestIds!.has(id); + config.testIdMatcher = id => lastRun.failedTestIds!.has(id); await runTests(config, failedTestIdCollector, { title: 're-running tests' }); - config._internal.testIdMatcher = undefined; + config.testIdMatcher = undefined; } continue; } @@ -259,7 +259,7 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector // Collect all the affected projects, follow project dependencies. // Prepare to exclude all the projects that do not depend on this file, as if they did not exist. - const projects = filterProjects(config.projects, config._internal.cliProjectFilter); + const projects = filterProjects(config.projects, config.cliProjectFilter); const projectClosure = buildProjectsClosure(projects); const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]); const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency'); @@ -305,7 +305,7 @@ function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected const result = new Set(affected); for (let i = 0; i < projectClosure.length; ++i) { for (const p of projectClosure) { - for (const dep of p._internal.deps) { + for (const dep of p.deps) { if (result.has(dep)) result.add(p); } @@ -379,11 +379,11 @@ let seq = 0; function printConfiguration(config: FullConfigInternal, title?: string) { const tokens: string[] = []; tokens.push('npx playwright test'); - tokens.push(...(config._internal.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); - if (config._internal.cliGrep) - tokens.push(colors.red(`--grep ${config._internal.cliGrep}`)); - if (config._internal.cliArgs) - tokens.push(...config._internal.cliArgs.map(a => colors.bold(a))); + tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); + if (config.cliGrep) + tokens.push(colors.red(`--grep ${config.cliGrep}`)); + if (config.cliArgs) + tokens.push(...config.cliArgs.map(a => colors.bold(a))); if (title) tokens.push(colors.dim(`(${title})`)); if (seq) @@ -407,14 +407,14 @@ ${colors.dim('Waiting for file changes. Press')} ${colors.bold('enter')} ${color async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) { if (!showBrowserServer) { - config.workers = 1; + config.config.workers = 1; showBrowserServer = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: 1 }); const wsEndpoint = await showBrowserServer.listen(); process.env.PW_TEST_REUSE_CONTEXT = '1'; process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint; process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`); } else { - config.workers = originalWorkers; + config.config.workers = originalWorkers; await showBrowserServer?.close(); showBrowserServer = undefined; delete process.env.PW_TEST_REUSE_CONTEXT; diff --git a/packages/playwright-test/src/store.ts b/packages/playwright-test/src/store.ts index d7bcddeae1..ceb5bd85c7 100644 --- a/packages/playwright-test/src/store.ts +++ b/packages/playwright-test/src/store.ts @@ -49,7 +49,7 @@ class JsonStore { const config = currentConfig(); if (!config) throw new Error('Cannot access store before config is loaded'); - return config._internal.storeDir; + return config.storeDir; } async set(name: string, value: T | undefined) { diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 50bc893ba1..656d7f7393 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -21,7 +21,8 @@ import util from 'util'; import path from 'path'; import url from 'url'; import { colors, debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; -import type { TestInfoError, Location } from './common/types'; +import type { TestInfoError } from './../types/test'; +import type { Location } from './../types/testReporter'; import { calculateSha1, isRegExp, isString } from 'playwright-core/lib/utils'; import type { RawStack } from 'playwright-core/lib/utils'; diff --git a/packages/playwright-test/src/worker/fixtureRunner.ts b/packages/playwright-test/src/worker/fixtureRunner.ts index f42581b23f..93468b7bdd 100644 --- a/packages/playwright-test/src/worker/fixtureRunner.ts +++ b/packages/playwright-test/src/worker/fixtureRunner.ts @@ -15,11 +15,12 @@ */ import { formatLocation, debugTest } from '../util'; -import type { Location, WorkerInfo } from '../common/types'; import { ManualPromise } from 'playwright-core/lib/utils'; import type { TestInfoImpl } from './testInfo'; import type { FixtureDescription, TimeoutManager } from './timeoutManager'; import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures'; +import type { WorkerInfo } from '../../types/test'; +import type { Location } from '../../types/testReporter'; class Fixture { runner: FixtureRunner; diff --git a/packages/playwright-test/src/worker/testInfo.ts b/packages/playwright-test/src/worker/testInfo.ts index 5052bd2ec8..7b8327e971 100644 --- a/packages/playwright-test/src/worker/testInfo.ts +++ b/packages/playwright-test/src/worker/testInfo.ts @@ -17,11 +17,12 @@ import fs from 'fs'; import path from 'path'; import { captureRawStack, monotonicTime, zones } from 'playwright-core/lib/utils'; -import type { TestInfoError, TestInfo, TestStatus } from '../../types/test'; +import type { TestInfoError, TestInfo, TestStatus, FullProject, FullConfig } from '../../types/test'; import type { StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; import { TimeoutManager } from './timeoutManager'; -import type { Annotation, FullConfigInternal, FullProjectInternal, Location } from '../common/types'; +import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; +import type { Location } from '../../types/testReporter'; import { getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from '../util'; import type * as trace from '@trace/trace'; @@ -54,6 +55,8 @@ export class TestInfoImpl implements TestInfo { _didTimeout = false; _wasInterrupted = false; _lastStepId = 0; + readonly _projectInternal: FullProjectInternal; + readonly _configInternal: FullConfigInternal; // ------------ TestInfo fields ------------ readonly testId: string; @@ -61,8 +64,8 @@ export class TestInfoImpl implements TestInfo { readonly retry: number; readonly workerIndex: number; readonly parallelIndex: number; - readonly project: FullProjectInternal; - config: FullConfigInternal; + readonly project: FullProject; + readonly config: FullConfig; readonly title: string; readonly titlePath: string[]; readonly file: string; @@ -101,8 +104,8 @@ export class TestInfoImpl implements TestInfo { } constructor( - config: FullConfigInternal, - project: FullProjectInternal, + configInternal: FullConfigInternal, + projectInternal: FullProjectInternal, workerParams: WorkerInitParams, test: TestCase, retry: number, @@ -120,8 +123,10 @@ export class TestInfoImpl implements TestInfo { this.retry = retry; this.workerIndex = workerParams.workerIndex; this.parallelIndex = workerParams.parallelIndex; - this.project = project; - this.config = config; + this._projectInternal = projectInternal; + this.project = projectInternal.project; + this._configInternal = configInternal; + this.config = configInternal.config; this.title = test.title; this.titlePath = test.titlePath(); this.file = test.location.file; @@ -138,8 +143,8 @@ export class TestInfoImpl implements TestInfo { const fullTitleWithoutSpec = test.titlePath().slice(1).join(' '); let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec)); - if (project._internal.id) - testOutputDir += '-' + sanitizeForFilePath(project._internal.id); + if (projectInternal.id) + testOutputDir += '-' + sanitizeForFilePath(projectInternal.id); if (this.retry) testOutputDir += '-retry' + this.retry; if (this.repeatEachIndex) @@ -345,7 +350,7 @@ export class TestInfoImpl implements TestInfo { const parsedRelativeTestFilePath = path.parse(relativeTestFilePath); const projectNamePathSegment = sanitizeForFilePath(this.project.name); - const snapshotPath = this.project.snapshotPathTemplate + const snapshotPath = (this._projectInternal.snapshotPathTemplate || '') .replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir) .replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir) .replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '') @@ -358,7 +363,7 @@ export class TestInfoImpl implements TestInfo { .replace(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name)) .replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : ''); - return path.normalize(path.resolve(this.config._internal.configDir, snapshotPath)); + return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath)); } skip(...args: [arg?: any, description?: string]) { diff --git a/packages/playwright-test/src/worker/timeoutManager.ts b/packages/playwright-test/src/worker/timeoutManager.ts index 8ec290857c..effaac4c3a 100644 --- a/packages/playwright-test/src/worker/timeoutManager.ts +++ b/packages/playwright-test/src/worker/timeoutManager.ts @@ -16,7 +16,8 @@ import { colors } from 'playwright-core/lib/utilsBundle'; import { TimeoutRunner, TimeoutRunnerError } from 'playwright-core/lib/utils'; -import type { Location, TestInfoError } from '../common/types'; +import type { TestInfoError } from '../../types/test'; +import type { Location } from '../../types/testReporter'; export type TimeSlot = { timeout: number; diff --git a/packages/playwright-test/src/worker/workerMain.ts b/packages/playwright-test/src/worker/workerMain.ts index 98a97be100..d92ab2b87c 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, setIsWorkerProcess } from '../common/globals'; import { ConfigLoader } from '../common/configLoader'; import type { Suite, TestCase } from '../common/test'; -import type { Annotation, FullConfigInternal, FullProjectInternal, TestInfoError } from '../common/types'; +import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import { FixtureRunner } from './fixtureRunner'; import { ManualPromise } from 'playwright-core/lib/utils'; import { TestInfoImpl } from './testInfo'; @@ -31,6 +31,7 @@ import { loadTestFile } from '../common/testLoader'; import { buildFileSuiteForProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { PoolBuilder } from '../common/poolBuilder'; import { addToCompilationCache } from '../common/compilationCache'; +import type { TestInfoError } from '../../types/test'; const removeFolderAsync = util.promisify(rimraf); @@ -150,7 +151,7 @@ export class WorkerMain extends ProcessRunner { private async _teardownScopes() { // TODO: separate timeout for teardown? - const timeoutManager = new TimeoutManager(this._project.timeout); + const timeoutManager = new TimeoutManager(this._project.project.timeout); timeoutManager.setCurrentRunnable({ type: 'teardown' }); const timeoutError = await timeoutManager.runWithTimeout(async () => { await this._fixtureRunner.teardownScope('test', timeoutManager); @@ -189,9 +190,8 @@ export class WorkerMain extends ProcessRunner { if (this._config) return; - const configLoader = await ConfigLoader.deserialize(this._params.config); - this._config = configLoader.fullConfig(); - this._project = this._config.projects.find(p => p._internal.id === this._params.projectId)!; + this._config = await ConfigLoader.deserialize(this._params.config); + this._project = this._config.projects.find(p => p.id === this._params.projectId)!; this._poolBuilder = PoolBuilder.createForWorker(this._project); } @@ -201,7 +201,7 @@ export class WorkerMain extends ProcessRunner { let fatalUnknownTestIds; try { await this._loadIfNeeded(); - const fileSuite = await loadTestFile(runPayload.file, this._config.rootDir); + const fileSuite = await loadTestFile(runPayload.file, this._config.config.rootDir); const suite = buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex); const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id)); if (hasEntries) { @@ -332,7 +332,7 @@ export class WorkerMain extends ProcessRunner { this._extraSuiteAnnotations.set(suite, extraAnnotations); didFailBeforeAllForSuite = suite; // Assume failure, unless reset below. // Separate timeout for each "beforeAll" modifier. - const timeSlot = { timeout: this._project.timeout, elapsed: 0 }; + const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 }; await this._runModifiersForSuite(suite, testInfo, 'worker', timeSlot, extraAnnotations); } @@ -385,7 +385,7 @@ export class WorkerMain extends ProcessRunner { let afterHooksSlot: TimeSlot | undefined; if (testInfo._didTimeout) { // A timed-out test gets a full additional timeout to run after hooks. - afterHooksSlot = { timeout: this._project.timeout, elapsed: 0 }; + afterHooksSlot = { timeout: this._project.project.timeout, elapsed: 0 }; testInfo._timeoutManager.setCurrentRunnable({ type: 'afterEach', slot: afterHooksSlot }); } await testInfo._runAsStep(async step => { @@ -450,7 +450,7 @@ export class WorkerMain extends ProcessRunner { const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo); firstAfterHooksError = firstAfterHooksError || afterAllError; } - const teardownSlot = { timeout: this._project.timeout, elapsed: 0 }; + const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 }; // Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue. testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot }); debugTest(`tearing down test scope started`); @@ -476,8 +476,8 @@ export class WorkerMain extends ProcessRunner { setCurrentTestInfo(null); this.dispatchEvent('testEnd', buildTestEndPayload(testInfo)); - const preserveOutput = this._config.preserveOutput === 'always' || - (this._config.preserveOutput === 'failures-only' && testInfo._isFailure()); + const preserveOutput = this._config.config.preserveOutput === 'always' || + (this._config.config.preserveOutput === 'failures-only' && testInfo._isFailure()); if (!preserveOutput) await removeFolderAsync(testInfo.outputDir).catch(e => {}); } @@ -512,7 +512,7 @@ export class WorkerMain extends ProcessRunner { debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`); try { // Separate time slot for each "beforeAll" hook. - const timeSlot = { timeout: this._project.timeout, elapsed: 0 }; + const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 }; testInfo._timeoutManager.setCurrentRunnable({ type: 'beforeAll', location: hook.location, slot: timeSlot }); await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), { category: 'hook', @@ -540,7 +540,7 @@ export class WorkerMain extends ProcessRunner { debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`); const afterAllError = await testInfo._runFn(async () => { // Separate time slot for each "afterAll" hook. - const timeSlot = { timeout: this._project.timeout, elapsed: 0 }; + const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 }; testInfo._timeoutManager.setCurrentRunnable({ type: 'afterAll', location: hook.location, slot: timeSlot }); await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), { category: 'hook',