mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: simplify full config and full project building (#22278)
This commit is contained in:
		
							parent
							
								
									8d2502ee62
								
							
						
					
					
						commit
						3b94bb6740
					
				| @ -28,7 +28,7 @@ import { ConfigLoader, kDefaultConfigFiles, resolveConfigFile } from './common/c | |||||||
| import type { ConfigCLIOverrides } from './common/ipc'; | import type { ConfigCLIOverrides } from './common/ipc'; | ||||||
| import type { FullResult } from '../reporter'; | import type { FullResult } from '../reporter'; | ||||||
| import type { TraceMode } from '../types/test'; | import type { TraceMode } from '../types/test'; | ||||||
| import { baseFullConfig, builtInReporters, defaultTimeout } from './common/config'; | import { builtInReporters, defaultReporter, defaultTimeout } from './common/config'; | ||||||
| import type { FullConfigInternal } from './common/config'; | import type { FullConfigInternal } from './common/config'; | ||||||
| 
 | 
 | ||||||
| export function addTestCommands(program: Command) { | export function addTestCommands(program: Command) { | ||||||
| @ -117,41 +117,13 @@ Examples: | |||||||
| async function runTests(args: string[], opts: { [key: string]: any }) { | async function runTests(args: string[], opts: { [key: string]: any }) { | ||||||
|   await startProfiling(); |   await startProfiling(); | ||||||
| 
 | 
 | ||||||
|   const overrides = overridesFromOptions(opts); |  | ||||||
|   if (opts.browser) { |  | ||||||
|     const browserOpt = opts.browser.toLowerCase(); |  | ||||||
|     if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt)) |  | ||||||
|       throw new Error(`Unsupported browser "${opts.browser}", must be one of "all", "chromium", "firefox" or "webkit"`); |  | ||||||
|     const browserNames = browserOpt === 'all' ? ['chromium', 'firefox', 'webkit'] : [browserOpt]; |  | ||||||
|     overrides.projects = browserNames.map(browserName => { |  | ||||||
|       return { |  | ||||||
|         name: browserName, |  | ||||||
|         use: { browserName }, |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (opts.headed || opts.debug) |  | ||||||
|     overrides.use = { headless: false }; |  | ||||||
|   if (!opts.ui && opts.debug) { |  | ||||||
|     overrides.maxFailures = 1; |  | ||||||
|     overrides.timeout = 0; |  | ||||||
|     overrides.workers = 1; |  | ||||||
|     process.env.PWDEBUG = '1'; |  | ||||||
|   } |  | ||||||
|   if (!opts.ui && opts.trace) { |  | ||||||
|     if (!kTraceModes.includes(opts.trace)) |  | ||||||
|       throw new Error(`Unsupported trace mode "${opts.trace}", must be one of ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`); |  | ||||||
|     overrides.use = overrides.use || {}; |  | ||||||
|     overrides.use.trace = opts.trace; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // When no --config option is passed, let's look for the config file in the current directory.
 |   // When no --config option is passed, let's look for the config file in the current directory.
 | ||||||
|   const configFileOrDirectory = opts.config ? path.resolve(process.cwd(), opts.config) : process.cwd(); |   const configFileOrDirectory = opts.config ? path.resolve(process.cwd(), opts.config) : process.cwd(); | ||||||
|   const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); |   const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); | ||||||
|   if (restartWithExperimentalTsEsm(resolvedConfigFile)) |   if (restartWithExperimentalTsEsm(resolvedConfigFile)) | ||||||
|     return; |     return; | ||||||
| 
 | 
 | ||||||
|  |   const overrides = overridesFromOptions(opts); | ||||||
|   const configLoader = new ConfigLoader(overrides); |   const configLoader = new ConfigLoader(overrides); | ||||||
|   let config: FullConfigInternal; |   let config: FullConfigInternal; | ||||||
|   if (resolvedConfigFile) |   if (resolvedConfigFile) | ||||||
| @ -162,9 +134,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { | |||||||
|   config.cliArgs = args; |   config.cliArgs = args; | ||||||
|   config.cliGrep = opts.grep as string | undefined; |   config.cliGrep = opts.grep as string | undefined; | ||||||
|   config.cliGrepInvert = opts.grepInvert as string | undefined; |   config.cliGrepInvert = opts.grepInvert as string | undefined; | ||||||
|   config.listOnly = !!opts.list; |   config.cliListOnly = !!opts.list; | ||||||
|   config.cliProjectFilter = opts.project || undefined; |   config.cliProjectFilter = opts.project || undefined; | ||||||
|   config.passWithNoTests = !!opts.passWithNoTests; |   config.cliPassWithNoTests = !!opts.passWithNoTests; | ||||||
| 
 | 
 | ||||||
|   const runner = new Runner(config); |   const runner = new Runner(config); | ||||||
|   let status: FullResult['status']; |   let status: FullResult['status']; | ||||||
| @ -218,7 +190,7 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string] | |||||||
| 
 | 
 | ||||||
| function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { | function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides { | ||||||
|   const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; |   const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined; | ||||||
|   return { |   const overrides: ConfigCLIOverrides = { | ||||||
|     forbidOnly: options.forbidOnly ? true : undefined, |     forbidOnly: options.forbidOnly ? true : undefined, | ||||||
|     fullyParallel: options.fullyParallel ? true : undefined, |     fullyParallel: options.fullyParallel ? true : undefined, | ||||||
|     globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined, |     globalTimeout: options.globalTimeout ? parseInt(options.globalTimeout, 10) : undefined, | ||||||
| @ -234,6 +206,35 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid | |||||||
|     updateSnapshots: options.updateSnapshots ? 'all' as const : undefined, |     updateSnapshots: options.updateSnapshots ? 'all' as const : undefined, | ||||||
|     workers: options.workers, |     workers: options.workers, | ||||||
|   }; |   }; | ||||||
|  | 
 | ||||||
|  |   if (options.browser) { | ||||||
|  |     const browserOpt = options.browser.toLowerCase(); | ||||||
|  |     if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt)) | ||||||
|  |       throw new Error(`Unsupported browser "${options.browser}", must be one of "all", "chromium", "firefox" or "webkit"`); | ||||||
|  |     const browserNames = browserOpt === 'all' ? ['chromium', 'firefox', 'webkit'] : [browserOpt]; | ||||||
|  |     overrides.projects = browserNames.map(browserName => { | ||||||
|  |       return { | ||||||
|  |         name: browserName, | ||||||
|  |         use: { browserName }, | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (options.headed || options.debug) | ||||||
|  |     overrides.use = { headless: false }; | ||||||
|  |   if (!options.ui && options.debug) { | ||||||
|  |     overrides.maxFailures = 1; | ||||||
|  |     overrides.timeout = 0; | ||||||
|  |     overrides.workers = 1; | ||||||
|  |     process.env.PWDEBUG = '1'; | ||||||
|  |   } | ||||||
|  |   if (!options.ui && options.trace) { | ||||||
|  |     if (!kTraceModes.includes(options.trace)) | ||||||
|  |       throw new Error(`Unsupported trace mode "${options.trace}", must be one of ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`); | ||||||
|  |     overrides.use = overrides.use || {}; | ||||||
|  |     overrides.use.trace = options.trace; | ||||||
|  |   } | ||||||
|  |   return overrides; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function resolveReporter(id: string) { | function resolveReporter(id: string) { | ||||||
| @ -295,7 +296,7 @@ const testOptions: [string, string][] = [ | |||||||
|   ['--project <project-name...>', `Only run tests from the specified list of projects (default: run all projects)`], |   ['--project <project-name...>', `Only run tests from the specified list of projects (default: run all projects)`], | ||||||
|   ['--quiet', `Suppress stdio`], |   ['--quiet', `Suppress stdio`], | ||||||
|   ['--repeat-each <N>', `Run each test N times (default: 1)`], |   ['--repeat-each <N>', `Run each test N times (default: 1)`], | ||||||
|   ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${baseFullConfig.reporter[0]}")`], |   ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`], | ||||||
|   ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], |   ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], | ||||||
|   ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], |   ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], | ||||||
|   ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`], |   ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})`], | ||||||
|  | |||||||
| @ -14,13 +14,14 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | import fs from 'fs'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import os from 'os'; | import os from 'os'; | ||||||
| import type { Config, Fixtures, Project, ReporterDescription } from '../../types/test'; | import type { Config, Fixtures, Project, ReporterDescription } from '../../types/test'; | ||||||
| import type { Location } from '../../types/testReporter'; | import type { Location } from '../../types/testReporter'; | ||||||
| import type { TestRunnerPluginRegistration } from '../plugins'; | import type { TestRunnerPluginRegistration } from '../plugins'; | ||||||
|  | import { getPackageJsonPath, mergeObjects } from '../util'; | ||||||
| import type { Matcher } from '../util'; | import type { Matcher } from '../util'; | ||||||
| import { mergeObjects } from '../util'; |  | ||||||
| import type { ConfigCLIOverrides } from './ipc'; | import type { ConfigCLIOverrides } from './ipc'; | ||||||
| import type { FullConfig, FullProject } from '../../types/test'; | import type { FullConfig, FullProject } from '../../types/test'; | ||||||
| 
 | 
 | ||||||
| @ -34,54 +35,68 @@ export const defaultTimeout = 30000; | |||||||
| 
 | 
 | ||||||
| export class FullConfigInternal { | export class FullConfigInternal { | ||||||
|   readonly config: FullConfig; |   readonly config: FullConfig; | ||||||
|   globalOutputDir = path.resolve(process.cwd()); |   readonly globalOutputDir: string; | ||||||
|   configDir = ''; |   readonly configDir: string; | ||||||
|   configCLIOverrides: ConfigCLIOverrides = {}; |   readonly configCLIOverrides: ConfigCLIOverrides; | ||||||
|   storeDir = ''; |   readonly storeDir: string; | ||||||
|   ignoreSnapshots = false; |   readonly ignoreSnapshots: boolean; | ||||||
|   webServers: Exclude<FullConfig['webServer'], null>[] = []; |   readonly webServers: Exclude<FullConfig['webServer'], null>[]; | ||||||
|   plugins: TestRunnerPluginRegistration[] = []; |   readonly plugins: TestRunnerPluginRegistration[]; | ||||||
|   listOnly = false; |   readonly projects: FullProjectInternal[] = []; | ||||||
|   cliArgs: string[] = []; |   cliArgs: string[] = []; | ||||||
|   cliGrep: string | undefined; |   cliGrep: string | undefined; | ||||||
|   cliGrepInvert: string | undefined; |   cliGrepInvert: string | undefined; | ||||||
|   cliProjectFilter?: string[]; |   cliProjectFilter?: string[]; | ||||||
|  |   cliListOnly = false; | ||||||
|  |   cliPassWithNoTests?: boolean; | ||||||
|   testIdMatcher?: Matcher; |   testIdMatcher?: Matcher; | ||||||
|   passWithNoTests?: boolean; |  | ||||||
|   defineConfigWasUsed = false; |   defineConfigWasUsed = false; | ||||||
|   projects: FullProjectInternal[] = []; |  | ||||||
| 
 | 
 | ||||||
|   static from(config: FullConfig): FullConfigInternal { |   static from(config: FullConfig): FullConfigInternal { | ||||||
|     return (config as any)[configInternalSymbol]; |     return (config as any)[configInternalSymbol]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   constructor(configDir: string, configFile: string | undefined, config: Config, throwawayArtifactsPath: string) { |   constructor(configDir: string, configFile: string | undefined, config: Config, configCLIOverrides: ConfigCLIOverrides) { | ||||||
|  |     if (configCLIOverrides.projects && config.projects) | ||||||
|  |       throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); | ||||||
|  | 
 | ||||||
|  |     const packageJsonPath = getPackageJsonPath(configDir); | ||||||
|  |     const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined; | ||||||
|  |     const throwawayArtifactsPath = packageJsonDir || process.cwd(); | ||||||
|  | 
 | ||||||
|     this.configDir = configDir; |     this.configDir = configDir; | ||||||
|     this.config = { ...baseFullConfig }; |     this.configCLIOverrides = configCLIOverrides; | ||||||
|     (this.config as any)[configInternalSymbol] = this; |  | ||||||
|     this.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright'); |     this.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright'); | ||||||
|     this.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, path.resolve(process.cwd())); |     this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, config.outputDir), throwawayArtifactsPath, path.resolve(process.cwd())); | ||||||
|     this.ignoreSnapshots = takeFirst(config.ignoreSnapshots, false); |     this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots, false); | ||||||
|     this.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p })); |     this.plugins = ((config as any)._plugins || []).map((p: any) => ({ factory: p })); | ||||||
| 
 | 
 | ||||||
|     this.config.configFile = configFile; |     this.config = { | ||||||
|     this.config.rootDir = config.testDir || configDir; |       configFile, | ||||||
|     this.config.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); |       rootDir: pathResolve(configDir, config.testDir) || configDir, | ||||||
|     this.config.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel); |       forbidOnly: takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly, false), | ||||||
|     this.config.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup); |       fullyParallel: takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel, false), | ||||||
|     this.config.globalTeardown = takeFirst(config.globalTeardown, baseFullConfig.globalTeardown); |       globalSetup: takeFirst(resolveScript(config.globalSetup, configDir), null), | ||||||
|     this.config.globalTimeout = takeFirst(config.globalTimeout, baseFullConfig.globalTimeout); |       globalTeardown: takeFirst(resolveScript(config.globalTeardown, configDir), null), | ||||||
|     this.config.grep = takeFirst(config.grep, baseFullConfig.grep); |       globalTimeout: takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout, 0), | ||||||
|     this.config.grepInvert = takeFirst(config.grepInvert, baseFullConfig.grepInvert); |       grep: takeFirst(config.grep, defaultGrep), | ||||||
|     this.config.maxFailures = takeFirst(config.maxFailures, baseFullConfig.maxFailures); |       grepInvert: takeFirst(config.grepInvert, null), | ||||||
|     this.config.preserveOutput = takeFirst(config.preserveOutput, baseFullConfig.preserveOutput); |       maxFailures: takeFirst(configCLIOverrides.maxFailures, config.maxFailures, 0), | ||||||
|     this.config.reporter = takeFirst(resolveReporters(config.reporter, configDir), baseFullConfig.reporter); |       metadata: takeFirst(config.metadata, {}), | ||||||
|     this.config.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests); |       preserveOutput: takeFirst(config.preserveOutput, 'always'), | ||||||
|     this.config.quiet = takeFirst(config.quiet, baseFullConfig.quiet); |       reporter: takeFirst(configCLIOverrides.reporter ? toReporters(configCLIOverrides.reporter as any) : undefined, resolveReporters(config.reporter, configDir), [[defaultReporter]]), | ||||||
|     this.config.shard = takeFirst(config.shard, baseFullConfig.shard); |       reportSlowTests: takeFirst(config.reportSlowTests, { max: 5, threshold: 15000 }), | ||||||
|     this.config.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); |       quiet: takeFirst(configCLIOverrides.quiet, config.quiet, false), | ||||||
|  |       projects: [], | ||||||
|  |       shard: takeFirst(configCLIOverrides.shard, config.shard, null), | ||||||
|  |       updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, config.updateSnapshots, 'missing'), | ||||||
|  |       version: require('../../package.json').version, | ||||||
|  |       workers: 0, | ||||||
|  |       webServer: null, | ||||||
|  |     }; | ||||||
|  |     (this.config as any)[configInternalSymbol] = this; | ||||||
| 
 | 
 | ||||||
