chore: inside out the config & project internal (#22260)

This commit is contained in:
Pavel Feldman 2023-04-07 09:54:01 -07:00 committed by GitHub
parent 02ca63b381
commit a42567d549
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 557 additions and 559 deletions

View file

@ -23,10 +23,12 @@ import { Runner } from './runner/runner';
import { stopProfiling, startProfiling } from 'playwright-core/lib/utils'; import { stopProfiling, startProfiling } from 'playwright-core/lib/utils';
import { experimentalLoaderOption, fileIsModule } from './util'; import { experimentalLoaderOption, fileIsModule } from './util';
import { showHTMLReport } from './reporters/html'; import { showHTMLReport } from './reporters/html';
import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; import { ConfigLoader, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import type { TraceMode } from './common/types';
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 { baseFullConfig, builtInReporters, defaultTimeout } from './common/config';
import type { FullConfigInternal } from './common/config';
export function addTestCommands(program: Command) { export function addTestCommands(program: Command) {
addTestCommand(program); addTestCommand(program);
@ -127,20 +129,18 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
return; return;
const configLoader = new ConfigLoader(overrides); const configLoader = new ConfigLoader(overrides);
let config: FullConfigInternal;
if (resolvedConfigFile) if (resolvedConfigFile)
await configLoader.loadConfigFile(resolvedConfigFile); config = await configLoader.loadConfigFile(resolvedConfigFile, opts.deps === false);
else else
await configLoader.loadEmptyConfig(configFileOrDirectory); config = await configLoader.loadEmptyConfig(configFileOrDirectory);
if (opts.deps === false)
configLoader.ignoreProjectDependencies();
const config = configLoader.fullConfig(); config.cliArgs = args;
config._internal.cliArgs = args; config.cliGrep = opts.grep as string | undefined;
config._internal.cliGrep = opts.grep as string | undefined; config.cliGrepInvert = opts.grepInvert as string | undefined;
config._internal.cliGrepInvert = opts.grepInvert as string | undefined; config.listOnly = !!opts.list;
config._internal.listOnly = !!opts.list; config.cliProjectFilter = opts.project || undefined;
config._internal.cliProjectFilter = opts.project || undefined; config.passWithNoTests = !!opts.passWithNoTests;
config._internal.passWithNoTests = !!opts.passWithNoTests;
const runner = new Runner(config); const runner = new Runner(config);
let status: FullResult['status']; let status: FullResult['status'];
@ -166,8 +166,8 @@ async function listTestFiles(opts: { [key: string]: any }) {
return; return;
const configLoader = new ConfigLoader(); const configLoader = new ConfigLoader();
const runner = new Runner(configLoader.fullConfig()); const config = await configLoader.loadConfigFile(resolvedConfigFile);
await configLoader.loadConfigFile(resolvedConfigFile); const runner = new Runner(config);
const report = await runner.listTestFiles(opts.project); const report = await runner.listTestFiles(opts.project);
write(JSON.stringify(report), () => { write(JSON.stringify(report), () => {
process.exit(0); process.exit(0);

View file

@ -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<FullConfig['webServer'], null>[] = [];
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<T>(...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');

View file

@ -15,16 +15,14 @@
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { isRegExp } from 'playwright-core/lib/utils'; 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, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types'; import type { Config, Project } from '../../types/test';
import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util'; import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
import { setCurrentConfig } from './globals'; import { setCurrentConfig } from './globals';
import { FullConfigInternal, takeFirst, toReporters } from './config';
export const defaultTimeout = 30000;
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed'); const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
export const defineConfig = (config: any) => { export const defineConfig = (config: any) => {
@ -33,62 +31,64 @@ export const defineConfig = (config: any) => {
}; };
export class ConfigLoader { export class ConfigLoader {
private _fullConfig: FullConfigInternal; private _configCLIOverrides: ConfigCLIOverrides;
private _fullConfig: FullConfigInternal | undefined;
constructor(configCLIOverrides?: ConfigCLIOverrides) { constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._fullConfig = { ...baseFullConfig }; this._configCLIOverrides = configCLIOverrides || {};
this._fullConfig._internal.configCLIOverrides = configCLIOverrides || {};
} }
static async deserialize(data: SerializedConfig): Promise<ConfigLoader> { static async deserialize(data: SerializedConfig): Promise<FullConfigInternal> {
const loader = new ConfigLoader(data.configCLIOverrides); const loader = new ConfigLoader(data.configCLIOverrides);
if (data.configFile) if (data.configFile)
await loader.loadConfigFile(data.configFile); return await loader.loadConfigFile(data.configFile);
else return await loader.loadEmptyConfig(data.configDir);
await loader.loadEmptyConfig(data.configDir);
return loader;
} }
async loadConfigFile(file: string): Promise<FullConfigInternal> { async loadConfigFile(file: string, ignoreProjectDependencies = false): Promise<FullConfigInternal> {
if (this._fullConfig.configFile) if (this._fullConfig)
throw new Error('Cannot load two config files'); throw new Error('Cannot load two config files');
const config = await requireOrImportDefaultObject(file) as Config; const config = await requireOrImportDefaultObject(file) as Config;
await this._processConfigObject(config, path.dirname(file), file); const fullConfig = await this._loadConfig(config, path.dirname(file), file);
setCurrentConfig(this._fullConfig); setCurrentConfig(fullConfig);
return this._fullConfig; if (ignoreProjectDependencies) {
for (const project of fullConfig.projects)
project.deps = [];
}
this._fullConfig = fullConfig;
return fullConfig;
} }
async loadEmptyConfig(configDir: string): Promise<Config> { async loadEmptyConfig(configDir: string): Promise<FullConfigInternal> {
await this._processConfigObject({}, configDir); const fullConfig = await this._loadConfig({}, configDir);
setCurrentConfig(this._fullConfig); setCurrentConfig(fullConfig);
return {}; return fullConfig;
} }
private async _processConfigObject(config: Config, configDir: string, configFile?: string) { 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);
// 2. Override settings from CLI. // 2. Override settings from CLI.
const configCLIOverrides = this._fullConfig._internal.configCLIOverrides; config.forbidOnly = takeFirst(this._configCLIOverrides.forbidOnly, config.forbidOnly);
config.forbidOnly = takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly); config.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, config.fullyParallel);
config.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel); config.globalTimeout = takeFirst(this._configCLIOverrides.globalTimeout, config.globalTimeout);
config.globalTimeout = takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout); config.maxFailures = takeFirst(this._configCLIOverrides.maxFailures, config.maxFailures);
config.maxFailures = takeFirst(configCLIOverrides.maxFailures, config.maxFailures); config.outputDir = takeFirst(this._configCLIOverrides.outputDir, config.outputDir);
config.outputDir = takeFirst(configCLIOverrides.outputDir, config.outputDir); config.quiet = takeFirst(this._configCLIOverrides.quiet, config.quiet);
config.quiet = takeFirst(configCLIOverrides.quiet, config.quiet); config.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, config.repeatEach);
config.repeatEach = takeFirst(configCLIOverrides.repeatEach, config.repeatEach); config.retries = takeFirst(this._configCLIOverrides.retries, config.retries);
config.retries = takeFirst(configCLIOverrides.retries, config.retries); if (this._configCLIOverrides.reporter)
if (configCLIOverrides.reporter) config.reporter = toReporters(this._configCLIOverrides.reporter as any);
config.reporter = toReporters(configCLIOverrides.reporter as any); config.shard = takeFirst(this._configCLIOverrides.shard, config.shard);
config.shard = takeFirst(configCLIOverrides.shard, config.shard); config.timeout = takeFirst(this._configCLIOverrides.timeout, config.timeout);
config.timeout = takeFirst(configCLIOverrides.timeout, config.timeout); config.updateSnapshots = takeFirst(this._configCLIOverrides.updateSnapshots, config.updateSnapshots);
config.updateSnapshots = takeFirst(configCLIOverrides.updateSnapshots, config.updateSnapshots); config.ignoreSnapshots = takeFirst(this._configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots);
config.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots); if (this._configCLIOverrides.projects && config.projects)
if (configCLIOverrides.projects && config.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); 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.projects = takeFirst(this._configCLIOverrides.projects, config.projects as any);
config.workers = takeFirst(configCLIOverrides.workers, config.workers); config.workers = takeFirst(this._configCLIOverrides.workers, config.workers);
config.use = mergeObjects(config.use, configCLIOverrides.use); config.use = mergeObjects(config.use, this._configCLIOverrides.use);
for (const project of config.projects || []) for (const project of config.projects || [])
this._applyCLIOverridesToProject(project); this._applyCLIOverridesToProject(project);
@ -110,134 +110,19 @@ export class ConfigLoader {
if (config.snapshotDir !== undefined) if (config.snapshotDir !== undefined)
config.snapshotDir = path.resolve(configDir, config.snapshotDir); config.snapshotDir = path.resolve(configDir, config.snapshotDir);
this._fullConfig._internal.configDir = configDir; const fullConfig = new FullConfigInternal(configDir, configFile, config, throwawayArtifactsPath);
this._fullConfig._internal.storeDir = path.resolve(configDir, (config as any)._storeDir || 'playwright'); fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed];
this._fullConfig.configFile = configFile; fullConfig.configCLIOverrides = this._configCLIOverrides;
this._fullConfig.rootDir = config.testDir || configDir; return fullConfig;
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;
} }
private _applyCLIOverridesToProject(projectConfig: Project) { private _applyCLIOverridesToProject(projectConfig: Project) {
const configCLIOverrides = this._fullConfig._internal.configCLIOverrides; projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel); projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir);
projectConfig.outputDir = takeFirst(configCLIOverrides.outputDir, projectConfig.outputDir); projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach);
projectConfig.repeatEach = takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach); projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries);
projectConfig.retries = takeFirst(configCLIOverrides.retries, projectConfig.retries); projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout);
projectConfig.timeout = takeFirst(configCLIOverrides.timeout, projectConfig.timeout); projectConfig.use = mergeObjects(projectConfig.use, this._configCLIOverrides.use);
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 || [],
};
} }
} }
@ -248,14 +133,6 @@ async function requireOrImportDefaultObject(file: string) {
return object; return object;
} }
function takeFirst<T>(...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) { function validateConfig(file: string, config: Config) {
if (typeof config !== 'object' || !config) if (typeof config !== 'object' || !config)
throw errorWithFile(file, `Configuration file must export a single object`); 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) { function resolveScript(id: string, rootDir: string) {
const localPath = path.resolve(rootDir, id); const localPath = path.resolve(rootDir, id);
if (fs.existsSync(localPath)) if (fs.existsSync(localPath))
@ -482,19 +312,6 @@ function resolveScript(id: string, rootDir: string) {
return require.resolve(id, { paths: [rootDir] }); 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 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 {
@ -526,14 +343,3 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null
return configFile!; 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;
}

View file

@ -16,7 +16,9 @@
import { formatLocation } from '../util'; import { formatLocation } from '../util';
import * as crypto from 'crypto'; 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'; export type FixtureScope = 'test' | 'worker';
type FixtureAuto = boolean | 'all-hooks-included'; type FixtureAuto = boolean | 'all-hooks-included';

View file

@ -16,7 +16,8 @@
import type { TestInfoImpl } from '../worker/testInfo'; import type { TestInfoImpl } from '../worker/testInfo';
import type { Suite } from './test'; import type { Suite } from './test';
import type { FullConfigInternal } from './types'; import { FullProjectInternal } from './config';
import type { FullConfigInternal } from './config';
let currentTestInfoValue: TestInfoImpl | null = null; let currentTestInfoValue: TestInfoImpl | null = null;
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) { export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
@ -38,7 +39,7 @@ export function currentExpectTimeout(options: { timeout?: number }) {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (options.timeout !== undefined) if (options.timeout !== undefined)
return options.timeout; 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') if (typeof defaultExpectTimeout === 'undefined')
defaultExpectTimeout = 5000; defaultExpectTimeout = 5000;
return defaultExpectTimeout; return defaultExpectTimeout;

View file

@ -15,7 +15,8 @@
*/ */
import { serializeCompilationCache } from './compilationCache'; 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 = { export type ConfigCLIOverrides = {
forbidOnly?: boolean; forbidOnly?: boolean;
@ -126,9 +127,9 @@ export type EnvProducedPayload = [string, string | null][];
export function serializeConfig(config: FullConfigInternal): SerializedConfig { export function serializeConfig(config: FullConfigInternal): SerializedConfig {
const result: SerializedConfig = { const result: SerializedConfig = {
configFile: config.configFile, configFile: config.config.configFile,
configDir: config._internal.configDir, configDir: config.configDir,
configCLIOverrides: config._internal.configCLIOverrides, configCLIOverrides: config.configCLIOverrides,
compilationCache: serializeCompilationCache(), compilationCache: serializeCompilationCache(),
}; };
return result; return result;

View file

@ -18,7 +18,7 @@ import { FixturePool } from './fixtures';
import type { LoadError } from './fixtures'; import type { LoadError } from './fixtures';
import type { Suite, TestCase } from './test'; import type { Suite, TestCase } from './test';
import type { TestTypeImpl } from './testType'; import type { TestTypeImpl } from './testType';
import type { FullProjectInternal } from './types'; import type { FullProjectInternal } from './config';
import { formatLocation } from '../util'; import { formatLocation } from '../util';
import type { TestError } from '../../reporter'; import type { TestError } from '../../reporter';
@ -74,8 +74,8 @@ export class PoolBuilder {
private _buildTestTypePool(testType: TestTypeImpl, testErrors?: TestError[]): FixturePool { private _buildTestTypePool(testType: TestTypeImpl, testErrors?: TestError[]): FixturePool {
if (!this._testTypePools.has(testType)) { if (!this._testTypePools.has(testType)) {
const optionOverrides = { const optionOverrides = {
overrides: this._project?.use ?? {}, overrides: this._project?.project?.use ?? {},
location: { file: `project#${this._project?._internal.id}`, line: 1, column: 1 } location: { file: `project#${this._project?.id}`, line: 1, column: 1 }
}; };
const pool = new FixturePool(testType.fixtures, e => this._handleLoadError(e, testErrors), undefined, undefined, optionOverrides); const pool = new FixturePool(testType.fixtures, e => this._handleLoadError(e, testErrors), undefined, undefined, optionOverrides);
this._testTypePools.set(testType, pool); this._testTypePools.set(testType, pool);

View file

@ -17,7 +17,7 @@
import type { WriteStream } from 'tty'; import type { WriteStream } from 'tty';
import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc'; import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc';
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils'; import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
import type { TestInfoError } from './types'; import type { TestInfoError } from '../../types/test';
import { serializeError } from '../util'; import { serializeError } from '../util';
export type ProtocolRequest = { export type ProtocolRequest = {

View file

@ -17,7 +17,7 @@
import path from 'path'; import path from 'path';
import { calculateSha1 } from 'playwright-core/lib/utils'; import { calculateSha1 } from 'playwright-core/lib/utils';
import type { Suite, TestCase } from './test'; import type { Suite, TestCase } from './test';
import type { FullProjectInternal } from './types'; import type { FullProjectInternal } from './config';
import type { Matcher, TestFileFilter } from '../util'; import type { Matcher, TestFileFilter } from '../util';
import { createFileMatcher } 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 { 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); const fileId = calculateSha1(relativeFile).slice(0, 20);
// Clone suite. // Clone suite.
@ -54,11 +54,11 @@ export function buildFileSuiteForProject(project: FullProjectInternal, suite: Su
const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : ''; 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. // 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); const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
test.id = testId; test.id = testId;
test.repeatEachIndex = repeatEachIndex; test.repeatEachIndex = repeatEachIndex;
test._projectId = project._internal.id; test._projectId = project.id;
// Inherit properties from parent suites. // Inherit properties from parent suites.
let inheritedRetries: number | undefined; let inheritedRetries: number | undefined;
@ -70,8 +70,8 @@ export function buildFileSuiteForProject(project: FullProjectInternal, suite: Su
if (inheritedTimeout === undefined && parentSuite._timeout !== undefined) if (inheritedTimeout === undefined && parentSuite._timeout !== undefined)
inheritedTimeout = parentSuite._timeout; inheritedTimeout = parentSuite._timeout;
} }
test.retries = inheritedRetries ?? project.retries; test.retries = inheritedRetries ?? project.project.retries;
test.timeout = inheritedTimeout ?? project.timeout; test.timeout = inheritedTimeout ?? project.project.timeout;
// Skip annotations imply skipped expectedStatus. // Skip annotations imply skipped expectedStatus.
if (test._staticAnnotations.some(a => a.type === 'skip' || a.type === 'fixme')) 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. // We only compute / set digest in the runner.
if (test._poolDigest) if (test._poolDigest)
test._workerHash = `${project._internal.id}-${test._poolDigest}-${repeatEachIndex}`; test._workerHash = `${project.id}-${test._poolDigest}-${repeatEachIndex}`;
}); });
return result; return result;

View file

@ -19,7 +19,9 @@ import type * as reporterTypes from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
import type { TestTypeImpl } from './testType'; import type { TestTypeImpl } from './testType';
import { rootTestType } 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 { class Base {
title: string; title: string;
@ -197,7 +199,7 @@ export class Suite extends Base implements SuitePrivate {
} }
project(): FullProject | undefined { project(): FullProject | undefined {
return this._projectConfig || this.parent?.project(); return this._projectConfig?.project || this.parent?.project();
} }
} }

View file

@ -18,7 +18,9 @@ import { expect } from '../matchers/expect';
import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals'; import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals';
import { TestCase, Suite } from './test'; import { TestCase, Suite } from './test';
import { wrapFunctionWithLocation } from './transform'; 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'); const testTypeSymbol = Symbol('testType');

View file

@ -18,7 +18,7 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { sourceMapSupport, pirates } from '../utilsBundle'; import { sourceMapSupport, pirates } from '../utilsBundle';
import url from 'url'; import url from 'url';
import type { Location } from './types'; import type { Location } from '../../types/testReporter';
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader'; import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
import { tsConfigLoader } from '../third_party/tsconfig-loader'; import { tsConfigLoader } from '../third_party/tsconfig-loader';
import Module from 'module'; import Module from 'module';

View file

@ -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<FullConfigPublic['webServer'], null>[];
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';

View file

@ -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 { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo'; import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
import { type ContextReuseMode } from './common/types'; import { type ContextReuseMode } from './common/config';
import { artifactsFolderName } from './isomorphic/folders'; import { artifactsFolderName } from './isomorphic/folders';
export { expect } from './matchers/expect'; export { expect } from './matchers/expect';
export { store as _store } from './store'; export { store as _store } from './store';

View file

@ -15,7 +15,8 @@
*/ */
import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStatus, TestStep } from '../../types/testReporter'; 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 * as reporterTypes from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';

View file

@ -17,7 +17,7 @@
import type { SerializedConfig } from '../common/ipc'; import type { SerializedConfig } from '../common/ipc';
import { ConfigLoader } from '../common/configLoader'; import { ConfigLoader } from '../common/configLoader';
import { ProcessRunner } from '../common/process'; import { ProcessRunner } from '../common/process';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/config';
import { loadTestFile } from '../common/testLoader'; import { loadTestFile } from '../common/testLoader';
import type { TestError } from '../../reporter'; import type { TestError } from '../../reporter';
import { addToCompilationCache, serializeCompilationCache } from '../common/compilationCache'; import { addToCompilationCache, serializeCompilationCache } from '../common/compilationCache';
@ -36,14 +36,14 @@ export class LoaderMain extends ProcessRunner {
private _config(): Promise<FullConfigInternal> { private _config(): Promise<FullConfigInternal> {
if (!this._configPromise) if (!this._configPromise)
this._configPromise = ConfigLoader.deserialize(this._serializedConfig).then(configLoader => configLoader.fullConfig()); this._configPromise = ConfigLoader.deserialize(this._serializedConfig);
return this._configPromise; return this._configPromise;
} }
async loadTestFile(params: { file: string }) { async loadTestFile(params: { file: string }) {
const testErrors: TestError[] = []; const testErrors: TestError[] = [];
const config = await this._config(); 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); this._poolBuilder.buildPools(fileSuite);
return { fileSuite: fileSuite._deepSerialize(), testErrors }; return { fileSuite: fileSuite._deepSerialize(), testErrors };
} }

View file

@ -47,7 +47,7 @@ import {
toPass toPass
} from './matchers'; } from './matchers';
import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot'; import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot';
import type { Expect } from '../common/types'; import type { Expect } from '../../types/test';
import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import { currentTestInfo, currentExpectTimeout } from '../common/globals';
import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util'; import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util';
import { import {

View file

@ -15,7 +15,7 @@
*/ */
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import type { Expect } from '../common/types'; import type { Expect } from '../../types/test';
export function matcherHint(state: ReturnType<Expect['getState']>, matcherName: string, a: any, b: any, matcherOptions: any, timeout?: number) { export function matcherHint(state: ReturnType<Expect['getState']>, matcherName: string, a: any, b: any, matcherOptions: any, timeout?: number) {
const message = state.utils.matcherHint(matcherName, a, b, matcherOptions); const message = state.utils.matcherHint(matcherName, a, b, matcherOptions);

View file

@ -17,7 +17,7 @@
import type { Locator, Page, APIResponse } from 'playwright-core'; import type { Locator, Page, APIResponse } from 'playwright-core';
import type { FrameExpectOptions } from 'playwright-core/lib/client/types'; import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
import { colors } from 'playwright-core/lib/utilsBundle'; 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 { expectTypes, callLogText } from '../util';
import { currentTestInfo } from '../common/globals'; import { currentTestInfo } from '../common/globals';
import type { TestInfoErrorState } from '../worker/testInfo'; import type { TestInfoErrorState } from '../worker/testInfo';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Expect } from '../common/types'; import type { Expect } from '../../types/test';
import { expectTypes, callLogText } from '../util'; import { expectTypes, callLogText } from '../util';
import { matcherHint } from './matcherHint'; import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals'; import { currentExpectTimeout } from '../common/globals';

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Expect } from '../common/types'; import type { Expect } from '../../types/test';
import { expectTypes } from '../util'; import { expectTypes } from '../util';
import { callLogText } from '../util'; import { callLogText } from '../util';
import { matcherHint } from './matcherHint'; import { matcherHint } from './matcherHint';

View file

@ -17,7 +17,7 @@
import type { Locator, Page } from 'playwright-core'; import type { Locator, Page } from 'playwright-core';
import type { Page as PageEx } from 'playwright-core/lib/client/page'; import type { Page as PageEx } from 'playwright-core/lib/client/page';
import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator'; 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 { currentTestInfo, currentExpectTimeout } from '../common/globals';
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
import { getComparator } from 'playwright-core/lib/utils'; import { getComparator } from 'playwright-core/lib/utils';
@ -253,12 +253,12 @@ export function toMatchSnapshot(
if (received instanceof Promise) if (received instanceof Promise)
throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.'); 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: () => '' }; return { pass: !this.isNot, message: () => '' };
const helper = new SnapshotHelper( const helper = new SnapshotHelper(
testInfo, testInfo.snapshotPath.bind(testInfo), determineFileExtension(received), testInfo, testInfo.snapshotPath.bind(testInfo), determineFileExtension(received),
testInfo.project._internal.expect?.toMatchSnapshot || {}, testInfo._projectInternal.expect?.toMatchSnapshot || {},
nameOrOptions, optOptions); nameOrOptions, optOptions);
if (this.isNot) { if (this.isNot) {
@ -298,10 +298,10 @@ export async function toHaveScreenshot(
if (!testInfo) if (!testInfo)
throw new Error(`toHaveScreenshot() must be called during the test`); throw new Error(`toHaveScreenshot() must be called during the test`);
if (testInfo.config._internal.ignoreSnapshots) if (testInfo._configInternal.ignoreSnapshots)
return { pass: !this.isNot, message: () => '' }; 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 snapshotPathResolver = testInfo.snapshotPath.bind(testInfo);
const helper = new SnapshotHelper( const helper = new SnapshotHelper(
testInfo, snapshotPathResolver, 'png', testInfo, snapshotPathResolver, 'png',

View file

@ -17,7 +17,7 @@
import type { ExpectedTextValue } from '@protocol/channels'; import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from 'playwright-core/lib/utils'; 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 { expectTypes, callLogText } from '../util';
import { import {
printReceivedStringContainExpectedResult, printReceivedStringContainExpectedResult,

View file

@ -14,8 +14,9 @@
* limitations under the License. * 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 { Component, JsxComponent, MountOptions } from '../types/experimentalComponent';
import type { ContextReuseMode, FullConfigInternal } from './common/config';
let boundCallbacksForMount: Function[] = []; let boundCallbacksForMount: Function[] = [];
@ -38,7 +39,7 @@ export const fixtures: Fixtures<
_ctWorker: [{ context: undefined, hash: '' }, { scope: 'worker' }], _ctWorker: [{ context: undefined, hash: '' }, { scope: 'worker' }],
page: async ({ page }, use, info) => { 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'); 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 as any)._wrapApiCall(async () => {
await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => { await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => {

View file

@ -14,8 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Suite } from '../../types/testReporter'; import type { FullConfig, Suite } from '../../types/testReporter';
import type { FullConfig } from '../common/types';
import type { Multiplexer } from '../reporters/multiplexer'; import type { Multiplexer } from '../reporters/multiplexer';
export interface TestRunnerPlugin { export interface TestRunnerPlugin {

View file

@ -23,11 +23,10 @@ import { parse, traverse, types as t } from '../common/babelBundle';
import { stoppable } from '../utilsBundle'; import { stoppable } from '../utilsBundle';
import type { ComponentInfo } from '../common/tsxTransform'; import type { ComponentInfo } from '../common/tsxTransform';
import { collectComponentUsages, 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 { assert, calculateSha1 } from 'playwright-core/lib/utils';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import { getPlaywrightVersion } from 'playwright-core/lib/utils'; 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 type { PluginContext } from 'rollup';
import { setExternalDependencies } from '../common/compilationCache'; import { setExternalDependencies } from '../common/compilationCache';

View file

@ -21,7 +21,7 @@ import { raceAgainstTimeout, launchProcess, httpRequest } from 'playwright-core/
import type { FullConfig } from '../../types/testReporter'; import type { FullConfig } from '../../types/testReporter';
import type { TestRunnerPlugin } from '.'; import type { TestRunnerPlugin } from '.';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/config';
import { envWithoutExperimentalLoaderOptions } from '../util'; import { envWithoutExperimentalLoaderOptions } from '../util';
import type { Multiplexer } from '../reporters/multiplexer'; import type { Multiplexer } from '../reporters/multiplexer';
@ -204,9 +204,9 @@ export const webServer = (options: WebServerPluginOptions): TestRunnerPlugin =>
}; };
export const webServerPluginsForConfig = (config: FullConfigInternal): TestRunnerPlugin[] => { export const webServerPluginsForConfig = (config: FullConfigInternal): TestRunnerPlugin[] => {
const shouldSetBaseUrl = !!config.webServer; const shouldSetBaseUrl = !!config.config.webServer;
const webServerPlugins = []; const webServerPlugins = [];
for (const webServerConfig of config._internal.webServers) { for (const webServerConfig of config.webServers) {
if ((!webServerConfig.port && !webServerConfig.url) || (webServerConfig.port && webServerConfig.url)) if ((!webServerConfig.port && !webServerConfig.url) || (webServerConfig.port && webServerConfig.url))
throw new Error(`Exactly one of 'port' or 'url' is required in config.webServer.`); throw new Error(`Exactly one of 'port' or 'url' is required in config.webServer.`);

View file

@ -19,9 +19,9 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location, Reporter } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location, Reporter } from '../../types/testReporter';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
import type { FullConfigInternal } from '../common/types';
import { codeFrameColumns } from '../common/babelBundle'; import { codeFrameColumns } from '../common/babelBundle';
import { monotonicTime } from 'playwright-core/lib/utils'; import { monotonicTime } from 'playwright-core/lib/utils';
import { FullConfigInternal } from '../common/config';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
export const kOutputSymbol = Symbol('output'); export const kOutputSymbol = Symbol('output');
@ -49,7 +49,7 @@ type TestSummary = {
export class BaseReporter implements Reporter { export class BaseReporter implements Reporter {
duration = 0; duration = 0;
config!: FullConfigInternal; config!: FullConfig;
suite!: Suite; suite!: Suite;
totalTestCount = 0; totalTestCount = 0;
result!: FullResult; result!: FullResult;
@ -66,7 +66,7 @@ export class BaseReporter implements Reporter {
onBegin(config: FullConfig, suite: Suite) { onBegin(config: FullConfig, suite: Suite) {
this.monotonicStartTime = monotonicTime(); this.monotonicStartTime = monotonicTime();
this.config = config as FullConfigInternal; this.config = config;
this.suite = suite; this.suite = suite;
this.totalTestCount = suite.allTests().length; this.totalTestCount = suite.allTests().length;
} }
@ -122,7 +122,7 @@ export class BaseReporter implements Reporter {
} }
protected generateStartingMessage() { 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}` : ''; const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
if (!this.totalTestCount) if (!this.totalTestCount)
return ''; return '';

View file

@ -26,7 +26,7 @@ import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResul
import RawReporter from './raw'; import RawReporter from './raw';
import { stripAnsiEscapes } from './base'; import { stripAnsiEscapes } from './base';
import { getPackageJsonPath, sanitizeForFilePath } from '../util'; 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 type { ZipFile } from 'playwright-core/lib/zipBundle';
import { yazl } from 'playwright-core/lib/zipBundle'; import { yazl } from 'playwright-core/lib/zipBundle';
import { mime } from 'playwright-core/lib/utilsBundle'; import { mime } from 'playwright-core/lib/utilsBundle';

View file

@ -20,7 +20,7 @@ import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, Full
import { formatError, prepareErrorStack } from './base'; import { formatError, prepareErrorStack } from './base';
import { MultiMap } from 'playwright-core/lib/utils'; import { MultiMap } from 'playwright-core/lib/utils';
import { assert } 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 { export function toPosixPath(aPath: string): string {
return aPath.split(path.sep).join(path.posix.sep); return aPath.split(path.sep).join(path.posix.sep);
@ -64,7 +64,7 @@ class JSONReporter implements Reporter {
repeatEach: project.repeatEach, repeatEach: project.repeatEach,
retries: project.retries, retries: project.retries,
metadata: project.metadata, metadata: project.metadata,
id: (project as FullProjectInternal)._internal.id, id: FullProjectInternal.from(project).id,
name: project.name, name: project.name,
testDir: toPosixPath(project.testDir), testDir: toPosixPath(project.testDir),
testIgnore: serializePatterns(project.testIgnore), testIgnore: serializePatterns(project.testIgnore),
@ -81,7 +81,7 @@ class JSONReporter implements Reporter {
private _mergeSuites(suites: Suite[]): JSONReportSuite[] { private _mergeSuites(suites: Suite[]): JSONReportSuite[] {
const fileSuites = new MultiMap<string, JSONReportSuite>(); const fileSuites = new MultiMap<string, JSONReportSuite>();
for (const projectSuite of suites) { 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; const projectName = projectSuite.project()!.name;
for (const fileSuite of projectSuite.suites) { for (const fileSuite of projectSuite.suites) {
const file = fileSuite.location!.file; const file = fileSuite.location!.file;

View file

@ -16,6 +16,7 @@
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter } from '../../types/testReporter'; import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter } from '../../types/testReporter';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import type { FullConfigInternal } from '../common/config';
import { addSnippetToError } from './base'; import { addSnippetToError } from './base';
type StdIOChunk = { type StdIOChunk = {
@ -27,7 +28,7 @@ type StdIOChunk = {
export class Multiplexer { export class Multiplexer {
private _reporters: Reporter[]; private _reporters: Reporter[];
private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = [];
private _config!: FullConfig; private _config!: FullConfigInternal;
constructor(reporters: Reporter[]) { constructor(reporters: Reporter[]) {
this._reporters = reporters; this._reporters = reporters;
@ -37,7 +38,7 @@ export class Multiplexer {
return this._reporters.some(r => r.printsToStdio ? r.printsToStdio() : true); return this._reporters.some(r => r.printsToStdio ? r.printsToStdio() : true);
} }
onConfigure(config: FullConfig) { onConfigure(config: FullConfigInternal) {
this._config = config; this._config = config;
} }
@ -92,7 +93,7 @@ export class Multiplexer {
async onExit(result: FullResult) { async onExit(result: FullResult) {
if (this._deferred) { if (this._deferred) {
// onBegin was not reported, emit it. // 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) for (const reporter of this._reporters)
@ -107,7 +108,7 @@ export class Multiplexer {
this._deferred.push({ error }); this._deferred.push({ error });
return; return;
} }
addSnippetToError(this._config, error); addSnippetToError(this._config.config, error);
for (const reporter of this._reporters) for (const reporter of this._reporters)
wrap(() => reporter.onError?.(error)); wrap(() => reporter.onError?.(error));
} }
@ -125,12 +126,12 @@ export class Multiplexer {
private _addSnippetToTestErrors(test: TestCase, result: TestResult) { private _addSnippetToTestErrors(test: TestCase, result: TestResult) {
for (const error of result.errors) 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) { private _addSnippetToStepError(test: TestCase, step: TestStep) {
if (step.error) if (step.error)
addSnippetToError(this._config, step.error, test.location.file); addSnippetToError(this._config.config, step.error, test.location.file);
} }
} }

View file

@ -23,7 +23,7 @@ import { formatResultFailure } from './base';
import { toPosixPath, serializePatterns } from './json'; import { toPosixPath, serializePatterns } from './json';
import { MultiMap } from 'playwright-core/lib/utils'; import { MultiMap } from 'playwright-core/lib/utils';
import { codeFrameColumns } from '../common/babelBundle'; import { codeFrameColumns } from '../common/babelBundle';
import type { Metadata } from '../common/types'; import type { Metadata } from '../../types/test';
import type { SuitePrivate } from '../../types/reporterPrivate'; import type { SuitePrivate } from '../../types/reporterPrivate';
export type JsonLocation = Location; export type JsonLocation = Location;

View file

@ -18,7 +18,7 @@ import type { FullConfig, FullResult, Reporter, TestError, TestResult, TestStep,
import type { Suite, TestCase } from '../common/test'; import type { Suite, TestCase } from '../common/test';
import type { JsonConfig, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; import type { JsonConfig, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
import type { SuitePrivate } from '../../types/reporterPrivate'; 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 { createGuid } from 'playwright-core/lib/utils';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import path from 'path'; import path from 'path';
@ -123,14 +123,14 @@ export class TeleReporterEmitter implements Reporter {
return { return {
rootDir: config.rootDir, rootDir: config.rootDir,
configFile: this._relativePath(config.configFile), configFile: this._relativePath(config.configFile),
listOnly: (config as FullConfigInternal)._internal.listOnly, listOnly: FullConfigInternal.from(config).listOnly,
}; };
} }
private _serializeProject(suite: Suite): JsonProject { private _serializeProject(suite: Suite): JsonProject {
const project = suite.project()!; const project = suite.project()!;
const report: JsonProject = { const report: JsonProject = {
id: (project as FullProjectInternal)._internal.id, id: FullProjectInternal.from(project).id,
metadata: project.metadata, metadata: project.metadata,
name: project.name, name: project.name,
outputDir: this._relativePath(project.outputDir), outputDir: this._relativePath(project.outputDir),

View file

@ -23,7 +23,7 @@ import type { TestCase } from '../common/test';
import { ManualPromise } from 'playwright-core/lib/utils'; import { ManualPromise } from 'playwright-core/lib/utils';
import { WorkerHost } from './workerHost'; import { WorkerHost } from './workerHost';
import type { TestGroup } from './testGroups'; import type { TestGroup } from './testGroups';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/config';
import type { Multiplexer } from '../reporters/multiplexer'; import type { Multiplexer } from '../reporters/multiplexer';
type TestResultData = { type TestResultData = {
@ -180,7 +180,7 @@ export class Dispatcher {
this._isStopped = false; this._isStopped = false;
this._workerSlots = []; this._workerSlots = [];
// 1. Allocate workers. // 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 }); this._workerSlots.push({ busy: false });
// 2. Schedule enough jobs. // 2. Schedule enough jobs.
for (let i = 0; i < this._workerSlots.length; i++) for (let i = 0; i < this._workerSlots.length; i++)
@ -504,7 +504,7 @@ export class Dispatcher {
} }
private _hasReachedMaxFailures() { private _hasReachedMaxFailures() {
const maxFailures = this._config.maxFailures; const maxFailures = this._config.config.maxFailures;
return maxFailures > 0 && this._failureCount >= maxFailures; return maxFailures > 0 && this._failureCount >= maxFailures;
} }
@ -512,7 +512,7 @@ export class Dispatcher {
if (result.status !== 'skipped' && result.status !== test.expectedStatus) if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount; ++this._failureCount;
this._reporter.onTestEnd?.(test, result); this._reporter.onTestEnd?.(test, result);
const maxFailures = this._config.maxFailures; const maxFailures = this._config.config.maxFailures;
if (maxFailures && this._failureCount === maxFailures) if (maxFailures && this._failureCount === maxFailures)
this.stop().catch(e => {}); this.stop().catch(e => {});
} }

View file

@ -15,11 +15,12 @@
*/ */
import path from 'path'; 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 { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import type { TestCase } 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 { createFileMatcherFromArguments, createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util';
import type { Matcher, TestFileFilter } from '../util'; import type { Matcher, TestFileFilter } from '../util';
import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
@ -35,11 +36,11 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, additionalFi
const config = testRun.config; const config = testRun.config;
const fsCache = new Map(); const fsCache = new Map();
const sourceMapCache = 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. // First collect all files for the projects in the command line, don't apply any file filters.
const allFilesForProject = new Map<FullProjectInternal, string[]>(); const allFilesForProject = new Map<FullProjectInternal, string[]>();
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); const files = await collectFilesForProject(project, fsCache);
allFilesForProject.set(project, files); 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 file of allTestFiles) {
for (const dependency of dependenciesForTestFile(file)) { for (const dependency of dependenciesForTestFile(file)) {
if (allTestFiles.has(dependency)) { if (allTestFiles.has(dependency)) {
const importer = path.relative(config.rootDir, file); const importer = path.relative(config.config.rootDir, file);
const importee = path.relative(config.rootDir, dependency); const importee = path.relative(config.config.rootDir, dependency);
errors.push({ errors.push({
message: `Error: test file "${importer}" should not import test file "${importee}"`, message: `Error: test file "${importer}" should not import test file "${importee}"`,
location: { file, line: 1, column: 1 }, 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. // First add top-level projects, so that we can filterOnly and shard just top-level.
{ {
// Interpret cli parameters. // Interpret cli parameters.
const cliFileFilters = createFileFiltersFromArguments(config._internal.cliArgs); const cliFileFilters = createFileFiltersFromArguments(config.cliArgs);
const grepMatcher = config._internal.cliGrep ? createTitleMatcher(forceRegExp(config._internal.cliGrep)) : () => true; const grepMatcher = config.cliGrep ? createTitleMatcher(forceRegExp(config.cliGrep)) : () => true;
const grepInvertMatcher = config._internal.cliGrepInvert ? createTitleMatcher(forceRegExp(config._internal.cliGrepInvert)) : () => false; const grepInvertMatcher = config.cliGrepInvert ? createTitleMatcher(forceRegExp(config.cliGrepInvert)) : () => false;
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
// Clone file suites for top-level projects. // Clone file suites for top-level projects.
for (const [project, fileSuites] of testRun.projectSuites) { for (const [project, fileSuites] of testRun.projectSuites) {
if (testRun.projectType.get(project) === 'top-level') 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. // Complain about only.
if (config.forbidOnly) { if (config.config.forbidOnly) {
const onlyTestsAndSuites = rootSuite._getOnlyItems(); const onlyTestsAndSuites = rootSuite._getOnlyItems();
if (onlyTestsAndSuites.length > 0) if (onlyTestsAndSuites.length > 0)
errors.push(...createForbidOnlyErrors(onlyTestsAndSuites)); errors.push(...createForbidOnlyErrors(onlyTestsAndSuites));
@ -149,14 +150,14 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
filterOnly(rootSuite); filterOnly(rootSuite);
// Shard only the top-level projects. // Shard only the top-level projects.
if (config.shard) { if (config.config.shard) {
// Create test groups for top-level projects. // Create test groups for top-level projects.
const testGroups: TestGroup[] = []; const testGroups: TestGroup[] = [];
for (const projectSuite of rootSuite.suites) for (const projectSuite of rootSuite.suites)
testGroups.push(...createTestGroups(projectSuite, config.workers)); testGroups.push(...createTestGroups(projectSuite, config.config.workers));
// Shard test groups. // Shard test groups.
const testGroupsInThisShard = filterForShard(config.shard, testGroups); const testGroupsInThisShard = filterForShard(config.config.shard, testGroups);
const testsInThisShard = new Set<TestCase>(); const testsInThisShard = new Set<TestCase>();
for (const group of testGroupsInThisShard) { for (const group of testGroupsInThisShard) {
for (const test of group.tests) 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. // 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. // 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. // Clone file suites for dependency projects.
for (const [project, fileSuites] of testRun.projectSuites) { 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<Suite> { async function createProjectSuite(fileSuites: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }): Promise<Suite> {
const projectSuite = new Suite(project.name, 'project'); const projectSuite = new Suite(project.project.name, 'project');
projectSuite._projectConfig = project; projectSuite._projectConfig = project;
if (project._internal.fullyParallel) if (project.fullyParallel)
projectSuite._parallelMode = 'parallel'; projectSuite._parallelMode = 'parallel';
for (const fileSuite of fileSuites) { 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); const builtSuite = buildFileSuiteForProject(project, fileSuite, repeatEachIndex);
projectSuite._addSuite(builtSuite); projectSuite._addSuite(builtSuite);
} }
@ -198,8 +199,8 @@ async function createProjectSuite(fileSuites: Suite[], project: FullProjectInter
filterByFocusedLine(projectSuite, options.cliFileFilters); filterByFocusedLine(projectSuite, options.cliFileFilters);
filterByTestIds(projectSuite, options.testIdMatcher); filterByTestIds(projectSuite, options.testIdMatcher);
const grepMatcher = createTitleMatcher(project.grep); const grepMatcher = createTitleMatcher(project.project.grep);
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null; const grepInvertMatcher = project.project.grepInvert ? createTitleMatcher(project.project.grepInvert) : null;
const titleMatcher = (test: TestCase) => { const titleMatcher = (test: TestCase) => {
const grepTitle = test.titlePath().join(' '); const grepTitle = test.titlePath().join(' ');
@ -234,7 +235,7 @@ function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuite: Suit
const existingTest = testsByFullTitle.get(fullTitle); const existingTest = testsByFullTitle.get(fullTitle);
if (existingTest) { if (existingTest) {
const error: TestError = { 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, location: test.location,
}; };
errors.push(error); errors.push(error);
@ -259,12 +260,12 @@ async function requireOrImportDefaultFunction(file: string, expectConstructor: b
return func; return func;
} }
export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfigInternal) => any> { export function loadGlobalHook(config: FullConfigInternal, file: string): Promise<(config: FullConfig) => any> {
return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), false); return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false);
} }
export function loadReporter(config: FullConfigInternal, file: string): Promise<new (arg?: any) => Reporter> { export function loadReporter(config: FullConfigInternal, file: string): Promise<new (arg?: any) => 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, string[]>): string[] { function sourceMapSources(file: string, cache: Map<string, string[]>): string[] {

View file

@ -19,7 +19,7 @@ import { serializeConfig } from '../common/ipc';
import { ProcessHost } from './processHost'; import { ProcessHost } from './processHost';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import { loadTestFile } from '../common/testLoader'; import { loadTestFile } from '../common/testLoader';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/config';
import { PoolBuilder } from '../common/poolBuilder'; import { PoolBuilder } from '../common/poolBuilder';
import { addToCompilationCache } from '../common/compilationCache'; import { addToCompilationCache } from '../common/compilationCache';
@ -33,7 +33,7 @@ export class InProcessLoaderHost {
} }
async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> { async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
const result = await loadTestFile(file, this._config.rootDir, testErrors); const result = await loadTestFile(file, this._config.config.rootDir, testErrors);
this._poolBuilder.buildPools(result, testErrors); this._poolBuilder.buildPools(result, testErrors);
return result; return result;
} }

View file

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { minimatch } from 'playwright-core/lib/utilsBundle'; import { minimatch } from 'playwright-core/lib/utilsBundle';
import { promisify } from 'util'; import { promisify } from 'util';
import type { FullProjectInternal } from '../common/types'; import type { FullProjectInternal } from '../common/config';
import { createFileMatcher } from '../util'; import { createFileMatcher } from '../util';
const readFileAsync = promisify(fs.readFile); const readFileAsync = promisify(fs.readFile);
@ -35,12 +35,12 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
unknownProjects.set(name, n); unknownProjects.set(name, n);
}); });
const result = projects.filter(project => { const result = projects.filter(project => {
const name = project.name.toLocaleLowerCase(); const name = project.project.name.toLocaleLowerCase();
unknownProjects.delete(name); unknownProjects.delete(name);
return projectsToFind.has(name); return projectsToFind.has(name);
}); });
if (unknownProjects.size) { 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) if (!names.length)
throw new Error(`No named projects are specified in the configuration file`); throw new Error(`No named projects are specified in the configuration file`);
const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', '); const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', ');
@ -58,7 +58,7 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
throw error; throw error;
} }
result.set(project, depth ? 'dependency' : 'top-level'); result.set(project, depth ? 'dependency' : 'top-level');
project._internal.deps.map(visit.bind(undefined, depth + 1)); project.deps.map(visit.bind(undefined, depth + 1));
}; };
for (const p of projects) for (const p of projects)
result.set(p, 'top-level'); result.set(p, 'top-level');
@ -70,9 +70,9 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> { export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx']; const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
const testFileExtension = (file: string) => extensions.includes(path.extname(file)); const testFileExtension = (file: string) => extensions.includes(path.extname(file));
const allFiles = await cachedCollectFiles(project.testDir, project._internal.respectGitIgnore, fsCache); const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache);
const testMatch = createFileMatcher(project.testMatch); const testMatch = createFileMatcher(project.project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore); const testIgnore = createFileMatcher(project.project.testIgnore);
const testFiles = allFiles.filter(file => { const testFiles = allFiles.filter(file => {
if (!testFileExtension(file)) if (!testFileExtension(file))
return false; return false;

View file

@ -15,7 +15,7 @@
*/ */
import path from 'path'; 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 { formatError } from '../reporters/base';
import DotReporter from '../reporters/dot'; import DotReporter from '../reporters/dot';
import EmptyReporter from '../reporters/empty'; import EmptyReporter from '../reporters/empty';
@ -27,9 +27,8 @@ import LineReporter from '../reporters/line';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import { Multiplexer } from '../reporters/multiplexer'; import { Multiplexer } from '../reporters/multiplexer';
import type { Suite } from '../common/test'; import type { Suite } from '../common/test';
import type { FullConfigInternal } from '../common/types'; import type { BuiltInReporter, FullConfigInternal } from '../common/config';
import { loadReporter } from './loadUtils'; import { loadReporter } from './loadUtils';
import type { BuiltInReporter } from '../common/configLoader';
export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run' | 'ui', additionalReporters: Reporter[] = []): Promise<Multiplexer> { export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run' | 'ui', additionalReporters: Reporter[] = []): Promise<Multiplexer> {
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
@ -46,9 +45,9 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' |
if (mode === 'watch') { if (mode === 'watch') {
reporters.push(new ListReporter()); reporters.push(new ListReporter());
} else { } else {
for (const r of config.reporter) { for (const r of config.config.reporter) {
const [name, arg] = r; const [name, arg] = r;
const options = { ...arg, configDir: config._internal.configDir }; const options = { ...arg, configDir: config.configDir };
if (name in defaultReporters) { if (name in defaultReporters) {
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options)); reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options));
} else { } else {
@ -79,9 +78,9 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' |
} }
export class ListModeReporter implements Reporter { 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; this.config = config;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Listing tests:`); console.log(`Listing tests:`);

View file

@ -21,7 +21,7 @@ import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { collectFilesForProject, filterProjects } from './projectUtils'; import { collectFilesForProject, filterProjects } from './projectUtils';
import { createReporter } from './reporters'; import { createReporter } from './reporters';
import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; 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 { colors } from 'playwright-core/lib/utilsBundle';
import { runWatchModeLoop } from './watchMode'; import { runWatchModeLoop } from './watchMode';
import { runUIMode } from './uiMode'; import { runUIMode } from './uiMode';
@ -49,11 +49,11 @@ export class Runner {
async runAllTests(): Promise<FullResult['status']> { async runAllTests(): Promise<FullResult['status']> {
const config = this._config; const config = this._config;
const listOnly = config._internal.listOnly; const listOnly = config.listOnly;
const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0; const deadline = config.config.globalTimeout ? monotonicTime() + config.config.globalTimeout : 0;
// Legacy webServer support. // 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 reporter = await createReporter(config, listOnly ? 'list' : 'run');
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process') const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process')
@ -62,7 +62,7 @@ export class Runner {
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(config); reporter.onConfigure(config);
if (!listOnly && config._internal.ignoreSnapshots) { if (!listOnly && config.ignoreSnapshots) {
reporter.onStdOut(colors.dim([ reporter.onStdOut(colors.dim([
'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:', 'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:',
'- expect().toMatchSnapshot()', '- expect().toMatchSnapshot()',
@ -89,13 +89,13 @@ export class Runner {
async watchAllTests(): Promise<FullResult['status']> { async watchAllTests(): Promise<FullResult['status']> {
const config = this._config; 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); return await runWatchModeLoop(config);
} }
async uiAllTests(): Promise<FullResult['status']> { async uiAllTests(): Promise<FullResult['status']> {
const config = this._config; 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); return await runUIMode(config);
} }
} }

View file

@ -24,7 +24,7 @@ import type { Multiplexer } from '../reporters/multiplexer';
import { createTestGroups, type TestGroup } from '../runner/testGroups'; import { createTestGroups, type TestGroup } from '../runner/testGroups';
import type { Task } from './taskRunner'; import type { Task } from './taskRunner';
import { TaskRunner } 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 { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
import type { Matcher } from '../util'; import type { Matcher } from '../util';
import type { Suite } from '../common/test'; import type { Suite } from '../common/test';
@ -60,7 +60,7 @@ export class TestRun {
} }
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TestRun> { export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.globalTimeout); const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
addGlobalSetupTasks(taskRunner, config); addGlobalSetupTasks(taskRunner, config);
taskRunner.addTask('load tests', createLoadTask('in-process', true)); taskRunner.addTask('load tests', createLoadTask('in-process', true));
addRunTasks(taskRunner, config); addRunTasks(taskRunner, config);
@ -81,9 +81,9 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: M
} }
function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) { function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) {
for (const plugin of config._internal.plugins) for (const plugin of config.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); 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('global setup', createGlobalSetupTask());
taskRunner.addTask('clear output', createRemoveOutputDirsTask()); taskRunner.addTask('clear output', createRemoveOutputDirsTask());
} }
@ -91,10 +91,10 @@ function addGlobalSetupTasks(taskRunner: TaskRunner<TestRun>, config: FullConfig
function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) { function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal) {
taskRunner.addTask('create phases', createPhasesTask()); taskRunner.addTask('create phases', createPhasesTask());
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
reporter.onBegin?.(config, rootSuite!); reporter.onBegin?.(config.config, rootSuite!);
return () => reporter.onEnd(); return () => reporter.onEnd();
}); });
for (const plugin of config._internal.plugins) for (const plugin of config.plugins)
taskRunner.addTask('plugin begin', createPluginBeginTask(plugin)); taskRunner.addTask('plugin begin', createPluginBeginTask(plugin));
taskRunner.addTask('start workers', createWorkersTask()); taskRunner.addTask('start workers', createWorkersTask());
taskRunner.addTask('test suite', createRunTestsTask()); taskRunner.addTask('test suite', createRunTestsTask());
@ -102,10 +102,10 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
} }
export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner<TestRun> { export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.globalTimeout); const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
taskRunner.addTask('load tests', createLoadTask(mode, false)); taskRunner.addTask('load tests', createLoadTask(mode, false));
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
reporter.onBegin?.(config, rootSuite!); reporter.onBegin?.(config.config, rootSuite!);
return () => reporter.onEnd(); return () => reporter.onEnd();
}); });
return taskRunner; return taskRunner;
@ -117,7 +117,7 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestR
plugin.instance = await plugin.factory(); plugin.instance = await plugin.factory();
else else
plugin.instance = plugin.factory; plugin.instance = plugin.factory;
await plugin.instance?.setup?.(config, config._internal.configDir, reporter); await plugin.instance?.setup?.(config.config, config.configDir, reporter);
return () => plugin.instance?.teardown?.(); return () => plugin.instance?.teardown?.();
}; };
} }
@ -131,13 +131,13 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestR
function createGlobalSetupTask(): Task<TestRun> { function createGlobalSetupTask(): Task<TestRun> {
return async ({ config }) => { return async ({ config }) => {
const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined; const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined;
const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined; const teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined;
const globalSetupResult = setupHook ? await setupHook(config) : undefined; const globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
return async () => { return async () => {
if (typeof globalSetupResult === 'function') if (typeof globalSetupResult === 'function')
await globalSetupResult(); await globalSetupResult();
await teardownHook?.(config); await teardownHook?.(config.config);
}; };
}; };
} }
@ -146,8 +146,8 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
return async ({ config }) => { return async ({ config }) => {
const outputDirs = new Set<string>(); const outputDirs = new Set<string>();
for (const p of config.projects) { for (const p of config.projects) {
if (!config._internal.cliProjectFilter || config._internal.cliProjectFilter.includes(p.name)) if (!config.cliProjectFilter || config.cliProjectFilter.includes(p.project.name))
outputDirs.add(p.outputDir); outputDirs.add(p.project.outputDir);
} }
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async (error: any) => { 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); 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._internal.passWithNoTests && !testRun.config.shard) if (!testRun.rootSuite.allTests().length && !testRun.config.passWithNoTests && !testRun.config.config.shard)
throw new Error(`No tests found`); throw new Error(`No tests found`);
}; };
} }
function createPhasesTask(): Task<TestRun> { function createPhasesTask(): Task<TestRun> {
return async testRun => { return async testRun => {
testRun.config._internal.maxConcurrentTestGroups = 0; testRun.config.maxConcurrentTestGroups = 0;
const processed = new Set<FullProjectInternal>(); const processed = new Set<FullProjectInternal>();
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++) { for (let i = 0; i < projectToSuite.size; i++) {
// Find all projects that have all their dependencies processed by previous phases. // Find all projects that have all their dependencies processed by previous phases.
const phaseProjects: FullProjectInternal[] = []; const phaseProjects: FullProjectInternal[] = [];
for (const project of projectToSuite.keys()) { for (const project of projectToSuite.keys()) {
if (processed.has(project)) if (processed.has(project))
continue; continue;
if (project._internal.deps.find(p => !processed.has(p))) if (project.deps.find(p => !processed.has(p)))
continue; continue;
phaseProjects.push(project); phaseProjects.push(project);
} }
@ -201,12 +201,12 @@ function createPhasesTask(): Task<TestRun> {
testRun.phases.push(phase); testRun.phases.push(phase);
for (const project of phaseProjects) { for (const project of phaseProjects) {
const projectSuite = projectToSuite.get(project)!; 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 }); phase.projects.push({ project, projectSuite, testGroups });
testGroupsInPhase += testGroups.length; testGroupsInPhase += testGroups.length;
} }
debug('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); debug('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
testRun.config._internal.maxConcurrentTestGroups = Math.max(testRun.config._internal.maxConcurrentTestGroups, testGroupsInPhase); testRun.config.maxConcurrentTestGroups = Math.max(testRun.config.maxConcurrentTestGroups, testGroupsInPhase);
} }
} }
}; };
@ -235,11 +235,11 @@ function createRunTestsTask(): Task<TestRun> {
for (const { project, testGroups } of projects) { for (const { project, testGroups } of projects) {
// Inherit extra enviroment variables from dependencies. // Inherit extra enviroment variables from dependencies.
let extraEnv: Record<string, string | undefined> = {}; let extraEnv: Record<string, string | undefined> = {};
for (const dep of project._internal.deps) for (const dep of project.deps)
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep._internal.id) }; extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) };
extraEnvByProjectId.set(project._internal.id, extraEnv); 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) { if (!hasFailedDeps) {
phaseTestGroups.push(...testGroups); phaseTestGroups.push(...testGroups);
} else { } else {
@ -263,7 +263,7 @@ function createRunTestsTask(): Task<TestRun> {
// projects failed. // projects failed.
if (!dispatcher.hasWorkerErrors()) { if (!dispatcher.hasWorkerErrors()) {
for (const { project, projectSuite } of projects) { 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())) if (!hasFailedDeps && !projectSuite.allTests().some(test => !test.ok()))
successfulProjects.add(project); successfulProjects.add(project);
} }

View file

@ -19,7 +19,7 @@ import type { Page } from 'playwright-core/lib/server/page';
import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils'; import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils';
import type { FullResult } from '../../reporter'; import type { FullResult } from '../../reporter';
import { clearCompilationCache, collectAffectedTestFiles, dependenciesForTestFile } from '../common/compilationCache'; 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 { Multiplexer } from '../reporters/multiplexer';
import { TeleReporterEmitter } from '../reporters/teleEmitter'; import { TeleReporterEmitter } from '../reporters/teleEmitter';
import { createReporter } from './reporters'; import { createReporter } from './reporters';
@ -42,20 +42,20 @@ 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._internal.configCLIOverrides.forbidOnly = false; config.configCLIOverrides.forbidOnly = false;
config._internal.configCLIOverrides.globalTimeout = 0; config.configCLIOverrides.globalTimeout = 0;
config._internal.configCLIOverrides.repeatEach = 0; config.configCLIOverrides.repeatEach = 0;
config._internal.configCLIOverrides.shard = undefined; config.configCLIOverrides.shard = undefined;
config._internal.configCLIOverrides.updateSnapshots = undefined; config.configCLIOverrides.updateSnapshots = undefined;
config._internal.listOnly = false; config.listOnly = false;
config._internal.passWithNoTests = true; config.passWithNoTests = true;
for (const project of config.projects) for (const project of config.projects)
project._internal.deps = []; project.deps = [];
for (const p of config.projects) for (const p of config.projects)
p.retries = 0; p.project.retries = 0;
config._internal.configCLIOverrides.use = config._internal.configCLIOverrides.use || {}; config.configCLIOverrides.use = config.configCLIOverrides.use || {};
config._internal.configCLIOverrides.use.trace = { mode: 'on', sources: false }; config.configCLIOverrides.use.trace = { mode: 'on', sources: false };
this._originalStdoutWrite = process.stdout.write; this._originalStdoutWrite = process.stdout.write;
this._originalStderrWrite = process.stderr.write; this._originalStderrWrite = process.stderr.write;
@ -148,8 +148,8 @@ 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._internal.listOnly = true; this._config.listOnly = true;
this._config._internal.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);
clearCompilationCache(); clearCompilationCache();
@ -159,7 +159,7 @@ class UIMode {
const projectDirs = new Set<string>(); const projectDirs = new Set<string>();
for (const p of this._config.projects) for (const p of this._config.projects)
projectDirs.add(p.testDir); projectDirs.add(p.project.testDir);
this._globalWatcher.update([...projectDirs], false); this._globalWatcher.update([...projectDirs], false);
} }
@ -167,8 +167,8 @@ 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._internal.listOnly = false; this._config.listOnly = false;
this._config._internal.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));
const reporter = await createReporter(this._config, 'ui', [runReporter]); const reporter = await createReporter(this._config, 'ui', [runReporter]);
@ -180,7 +180,7 @@ class UIMode {
const run = taskRunner.run(testRun, 0, stop).then(async status => { const run = taskRunner.run(testRun, 0, stop).then(async status => {
await reporter.onExit({ status }); await reporter.onExit({ status });
this._testRun = undefined; this._testRun = undefined;
this._config._internal.testIdMatcher = undefined; this._config.testIdMatcher = undefined;
return status; return status;
}); });
this._testRun = { run, stop }; this._testRun = { run, stop };

View file

@ -16,7 +16,7 @@
import readline from 'readline'; import readline from 'readline';
import { createGuid, ManualPromise } from 'playwright-core/lib/utils'; 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 { Multiplexer } from '../reporters/multiplexer';
import { createFileMatcher, createFileMatcherFromArguments } from '../util'; import { createFileMatcher, createFileMatcherFromArguments } from '../util';
import type { Matcher } from '../util'; import type { Matcher } from '../util';
@ -40,15 +40,15 @@ class FSWatcher {
private _timer: NodeJS.Timeout | undefined; private _timer: NodeJS.Timeout | undefined;
async update(config: FullConfigInternal) { async update(config: FullConfigInternal) {
const commandLineFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true; const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true;
const projects = filterProjects(config.projects, config._internal.cliProjectFilter); const projects = filterProjects(config.projects, config.cliProjectFilter);
const projectClosure = buildProjectsClosure(projects); const projectClosure = buildProjectsClosure(projects);
const projectFilters = new Map<FullProjectInternal, Matcher>(); const projectFilters = new Map<FullProjectInternal, Matcher>();
for (const [project, type] of projectClosure) { for (const [project, type] of projectClosure) {
const testMatch = createFileMatcher(project.testMatch); const testMatch = createFileMatcher(project.project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore); const testIgnore = createFileMatcher(project.project.testIgnore);
projectFilters.set(project, file => { 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 false;
return type === 'dependency' || commandLineFileMatcher(file); return type === 'dependency' || commandLineFileMatcher(file);
}); });
@ -59,7 +59,7 @@ class FSWatcher {
if (this._watcher) if (this._watcher)
await this._watcher.close(); 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') if (event !== 'add' && event !== 'change')
return; return;
@ -108,9 +108,9 @@ 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._internal.passWithNoTests = true; config.passWithNoTests = true;
for (const p of config.projects) for (const p of config.projects)
p.retries = 0; p.project.retries = 0;
// Perform global setup. // Perform global setup.
const reporter = await createReporter(config, 'watch'); const reporter = await createReporter(config, 'watch');
@ -123,7 +123,7 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
// Prepare projects that will be watched, set up watcher. // Prepare projects that will be watched, set up watcher.
const failedTestIdCollector = new Set<string>(); const failedTestIdCollector = new Set<string>();
const originalWorkers = config.workers; const originalWorkers = config.config.workers;
const fsWatcher = new FSWatcher(); const fsWatcher = new FSWatcher();
await fsWatcher.update(config); await fsWatcher.update(config);
@ -165,11 +165,11 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
type: 'multiselect', type: 'multiselect',
name: 'projectNames', name: 'projectNames',
message: 'Select projects', message: 'Select projects',
choices: config.projects.map(p => ({ name: p.name })), choices: config.projects.map(p => ({ name: p.project.name })),
}).catch(() => ({ projectNames: null })); }).catch(() => ({ projectNames: null }));
if (!projectNames) if (!projectNames)
continue; continue;
config._internal.cliProjectFilter = projectNames.length ? projectNames : undefined; config.cliProjectFilter = projectNames.length ? projectNames : undefined;
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -185,9 +185,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
if (filePattern === null) if (filePattern === null)
continue; continue;
if (filePattern.trim()) if (filePattern.trim())
config._internal.cliArgs = filePattern.split(' '); config.cliArgs = filePattern.split(' ');
else else
config._internal.cliArgs = []; config.cliArgs = [];
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -203,9 +203,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
if (testPattern === null) if (testPattern === null)
continue; continue;
if (testPattern.trim()) if (testPattern.trim())
config._internal.cliGrep = testPattern; config.cliGrep = testPattern;
else else
config._internal.cliGrep = undefined; config.cliGrep = undefined;
await fsWatcher.update(config); await fsWatcher.update(config);
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
@ -213,10 +213,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
} }
if (command === 'failed') { if (command === 'failed') {
config._internal.testIdMatcher = id => failedTestIdCollector.has(id); config.testIdMatcher = id => failedTestIdCollector.has(id);
const failedTestIds = new Set(failedTestIdCollector); const failedTestIds = new Set(failedTestIdCollector);
await runTests(config, failedTestIdCollector, { title: 'running failed tests' }); await runTests(config, failedTestIdCollector, { title: 'running failed tests' });
config._internal.testIdMatcher = undefined; config.testIdMatcher = undefined;
lastRun = { type: 'failed', failedTestIds }; lastRun = { type: 'failed', failedTestIds };
continue; continue;
} }
@ -228,9 +228,9 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
} else if (lastRun.type === 'changed') { } else if (lastRun.type === 'changed') {
await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles!, 're-running tests'); await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles!, 're-running tests');
} else if (lastRun.type === 'failed') { } else if (lastRun.type === 'failed') {
config._internal.testIdMatcher = id => lastRun.failedTestIds!.has(id); config.testIdMatcher = id => lastRun.failedTestIds!.has(id);
await runTests(config, failedTestIdCollector, { title: 're-running tests' }); await runTests(config, failedTestIdCollector, { title: 're-running tests' });
config._internal.testIdMatcher = undefined; config.testIdMatcher = undefined;
} }
continue; continue;
} }
@ -259,7 +259,7 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector
// Collect all the affected projects, follow project dependencies. // 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. // 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 projectClosure = buildProjectsClosure(projects);
const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]); const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency'); const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
@ -305,7 +305,7 @@ function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected
const result = new Set<FullProjectInternal>(affected); const result = new Set<FullProjectInternal>(affected);
for (let i = 0; i < projectClosure.length; ++i) { for (let i = 0; i < projectClosure.length; ++i) {
for (const p of projectClosure) { for (const p of projectClosure) {
for (const dep of p._internal.deps) { for (const dep of p.deps) {
if (result.has(dep)) if (result.has(dep))
result.add(p); result.add(p);
} }
@ -379,11 +379,11 @@ let seq = 0;
function printConfiguration(config: FullConfigInternal, title?: string) { function printConfiguration(config: FullConfigInternal, title?: string) {
const tokens: string[] = []; const tokens: string[] = [];
tokens.push('npx playwright test'); tokens.push('npx playwright test');
tokens.push(...(config._internal.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`)));
if (config._internal.cliGrep) if (config.cliGrep)
tokens.push(colors.red(`--grep ${config._internal.cliGrep}`)); tokens.push(colors.red(`--grep ${config.cliGrep}`));
if (config._internal.cliArgs) if (config.cliArgs)
tokens.push(...config._internal.cliArgs.map(a => colors.bold(a))); tokens.push(...config.cliArgs.map(a => colors.bold(a)));
if (title) if (title)
tokens.push(colors.dim(`(${title})`)); tokens.push(colors.dim(`(${title})`));
if (seq) if (seq)
@ -407,14 +407,14 @@ ${colors.dim('Waiting for file changes. Press')} ${colors.bold('enter')} ${color
async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) { async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) {
if (!showBrowserServer) { if (!showBrowserServer) {
config.workers = 1; config.config.workers = 1;
showBrowserServer = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: 1 }); showBrowserServer = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: 1 });
const wsEndpoint = await showBrowserServer.listen(); const wsEndpoint = await showBrowserServer.listen();
process.env.PW_TEST_REUSE_CONTEXT = '1'; process.env.PW_TEST_REUSE_CONTEXT = '1';
process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint; process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint;
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`); process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`);
} else { } else {
config.workers = originalWorkers; config.config.workers = originalWorkers;
await showBrowserServer?.close(); await showBrowserServer?.close();
showBrowserServer = undefined; showBrowserServer = undefined;
delete process.env.PW_TEST_REUSE_CONTEXT; delete process.env.PW_TEST_REUSE_CONTEXT;

View file

@ -49,7 +49,7 @@ class JsonStore {
const config = currentConfig(); const config = currentConfig();
if (!config) if (!config)
throw new Error('Cannot access store before config is loaded'); throw new Error('Cannot access store before config is loaded');
return config._internal.storeDir; return config.storeDir;
} }
async set<T>(name: string, value: T | undefined) { async set<T>(name: string, value: T | undefined) {

View file

@ -21,7 +21,8 @@ import util from 'util';
import path from 'path'; import path from 'path';
import url from 'url'; import url from 'url';
import { colors, debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; 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 { calculateSha1, isRegExp, isString } from 'playwright-core/lib/utils';
import type { RawStack } from 'playwright-core/lib/utils'; import type { RawStack } from 'playwright-core/lib/utils';

View file

@ -15,11 +15,12 @@
*/ */
import { formatLocation, debugTest } from '../util'; import { formatLocation, debugTest } from '../util';
import type { Location, WorkerInfo } from '../common/types';
import { ManualPromise } from 'playwright-core/lib/utils'; import { ManualPromise } from 'playwright-core/lib/utils';
import type { TestInfoImpl } from './testInfo'; import type { TestInfoImpl } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager'; import type { FixtureDescription, TimeoutManager } from './timeoutManager';
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures'; 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 { class Fixture {
runner: FixtureRunner; runner: FixtureRunner;

View file

@ -17,11 +17,12 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { captureRawStack, monotonicTime, zones } from 'playwright-core/lib/utils'; 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 { StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test'; import type { TestCase } from '../common/test';
import { TimeoutManager } from './timeoutManager'; 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 { getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from '../util';
import type * as trace from '@trace/trace'; import type * as trace from '@trace/trace';
@ -54,6 +55,8 @@ export class TestInfoImpl implements TestInfo {
_didTimeout = false; _didTimeout = false;
_wasInterrupted = false; _wasInterrupted = false;
_lastStepId = 0; _lastStepId = 0;
readonly _projectInternal: FullProjectInternal;
readonly _configInternal: FullConfigInternal;
// ------------ TestInfo fields ------------ // ------------ TestInfo fields ------------
readonly testId: string; readonly testId: string;
@ -61,8 +64,8 @@ export class TestInfoImpl implements TestInfo {
readonly retry: number; readonly retry: number;
readonly workerIndex: number; readonly workerIndex: number;
readonly parallelIndex: number; readonly parallelIndex: number;
readonly project: FullProjectInternal; readonly project: FullProject;
config: FullConfigInternal; readonly config: FullConfig;
readonly title: string; readonly title: string;
readonly titlePath: string[]; readonly titlePath: string[];
readonly file: string; readonly file: string;
@ -101,8 +104,8 @@ export class TestInfoImpl implements TestInfo {
} }
constructor( constructor(
config: FullConfigInternal, configInternal: FullConfigInternal,
project: FullProjectInternal, projectInternal: FullProjectInternal,
workerParams: WorkerInitParams, workerParams: WorkerInitParams,
test: TestCase, test: TestCase,
retry: number, retry: number,
@ -120,8 +123,10 @@ export class TestInfoImpl implements TestInfo {
this.retry = retry; this.retry = retry;
this.workerIndex = workerParams.workerIndex; this.workerIndex = workerParams.workerIndex;
this.parallelIndex = workerParams.parallelIndex; this.parallelIndex = workerParams.parallelIndex;
this.project = project; this._projectInternal = projectInternal;
this.config = config; this.project = projectInternal.project;
this._configInternal = configInternal;
this.config = configInternal.config;
this.title = test.title; this.title = test.title;
this.titlePath = test.titlePath(); this.titlePath = test.titlePath();
this.file = test.location.file; this.file = test.location.file;
@ -138,8 +143,8 @@ export class TestInfoImpl implements TestInfo {
const fullTitleWithoutSpec = test.titlePath().slice(1).join(' '); const fullTitleWithoutSpec = test.titlePath().slice(1).join(' ');
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec)); let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
if (project._internal.id) if (projectInternal.id)
testOutputDir += '-' + sanitizeForFilePath(project._internal.id); testOutputDir += '-' + sanitizeForFilePath(projectInternal.id);
if (this.retry) if (this.retry)
testOutputDir += '-retry' + this.retry; testOutputDir += '-retry' + this.retry;
if (this.repeatEachIndex) if (this.repeatEachIndex)
@ -345,7 +350,7 @@ export class TestInfoImpl implements TestInfo {
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath); const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
const projectNamePathSegment = sanitizeForFilePath(this.project.name); const projectNamePathSegment = sanitizeForFilePath(this.project.name);
const snapshotPath = this.project.snapshotPathTemplate const snapshotPath = (this._projectInternal.snapshotPathTemplate || '')
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir) .replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir) .replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '') .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(/\{(.)?arg\}/g, '$1' + path.join(parsedSubPath.dir, parsedSubPath.name))
.replace(/\{(.)?ext\}/g, parsedSubPath.ext ? '$1' + parsedSubPath.ext : ''); .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]) { skip(...args: [arg?: any, description?: string]) {

View file

@ -16,7 +16,8 @@
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { TimeoutRunner, TimeoutRunnerError } from 'playwright-core/lib/utils'; 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 = { export type TimeSlot = {
timeout: number; timeout: number;

View file

@ -21,7 +21,7 @@ import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerI
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { ConfigLoader } from '../common/configLoader'; import { ConfigLoader } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test'; 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 { FixtureRunner } from './fixtureRunner';
import { ManualPromise } from 'playwright-core/lib/utils'; import { ManualPromise } from 'playwright-core/lib/utils';
import { TestInfoImpl } from './testInfo'; import { TestInfoImpl } from './testInfo';
@ -31,6 +31,7 @@ import { loadTestFile } from '../common/testLoader';
import { buildFileSuiteForProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { buildFileSuiteForProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { PoolBuilder } from '../common/poolBuilder'; import { PoolBuilder } from '../common/poolBuilder';
import { addToCompilationCache } from '../common/compilationCache'; import { addToCompilationCache } from '../common/compilationCache';
import type { TestInfoError } from '../../types/test';
const removeFolderAsync = util.promisify(rimraf); const removeFolderAsync = util.promisify(rimraf);
@ -150,7 +151,7 @@ export class WorkerMain extends ProcessRunner {
private async _teardownScopes() { private async _teardownScopes() {
// TODO: separate timeout for teardown? // TODO: separate timeout for teardown?
const timeoutManager = new TimeoutManager(this._project.timeout); const timeoutManager = new TimeoutManager(this._project.project.timeout);
timeoutManager.setCurrentRunnable({ type: 'teardown' }); timeoutManager.setCurrentRunnable({ type: 'teardown' });
const timeoutError = await timeoutManager.runWithTimeout(async () => { const timeoutError = await timeoutManager.runWithTimeout(async () => {
await this._fixtureRunner.teardownScope('test', timeoutManager); await this._fixtureRunner.teardownScope('test', timeoutManager);
@ -189,9 +190,8 @@ export class WorkerMain extends ProcessRunner {
if (this._config) if (this._config)
return; return;
const configLoader = await ConfigLoader.deserialize(this._params.config); this._config = await ConfigLoader.deserialize(this._params.config);
this._config = configLoader.fullConfig(); this._project = this._config.projects.find(p => p.id === this._params.projectId)!;
this._project = this._config.projects.find(p => p._internal.id === this._params.projectId)!;
this._poolBuilder = PoolBuilder.createForWorker(this._project); this._poolBuilder = PoolBuilder.createForWorker(this._project);
} }
@ -201,7 +201,7 @@ export class WorkerMain extends ProcessRunner {
let fatalUnknownTestIds; let fatalUnknownTestIds;
try { try {
await this._loadIfNeeded(); 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 suite = buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex);
const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id)); const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id));
if (hasEntries) { if (hasEntries) {
@ -332,7 +332,7 @@ export class WorkerMain extends ProcessRunner {
this._extraSuiteAnnotations.set(suite, extraAnnotations); this._extraSuiteAnnotations.set(suite, extraAnnotations);
didFailBeforeAllForSuite = suite; // Assume failure, unless reset below. didFailBeforeAllForSuite = suite; // Assume failure, unless reset below.
// Separate timeout for each "beforeAll" modifier. // 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); await this._runModifiersForSuite(suite, testInfo, 'worker', timeSlot, extraAnnotations);
} }
@ -385,7 +385,7 @@ export class WorkerMain extends ProcessRunner {
let afterHooksSlot: TimeSlot | undefined; let afterHooksSlot: TimeSlot | undefined;
if (testInfo._didTimeout) { if (testInfo._didTimeout) {
// A timed-out test gets a full additional timeout to run after hooks. // 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 }); testInfo._timeoutManager.setCurrentRunnable({ type: 'afterEach', slot: afterHooksSlot });
} }
await testInfo._runAsStep(async step => { await testInfo._runAsStep(async step => {
@ -450,7 +450,7 @@ export class WorkerMain extends ProcessRunner {
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo); const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
firstAfterHooksError = firstAfterHooksError || afterAllError; 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. // 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 }); testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot });
debugTest(`tearing down test scope started`); debugTest(`tearing down test scope started`);
@ -476,8 +476,8 @@ export class WorkerMain extends ProcessRunner {
setCurrentTestInfo(null); setCurrentTestInfo(null);
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo)); this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
const preserveOutput = this._config.preserveOutput === 'always' || const preserveOutput = this._config.config.preserveOutput === 'always' ||
(this._config.preserveOutput === 'failures-only' && testInfo._isFailure()); (this._config.config.preserveOutput === 'failures-only' && testInfo._isFailure());
if (!preserveOutput) if (!preserveOutput)
await removeFolderAsync(testInfo.outputDir).catch(e => {}); await removeFolderAsync(testInfo.outputDir).catch(e => {});
} }
@ -512,7 +512,7 @@ export class WorkerMain extends ProcessRunner {
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`); debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`);
try { try {
// Separate time slot for each "beforeAll" hook. // 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 }); testInfo._timeoutManager.setCurrentRunnable({ type: 'beforeAll', location: hook.location, slot: timeSlot });
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), { await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
category: 'hook', category: 'hook',
@ -540,7 +540,7 @@ export class WorkerMain extends ProcessRunner {
debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`); debugTest(`${hook.type} hook at "${formatLocation(hook.location)}" started`);
const afterAllError = await testInfo._runFn(async () => { const afterAllError = await testInfo._runFn(async () => {
// Separate time slot for each "afterAll" hook. // 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 }); testInfo._timeoutManager.setCurrentRunnable({ type: 'afterAll', location: hook.location, slot: timeSlot });
await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), { await testInfo._runAsStep(() => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'), {
category: 'hook', category: 'hook',