|     const workers = takeFirst(config.workers, '50%'); |     const workers = takeFirst(configCLIOverrides.workers, config.workers, '50%'); | ||||||
|     if (typeof workers === 'string') { |     if (typeof workers === 'string') { | ||||||
|       if (workers.endsWith('%')) { |       if (workers.endsWith('%')) { | ||||||
|         const cpus = os.cpus().length; |         const cpus = os.cpus().length; | ||||||
| @ -93,7 +108,7 @@ export class FullConfigInternal { | |||||||
|       this.config.workers = workers; |       this.config.workers = workers; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const webServers = takeFirst(config.webServer, baseFullConfig.webServer); |     const webServers = takeFirst(config.webServer, null); | ||||||
|     if (Array.isArray(webServers)) { // multiple web server mode
 |     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.
 |       // 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.config.webServer = null; | ||||||
| @ -101,9 +116,12 @@ export class FullConfigInternal { | |||||||
|     } else if (webServers) { // legacy singleton mode
 |     } else if (webServers) { // legacy singleton mode
 | ||||||
|       this.config.webServer = webServers; |       this.config.webServer = webServers; | ||||||
|       this.webServers = [webServers]; |       this.webServers = [webServers]; | ||||||
|  |     } else { | ||||||
|  |       this.webServers = []; | ||||||
|     } |     } | ||||||
|     this.config.metadata = takeFirst(config.metadata, baseFullConfig.metadata); | 
 | ||||||
|     this.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath)); |     const projectConfigs = configCLIOverrides.projects || config.projects || [config]; | ||||||
|  |     this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, config, this, p, this.configCLIOverrides, throwawayArtifactsPath)); | ||||||
|     resolveProjectDependencies(this.projects); |     resolveProjectDependencies(this.projects); | ||||||
|     this._assignUniqueProjectIds(this.projects); |     this._assignUniqueProjectIds(this.projects); | ||||||
|     this.config.projects = this.projects.map(p => p.project); |     this.config.projects = this.projects.map(p => p.project); | ||||||
| @ -123,91 +141,51 @@ export class FullConfigInternal { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   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 { | export class FullProjectInternal { | ||||||
|   readonly project: FullProject; |   readonly project: FullProject; | ||||||
|  |   readonly fullConfig: FullConfigInternal; | ||||||
|  |   readonly fullyParallel: boolean; | ||||||
|  |   readonly expect: Project['expect']; | ||||||
|  |   readonly respectGitIgnore: boolean; | ||||||
|  |   readonly snapshotPathTemplate: string; | ||||||
|   id = ''; |   id = ''; | ||||||
|   fullConfig: FullConfigInternal; |  | ||||||
|   fullyParallel: boolean; |  | ||||||
|   expect: Project['expect']; |  | ||||||
|   respectGitIgnore: boolean; |  | ||||||
|   deps: FullProjectInternal[] = []; |   deps: FullProjectInternal[] = []; | ||||||
|   snapshotPathTemplate: string; |  | ||||||
| 
 | 
 | ||||||
|   static from(project: FullProject): FullProjectInternal { |   static from(project: FullProject): FullProjectInternal { | ||||||
|     return (project as any)[projectInternalSymbol]; |     return (project as any)[projectInternalSymbol]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   constructor(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string) { |   constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, throwawayArtifactsPath: string) { | ||||||
|     this.fullConfig = fullConfig; |     this.fullConfig = fullConfig; | ||||||
| 
 |     const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir); | ||||||
|     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}'; |     const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; | ||||||
|     this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); |     this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); | ||||||
| 
 | 
 | ||||||
|     this.project = { |     this.project = { | ||||||
|       grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), |       grep: takeFirst(projectConfig.grep, config.grep, defaultGrep), | ||||||
|       grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert), |       grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, null), | ||||||
|       outputDir, |       outputDir: takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, projectConfig.outputDir), pathResolve(configDir, config.outputDir), path.join(throwawayArtifactsPath, 'test-results')), | ||||||
|       repeatEach: takeFirst(projectConfig.repeatEach, config.repeatEach, 1), |       repeatEach: takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach, config.repeatEach, 1), | ||||||
|       retries: takeFirst(projectConfig.retries, config.retries, 0), |       retries: takeFirst(configCLIOverrides.retries, projectConfig.retries, config.retries, 0), | ||||||
|       metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), |       metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), | ||||||
|       name, |       name: takeFirst(projectConfig.name, config.name, ''), | ||||||
|       testDir, |       testDir, | ||||||
|       snapshotDir, |       snapshotDir: takeFirst(pathResolve(configDir, projectConfig.snapshotDir), pathResolve(configDir, config.snapshotDir), testDir), | ||||||
|       testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []), |       testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []), | ||||||
|       testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).?(m)[jt]s?(x)'), |       testMatch: takeFirst(projectConfig.testMatch, config.testMatch, '**/?(*.)@(spec|test).?(m)[jt]s?(x)'), | ||||||
|       timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout), |       timeout: takeFirst(configCLIOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout), | ||||||
|       use: mergeObjects(config.use, projectConfig.use), |       use: mergeObjects(config.use, projectConfig.use, configCLIOverrides.use), | ||||||
|       dependencies: projectConfig.dependencies || [], |       dependencies: projectConfig.dependencies || [], | ||||||
|     }; |     }; | ||||||
|     (this.project as any)[projectInternalSymbol] = this; |     (this.project as any)[projectInternalSymbol] = this; | ||||||
|     this.fullyParallel = takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined); |     this.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined); | ||||||
|     this.expect = takeFirst(projectConfig.expect, config.expect, {}); |     this.expect = takeFirst(projectConfig.expect, config.expect, {}); | ||||||
|     this.respectGitIgnore = !projectConfig.testDir && !config.testDir; |     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<T>(...args: (T | undefined)[]): T { | export function takeFirst<T>(...args: (T | undefined)[]): T { | ||||||
|   for (const arg of args) { |   for (const arg of args) { | ||||||
|     if (arg !== undefined) |     if (arg !== undefined) | ||||||
| @ -216,6 +194,12 @@ export function takeFirst<T>(...args: (T | undefined)[]): T { | |||||||
|   return undefined as any as T; |   return undefined as any as T; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function pathResolve(baseDir: string, relative: string | undefined): string | undefined { | ||||||
|  |   if (!relative) | ||||||
|  |     return undefined; | ||||||
|  |   return path.resolve(baseDir, relative); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[] | undefined { | function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[] | undefined { | ||||||
|   return toReporters(reporters as any)?.map(([id, arg]) => { |   return toReporters(reporters as any)?.map(([id, arg]) => { | ||||||
|     if (builtInReporters.includes(id as any)) |     if (builtInReporters.includes(id as any)) | ||||||
| @ -250,5 +234,17 @@ export type BuiltInReporter = typeof builtInReporters[number]; | |||||||
| 
 | 
 | ||||||
| export type ContextReuseMode = 'none' | 'force' | 'when-possible'; | export type ContextReuseMode = 'none' | 'force' | 'when-possible'; | ||||||
| 
 | 
 | ||||||
|  | function resolveScript(id: string | undefined, rootDir: string): string | undefined { | ||||||
|  |   if (!id) | ||||||
|  |     return undefined; | ||||||
|  |   const localPath = path.resolve(rootDir, id); | ||||||
|  |   if (fs.existsSync(localPath)) | ||||||
|  |     return localPath; | ||||||
|  |   return require.resolve(id, { paths: [rootDir] }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const defaultGrep = /.*/; | ||||||
|  | export const defaultReporter = process.env.CI ? 'dot' : 'list'; | ||||||
|  | 
 | ||||||
| const configInternalSymbol = Symbol('configInternalSymbol'); | const configInternalSymbol = Symbol('configInternalSymbol'); | ||||||
| const projectInternalSymbol = Symbol('projectInternalSymbol'); | const projectInternalSymbol = Symbol('projectInternalSymbol'); | ||||||
|  | |||||||
| @ -20,9 +20,9 @@ import { isRegExp } from 'playwright-core/lib/utils'; | |||||||
| import type { ConfigCLIOverrides, SerializedConfig } from './ipc'; | import type { ConfigCLIOverrides, SerializedConfig } from './ipc'; | ||||||
| import { requireOrImport } from './transform'; | import { requireOrImport } from './transform'; | ||||||
| import type { Config, Project } from '../../types/test'; | import type { Config, Project } from '../../types/test'; | ||||||
| import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util'; | import { errorWithFile } from '../util'; | ||||||
| import { setCurrentConfig } from './globals'; | import { setCurrentConfig } from './globals'; | ||||||
| import { FullConfigInternal, takeFirst, toReporters } from './config'; | import { FullConfigInternal } from './config'; | ||||||
| 
 | 
 | ||||||
| const kDefineConfigWasUsed = Symbol('defineConfigWasUsed'); | const kDefineConfigWasUsed = Symbol('defineConfigWasUsed'); | ||||||
| export const defineConfig = (config: any) => { | export const defineConfig = (config: any) => { | ||||||
| @ -68,62 +68,10 @@ export class ConfigLoader { | |||||||
|   private async _loadConfig(config: Config, configDir: string, configFile?: string): Promise<FullConfigInternal> { |   private async _loadConfig(config: Config, configDir: string, configFile?: string): Promise<FullConfigInternal> { | ||||||
|     // 1. Validate data provided in the config file.
 |     // 1. Validate data provided in the config file.
 | ||||||
|     validateConfig(configFile || '<default config>', config); |     validateConfig(configFile || '<default config>', config); | ||||||
| 
 |     const fullConfig = new FullConfigInternal(configDir, configFile, config, this._configCLIOverrides); | ||||||
|     // 2. Override settings from CLI.
 |  | ||||||
|     config.forbidOnly = takeFirst(this._configCLIOverrides.forbidOnly, config.forbidOnly); |  | ||||||
|     config.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, config.fullyParallel); |  | ||||||
|     config.globalTimeout = takeFirst(this._configCLIOverrides.globalTimeout, config.globalTimeout); |  | ||||||
|     config.maxFailures = takeFirst(this._configCLIOverrides.maxFailures, config.maxFailures); |  | ||||||
|     config.outputDir = takeFirst(this._configCLIOverrides.outputDir, config.outputDir); |  | ||||||
|     config.quiet = takeFirst(this._configCLIOverrides.quiet, config.quiet); |  | ||||||
|     config.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, config.repeatEach); |  | ||||||
|     config.retries = takeFirst(this._configCLIOverrides.retries, config.retries); |  | ||||||
|     if (this._configCLIOverrides.reporter) |  | ||||||
|       config.reporter = toReporters(this._configCLIOverrides.reporter as any); |  | ||||||
|     config.shard = takeFirst(this._configCLIOverrides.shard, config.shard); |  | ||||||
|     config.timeout = takeFirst(this._configCLIOverrides.timeout, config.timeout); |  | ||||||
|     config.updateSnapshots = takeFirst(this._configCLIOverrides.updateSnapshots, config.updateSnapshots); |  | ||||||
|     config.ignoreSnapshots = takeFirst(this._configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots); |  | ||||||
|     if (this._configCLIOverrides.projects && config.projects) |  | ||||||
|       throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); |  | ||||||
|     config.projects = takeFirst(this._configCLIOverrides.projects, config.projects as any); |  | ||||||
|     config.workers = takeFirst(this._configCLIOverrides.workers, config.workers); |  | ||||||
|     config.use = mergeObjects(config.use, this._configCLIOverrides.use); |  | ||||||
|     for (const project of config.projects || []) |  | ||||||
|       this._applyCLIOverridesToProject(project); |  | ||||||
| 
 |  | ||||||
|     // 3. Resolve config.
 |  | ||||||
|     const packageJsonPath = getPackageJsonPath(configDir); |  | ||||||
|     const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined; |  | ||||||
|     const throwawayArtifactsPath = packageJsonDir || process.cwd(); |  | ||||||
| 
 |  | ||||||
|     // Resolve script hooks relative to the root dir.
 |  | ||||||
|     if (config.globalSetup) |  | ||||||
|       config.globalSetup = resolveScript(config.globalSetup, configDir); |  | ||||||
|     if (config.globalTeardown) |  | ||||||
|       config.globalTeardown = resolveScript(config.globalTeardown, configDir); |  | ||||||
|     // Resolve all config dirs relative to configDir.
 |  | ||||||
|     if (config.testDir !== undefined) |  | ||||||
|       config.testDir = path.resolve(configDir, config.testDir); |  | ||||||
|     if (config.outputDir !== undefined) |  | ||||||
|       config.outputDir = path.resolve(configDir, config.outputDir); |  | ||||||
|     if (config.snapshotDir !== undefined) |  | ||||||
|       config.snapshotDir = path.resolve(configDir, config.snapshotDir); |  | ||||||
| 
 |  | ||||||
|     const fullConfig = new FullConfigInternal(configDir, configFile, config, throwawayArtifactsPath); |  | ||||||
|     fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed]; |     fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed]; | ||||||
|     fullConfig.configCLIOverrides = this._configCLIOverrides; |  | ||||||
|     return fullConfig; |     return fullConfig; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   private _applyCLIOverridesToProject(projectConfig: Project) { |  | ||||||
|     projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel); |  | ||||||
|     projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir); |  | ||||||
|     projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach); |  | ||||||
|     projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries); |  | ||||||
|     projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout); |  | ||||||
|     projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function requireOrImportDefaultObject(file: string) { | async function requireOrImportDefaultObject(file: string) { | ||||||
| @ -305,13 +253,6 @@ function validateProject(file: string, project: Project, title: string) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function resolveScript(id: string, rootDir: string) { |  | ||||||
|   const localPath = path.resolve(rootDir, id); |  | ||||||
|   if (fs.existsSync(localPath)) |  | ||||||
|     return localPath; |  | ||||||
|   return require.resolve(id, { paths: [rootDir] }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs']; | export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs']; | ||||||
| 
 | 
 | ||||||
| export function resolveConfigFile(configFileOrDirectory: string): string | null { | export function resolveConfigFile(configFileOrDirectory: string): string | null { | ||||||
|  | |||||||
| @ -123,7 +123,7 @@ export class TeleReporterEmitter implements Reporter { | |||||||
|     return { |     return { | ||||||
|       rootDir: config.rootDir, |       rootDir: config.rootDir, | ||||||
|       configFile: this._relativePath(config.configFile), |       configFile: this._relativePath(config.configFile), | ||||||
|       listOnly: FullConfigInternal.from(config).listOnly, |       listOnly: FullConfigInternal.from(config).cliListOnly, | ||||||
|       workers: config.workers, |       workers: config.workers, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -49,7 +49,7 @@ export class Runner { | |||||||
| 
 | 
 | ||||||
|   async runAllTests(): Promise<FullResult['status']> { |   async runAllTests(): Promise<FullResult['status']> { | ||||||
|     const config = this._config; |     const config = this._config; | ||||||
|     const listOnly = config.listOnly; |     const listOnly = config.cliListOnly; | ||||||
|     const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0; |     const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0; | ||||||
| 
 | 
 | ||||||
|     // Legacy webServer support.
 |     // Legacy webServer support.
 | ||||||
|  | |||||||
| @ -170,7 +170,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: | |||||||
|     await loadFileSuites(testRun, mode, errors); |     await loadFileSuites(testRun, mode, errors); | ||||||
|     testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly); |     testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly); | ||||||
|     // Fail when no tests.
 |     // Fail when no tests.
 | ||||||
|     if (!testRun.rootSuite.allTests().length && !testRun.config.passWithNoTests && !testRun.config.config.shard) |     if (!testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) | ||||||
|       throw new Error(`No tests found`); |       throw new Error(`No tests found`); | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -42,13 +42,8 @@ class UIMode { | |||||||
|   constructor(config: FullConfigInternal) { |   constructor(config: FullConfigInternal) { | ||||||
|     this._config = config; |     this._config = config; | ||||||
|     process.env.PW_LIVE_TRACE_STACKS = '1'; |     process.env.PW_LIVE_TRACE_STACKS = '1'; | ||||||
|     config.configCLIOverrides.forbidOnly = false; |     config.cliListOnly = false; | ||||||
|     config.configCLIOverrides.globalTimeout = 0; |     config.cliPassWithNoTests = true; | ||||||
|     config.configCLIOverrides.repeatEach = 0; |  | ||||||
|     config.configCLIOverrides.shard = undefined; |  | ||||||
|     config.configCLIOverrides.updateSnapshots = undefined; |  | ||||||
|     config.listOnly = false; |  | ||||||
|     config.passWithNoTests = true; |  | ||||||
|     for (const project of config.projects) |     for (const project of config.projects) | ||||||
|       project.deps = []; |       project.deps = []; | ||||||
| 
 | 
 | ||||||
| @ -148,7 +143,7 @@ class UIMode { | |||||||
|   private async _listTests() { |   private async _listTests() { | ||||||
|     const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); |     const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); | ||||||
|     const reporter = new Multiplexer([listReporter]); |     const reporter = new Multiplexer([listReporter]); | ||||||
|     this._config.listOnly = true; |     this._config.cliListOnly = true; | ||||||
|     this._config.testIdMatcher = undefined; |     this._config.testIdMatcher = undefined; | ||||||
|     const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process'); |     const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process'); | ||||||
|     const testRun = new TestRun(this._config, reporter); |     const testRun = new TestRun(this._config, reporter); | ||||||
| @ -167,7 +162,7 @@ class UIMode { | |||||||
|     await this._stopTests(); |     await this._stopTests(); | ||||||
| 
 | 
 | ||||||
|     const testIdSet = testIds ? new Set<string>(testIds) : null; |     const testIdSet = testIds ? new Set<string>(testIds) : null; | ||||||
|     this._config.listOnly = false; |     this._config.cliListOnly = false; | ||||||
|     this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); |     this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); | ||||||
| 
 | 
 | ||||||
|     const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); |     const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); | ||||||
|  | |||||||
| @ -108,7 +108,7 @@ class FSWatcher { | |||||||
| 
 | 
 | ||||||
| export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> { | export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> { | ||||||
|   // Reset the settings that don't apply to watch.
 |   // Reset the settings that don't apply to watch.
 | ||||||
|   config.passWithNoTests = true; |   config.cliPassWithNoTests = true; | ||||||
|   for (const p of config.projects) |   for (const p of config.projects) | ||||||
|     p.project.retries = 0; |     p.project.retries = 0; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -151,10 +151,10 @@ export function createTitleMatcher(patterns: RegExp | RegExp[]): Matcher { | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mergeObjects<A extends object, B extends object>(a: A | undefined | void, b: B | undefined | void): A & B { | export function mergeObjects<A extends object, B extends object, C extends object>(a: A | undefined | void, b: B | undefined | void, c: B | undefined | void): A & B & C { | ||||||
|   const result = { ...a } as any; |   const result = { ...a } as any; | ||||||
|   if (!Object.is(b, undefined)) { |   for (const x of [b, c].filter(Boolean)) { | ||||||
|     for (const [name, value] of Object.entries(b as B)) { |     for (const [name, value] of Object.entries(x as any)) { | ||||||
|       if (!Object.is(value, undefined)) |       if (!Object.is(value, undefined)) | ||||||
|         result[name] = value; |         result[name] = value; | ||||||
|     } |     } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pavel Feldman
						Pavel Feldman