chore(test): move run options into config (#20568)

This commit is contained in:
Pavel Feldman 2023-02-01 15:25:26 -08:00 committed by GitHub
parent 6ad4687f4d
commit cb9ace6035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 150 additions and 143 deletions

View File

@ -90,7 +90,10 @@ export default defineConfig({
- type: ?<[Array]<[string]>> - type: ?<[Array]<[string]>>
List of projects that need to run before any test in this project runs. Dependencies can List of projects that need to run before any test in this project runs. Dependencies can
be useful for configuring the global setup actions in a way that every action is a test. be useful for configuring the global setup actions in a way that every action is
in a form of a test. That way one can record traces and other artifacts for the
global setup routine, see the setup steps in the test report, etc.
For example: For example:
```js ```js

View File

@ -22,7 +22,6 @@ import path from 'path';
import { Runner } from './runner/runner'; import { Runner } from './runner/runner';
import { stopProfiling, startProfiling } from './common/profiler'; import { stopProfiling, startProfiling } from './common/profiler';
import { experimentalLoaderOption, fileIsModule } from './util'; import { experimentalLoaderOption, fileIsModule } from './util';
import type { TestFileFilter } from './util';
import { createTitleMatcher } from './util'; import { createTitleMatcher } from './util';
import { showHTMLReport } from './reporters/html'; import { showHTMLReport } from './reporters/html';
import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
@ -155,9 +154,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
await configLoader.loadConfigFile(resolvedConfigFile); await configLoader.loadConfigFile(resolvedConfigFile);
else else
await configLoader.loadEmptyConfig(configFileOrDirectory); await configLoader.loadEmptyConfig(configFileOrDirectory);
const runner = new Runner(configLoader.fullConfig());
const testFileFilters: TestFileFilter[] = args.map(arg => { const config = configLoader.fullConfig();
config._internal.testFileFilters = args.map(arg => {
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
return { return {
re: forceRegExp(match ? match[1] : arg), re: forceRegExp(match ? match[1] : arg),
@ -165,18 +164,15 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
column: match?.[3] ? parseInt(match[3], 10) : null, column: match?.[3] ? parseInt(match[3], 10) : null,
}; };
}); });
const grepMatcher = opts.grep ? createTitleMatcher(forceRegExp(opts.grep)) : () => true; const grepMatcher = opts.grep ? createTitleMatcher(forceRegExp(opts.grep)) : () => true;
const grepInvertMatcher = opts.grepInvert ? createTitleMatcher(forceRegExp(opts.grepInvert)) : () => false; const grepInvertMatcher = opts.grepInvert ? createTitleMatcher(forceRegExp(opts.grepInvert)) : () => false;
const testTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); config._internal.testTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
config._internal.listOnly = !!opts.list;
config._internal.projectFilter = opts.project || undefined;
config._internal.passWithNoTests = !!opts.passWithNoTests;
const status = await runner.runAllTests({ const runner = new Runner(config);
listOnly: !!opts.list, const status = await runner.runAllTests();
testFileFilters,
testTitleMatcher,
projectFilter: opts.project || undefined,
passWithNoTests: opts.passWithNoTests,
});
await stopProfiling(undefined); await stopProfiling(undefined);
if (status === 'interrupted') if (status === 'interrupted')

View File

@ -30,7 +30,7 @@ export class ConfigLoader {
constructor(configCLIOverrides?: ConfigCLIOverrides) { constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._fullConfig = { ...baseFullConfig }; this._fullConfig = { ...baseFullConfig };
this._fullConfig._configCLIOverrides = configCLIOverrides || {}; this._fullConfig._internal.configCLIOverrides = configCLIOverrides || {};
} }
static async deserialize(data: SerializedConfig): Promise<ConfigLoader> { static async deserialize(data: SerializedConfig): Promise<ConfigLoader> {
@ -60,7 +60,7 @@ export class ConfigLoader {
validateConfig(configFile || '<default config>', config); validateConfig(configFile || '<default config>', config);
// 2. Override settings from CLI. // 2. Override settings from CLI.
const configCLIOverrides = this._fullConfig._configCLIOverrides; const configCLIOverrides = this._fullConfig._internal.configCLIOverrides;
config.forbidOnly = takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly); config.forbidOnly = takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly);
config.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel); config.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel);
config.globalTimeout = takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout); config.globalTimeout = takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout);
@ -101,11 +101,11 @@ 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._configDir = configDir; this._fullConfig._internal.configDir = configDir;
this._fullConfig._storeDir = path.resolve(configDir, '.playwright-store'); this._fullConfig._internal.storeDir = path.resolve(configDir, '.playwright-store');
this._fullConfig.configFile = configFile; this._fullConfig.configFile = configFile;
this._fullConfig.rootDir = config.testDir || configDir; this._fullConfig.rootDir = config.testDir || configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); this._fullConfig._internal.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._internal.globalOutputDir);
this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly);
this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel); this._fullConfig.fullyParallel = takeFirst(config.fullyParallel, baseFullConfig.fullyParallel);
this._fullConfig.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup); this._fullConfig.globalSetup = takeFirst(config.globalSetup, baseFullConfig.globalSetup);
@ -119,9 +119,9 @@ export class ConfigLoader {
this._fullConfig.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests); this._fullConfig.reportSlowTests = takeFirst(config.reportSlowTests, baseFullConfig.reportSlowTests);
this._fullConfig.quiet = takeFirst(config.quiet, baseFullConfig.quiet); this._fullConfig.quiet = takeFirst(config.quiet, baseFullConfig.quiet);
this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard); this._fullConfig.shard = takeFirst(config.shard, baseFullConfig.shard);
this._fullConfig._ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._ignoreSnapshots); this._fullConfig._internal.ignoreSnapshots = takeFirst(config.ignoreSnapshots, baseFullConfig._internal.ignoreSnapshots);
this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots); this._fullConfig.updateSnapshots = takeFirst(config.updateSnapshots, baseFullConfig.updateSnapshots);
this._fullConfig._pluginRegistrations = (config as any)._plugins || []; this._fullConfig._internal.pluginRegistrations = (config as any)._plugins || [];
const workers = takeFirst(config.workers, '50%'); const workers = takeFirst(config.workers, '50%');
if (typeof workers === 'string') { if (typeof workers === 'string') {
@ -139,10 +139,10 @@ export class ConfigLoader {
if (Array.isArray(webServers)) { // multiple web server mode if (Array.isArray(webServers)) { // multiple web server mode
// Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type. // Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type.
this._fullConfig.webServer = null; this._fullConfig.webServer = null;
this._fullConfig._webServers = webServers; this._fullConfig._internal.webServers = webServers;
} else if (webServers) { // legacy singleton mode } else if (webServers) { // legacy singleton mode
this._fullConfig.webServer = webServers; this._fullConfig.webServer = webServers;
this._fullConfig._webServers = [webServers]; this._fullConfig._internal.webServers = [webServers];
} }
this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata); this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath)); this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath));
@ -159,7 +159,7 @@ export class ConfigLoader {
const candidate = name + (i ? i : ''); const candidate = name + (i ? i : '');
if (usedNames.has(candidate)) if (usedNames.has(candidate))
continue; continue;
p._id = candidate; p._internal.id = candidate;
usedNames.add(candidate); usedNames.add(candidate);
break; break;
} }
@ -171,7 +171,7 @@ export class ConfigLoader {
} }
private _applyCLIOverridesToProject(projectConfig: Project) { private _applyCLIOverridesToProject(projectConfig: Project) {
const configCLIOverrides = this._fullConfig._configCLIOverrides; const configCLIOverrides = this._fullConfig._internal.configCLIOverrides;
projectConfig.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel); projectConfig.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel);
projectConfig.outputDir = takeFirst(configCLIOverrides.outputDir, projectConfig.outputDir); projectConfig.outputDir = takeFirst(configCLIOverrides.outputDir, projectConfig.outputDir);
projectConfig.repeatEach = takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach); projectConfig.repeatEach = takeFirst(configCLIOverrides.repeatEach, projectConfig.repeatEach);
@ -183,13 +183,13 @@ export class ConfigLoader {
private _resolveProject(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal { private _resolveProject(config: Config, fullConfig: FullConfigInternal, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal {
// Resolve all config dirs relative to configDir. // Resolve all config dirs relative to configDir.
if (projectConfig.testDir !== undefined) if (projectConfig.testDir !== undefined)
projectConfig.testDir = path.resolve(fullConfig._configDir, projectConfig.testDir); projectConfig.testDir = path.resolve(fullConfig._internal.configDir, projectConfig.testDir);
if (projectConfig.outputDir !== undefined) if (projectConfig.outputDir !== undefined)
projectConfig.outputDir = path.resolve(fullConfig._configDir, projectConfig.outputDir); projectConfig.outputDir = path.resolve(fullConfig._internal.configDir, projectConfig.outputDir);
if (projectConfig.snapshotDir !== undefined) if (projectConfig.snapshotDir !== undefined)
projectConfig.snapshotDir = path.resolve(fullConfig._configDir, projectConfig.snapshotDir); projectConfig.snapshotDir = path.resolve(fullConfig._internal.configDir, projectConfig.snapshotDir);
const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig._configDir); const testDir = takeFirst(projectConfig.testDir, config.testDir, fullConfig._internal.configDir);
const respectGitIgnore = !projectConfig.testDir && !config.testDir; const respectGitIgnore = !projectConfig.testDir && !config.testDir;
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
@ -199,11 +199,15 @@ export class ConfigLoader {
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}'; const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
return { return {
_id: '', _internal: {
_fullConfig: fullConfig, id: '',
_fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), type: 'top-level',
_expect: takeFirst(projectConfig.expect, config.expect, {}), fullConfig: fullConfig,
_deps: [], fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
expect: takeFirst(projectConfig.expect, config.expect, {}),
deps: [],
respectGitIgnore: respectGitIgnore,
},
grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep),
grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert), grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, baseFullConfig.grepInvert),
outputDir, outputDir,
@ -212,7 +216,6 @@ export class ConfigLoader {
metadata: takeFirst(projectConfig.metadata, config.metadata, undefined), metadata: takeFirst(projectConfig.metadata, config.metadata, undefined),
name, name,
testDir, testDir,
_respectGitIgnore: respectGitIgnore,
snapshotDir, snapshotDir,
snapshotPathTemplate, snapshotPathTemplate,
testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []), testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []),
@ -433,14 +436,19 @@ export const baseFullConfig: FullConfigInternal = {
version: require('../../package.json').version, version: require('../../package.json').version,
workers: 0, workers: 0,
webServer: null, webServer: null,
_webServers: [], _internal: {
_globalOutputDir: path.resolve(process.cwd()), webServers: [],
_configDir: '', globalOutputDir: path.resolve(process.cwd()),
_configCLIOverrides: {}, configDir: '',
_storeDir: '', configCLIOverrides: {},
_maxConcurrentTestGroups: 0, storeDir: '',
_ignoreSnapshots: false, maxConcurrentTestGroups: 0,
_pluginRegistrations: [], ignoreSnapshots: false,
pluginRegistrations: [],
testTitleMatcher: () => true,
testFileFilters: [],
listOnly: false,
}
}; };
function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined { function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {
@ -466,7 +474,7 @@ function resolveProjectDependencies(projects: FullProjectInternal[]) {
throw new Error(`Project '${project.name}' depends on unknown project '${dependencyName}'`); throw new Error(`Project '${project.name}' depends on unknown project '${dependencyName}'`);
if (dependencies.length > 1) if (dependencies.length > 1)
throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`); throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`);
project._deps.push(...dependencies); project._internal.deps.push(...dependencies);
} }
} }
} }

View File

@ -37,7 +37,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._expect?.timeout; let defaultExpectTimeout = testInfo?.project._internal.expect?.timeout;
if (typeof defaultExpectTimeout === 'undefined') if (typeof defaultExpectTimeout === 'undefined')
defaultExpectTimeout = 5000; defaultExpectTimeout = 5000;
return defaultExpectTimeout; return defaultExpectTimeout;

View File

@ -124,8 +124,8 @@ export type TeardownErrorsPayload = {
export function serializeConfig(config: FullConfigInternal): SerializedConfig { export function serializeConfig(config: FullConfigInternal): SerializedConfig {
const result: SerializedConfig = { const result: SerializedConfig = {
configFile: config.configFile, configFile: config.configFile,
configDir: config._configDir, configDir: config._internal.configDir,
configCLIOverrides: config._configCLIOverrides, configCLIOverrides: config._internal.configCLIOverrides,
}; };
return result; return result;
} }

View File

@ -103,7 +103,7 @@ export class PoolBuilder {
if (Object.entries(optionsFromConfig).length) { if (Object.entries(optionsFromConfig).length) {
// Add config options immediately after original option definition, // Add config options immediately after original option definition,
// so that any test.use() override it. // so that any test.use() override it.
result.push({ fixtures: optionsFromConfig, location: { file: `project#${project._id}`, line: 1, column: 1 }, fromConfig: true }); result.push({ fixtures: optionsFromConfig, location: { file: `project#${project._internal.id}`, line: 1, column: 1 }, fromConfig: true });
} }
} }
return result; return result;

View File

@ -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._id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`; const testIdExpression = `[project=${project._internal.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._id; test._projectId = project._internal.id;
// Inherit properties from parent suites. // Inherit properties from parent suites.
let inheritedRetries: number | undefined; let inheritedRetries: number | undefined;
@ -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._id}-${test._poolDigest}-${repeatEachIndex}`; test._workerHash = `${project._internal.id}-${test._poolDigest}-${repeatEachIndex}`;
}); });
return result; return result;

View File

@ -117,8 +117,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._id) if (project._internal.id)
testOutputDir += '-' + sanitizeForFilePath(project._id); testOutputDir += '-' + sanitizeForFilePath(project._internal.id);
if (this.retry) if (this.retry)
testOutputDir += '-retry' + this.retry; testOutputDir += '-retry' + this.retry;
if (this.repeatEachIndex) if (this.repeatEachIndex)
@ -302,7 +302,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._configDir, snapshotPath)); return path.normalize(path.resolve(this.config._internal.configDir, snapshotPath));
} }
skip(...args: [arg?: any, description?: string]) { skip(...args: [arg?: any, description?: string]) {

View File

@ -17,6 +17,7 @@
import type { Fixtures, TestInfoError, Project } from '../../types/test'; import type { Fixtures, TestInfoError, Project } from '../../types/test';
import type { Location } from '../../types/testReporter'; import type { Location } from '../../types/testReporter';
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
import type { Matcher, TestFileFilter } from '../util';
import type { ConfigCLIOverrides } from './ipc'; import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types'; import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
export * from '../../types/test'; export * from '../../types/test';
@ -39,40 +40,54 @@ export interface TestStepInternal {
refinedTitle?: string; refinedTitle?: string;
} }
type ConfigInternal = {
globalOutputDir: string;
configDir: string;
configCLIOverrides: ConfigCLIOverrides;
storeDir: string;
maxConcurrentTestGroups: number;
ignoreSnapshots: boolean;
webServers: Exclude<FullConfigPublic['webServer'], null>[];
pluginRegistrations: TestRunnerPluginRegistration[];
listOnly: boolean;
testFileFilters: TestFileFilter[];
testTitleMatcher: Matcher;
projectFilter?: string[];
passWithNoTests?: boolean;
};
/** /**
* FullConfigInternal allows the plumbing of configuration details throughout the Test Runner without * FullConfigInternal allows the plumbing of configuration details throughout the Test Runner without
* increasing the surface area of the public API type called FullConfig. * increasing the surface area of the public API type called FullConfig.
*/ */
export interface FullConfigInternal extends FullConfigPublic { export interface FullConfigInternal extends FullConfigPublic {
_globalOutputDir: string; _internal: ConfigInternal;
_configDir: string;
_configCLIOverrides: ConfigCLIOverrides;
_storeDir: string;
_maxConcurrentTestGroups: number;
_ignoreSnapshots: boolean;
/** /**
* If populated, this should also be the first/only entry in _webServers. Legacy singleton `webServer` as well as those provided via an array in the user-facing playwright.config.{ts,js} will be in `_webServers`. The legacy field (`webServer`) field additionally stores the backwards-compatible singleton `webServer` since it had been showing up in globalSetup to the user. * If populated, this should also be the first/only entry in _webServers. Legacy singleton `webServer` as well as those provided via an array in the user-facing playwright.config.{ts,js} will be in `_webServers`. The legacy field (`webServer`) field additionally stores the backwards-compatible singleton `webServer` since it had been showing up in globalSetup to the user.
*/ */
webServer: FullConfigPublic['webServer']; webServer: FullConfigPublic['webServer'];
_webServers: Exclude<FullConfigPublic['webServer'], null>[];
// Overrides the public field. // Overrides the public field.
projects: FullProjectInternal[]; projects: FullProjectInternal[];
_pluginRegistrations: TestRunnerPluginRegistration[];
} }
type ProjectInternal = {
id: string;
type: 'top-level' | 'dependency';
fullConfig: FullConfigInternal;
fullyParallel: boolean;
expect: Project['expect'];
respectGitIgnore: boolean;
deps: FullProjectInternal[];
};
/** /**
* FullProjectInternal allows the plumbing of configuration details throughout the Test Runner without * FullProjectInternal allows the plumbing of configuration details throughout the Test Runner without
* increasing the surface area of the public API type called FullProject. * increasing the surface area of the public API type called FullProject.
*/ */
export interface FullProjectInternal extends FullProjectPublic { export interface FullProjectInternal extends FullProjectPublic {
_id: string; _internal: ProjectInternal;
_fullConfig: FullConfigInternal;
_fullyParallel: boolean;
_expect: Project['expect'];
_respectGitIgnore: boolean;
_deps: FullProjectInternal[];
snapshotPathTemplate: string; snapshotPathTemplate: string;
} }

View File

@ -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._ignoreSnapshots) if (testInfo.config._internal.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._expect?.toMatchSnapshot || {}, testInfo.project._internal.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._ignoreSnapshots) if (testInfo.config._internal.ignoreSnapshots)
return { pass: !this.isNot, message: () => '' }; return { pass: !this.isNot, message: () => '' };
const config = (testInfo.project._expect as any)?.toHaveScreenshot; const config = (testInfo.project._internal.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

@ -209,7 +209,7 @@ 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.webServer;
const webServerPlugins = []; const webServerPlugins = [];
for (const webServerConfig of config._webServers) { for (const webServerConfig of config._internal.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

@ -121,7 +121,7 @@ export class BaseReporter implements Reporter {
} }
protected generateStartingMessage() { protected generateStartingMessage() {
const jobs = Math.min(this.config.workers, this.config._maxConcurrentTestGroups); const jobs = Math.min(this.config.workers, this.config._internal.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}` : '';
return `\nRunning ${this.totalTestCount} test${this.totalTestCount !== 1 ? 's' : ''} using ${jobs} worker${jobs !== 1 ? 's' : ''}${shardDetails}`; return `\nRunning ${this.totalTestCount} test${this.totalTestCount !== 1 ? 's' : ''} using ${jobs} worker${jobs !== 1 ? 's' : ''}${shardDetails}`;
} }

View File

@ -92,9 +92,9 @@ class HtmlReporter implements Reporter {
_resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption } { _resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption } {
let { outputFolder } = this._options; let { outputFolder } = this._options;
if (outputFolder) if (outputFolder)
outputFolder = path.resolve(this.config._configDir, outputFolder); outputFolder = path.resolve(this.config._internal.configDir, outputFolder);
return { return {
outputFolder: reportFolderFromEnv() ?? outputFolder ?? defaultReportFolder(this.config._configDir), outputFolder: reportFolderFromEnv() ?? outputFolder ?? defaultReportFolder(this.config._internal.configDir),
open: process.env.PW_TEST_HTML_REPORT_OPEN as any || this._options.open || 'on-failure', open: process.env.PW_TEST_HTML_REPORT_OPEN as any || this._options.open || 'on-failure',
}; };
} }

View File

@ -23,21 +23,13 @@ import type { TestCase } from '../common/test';
import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { createFileMatcherFromFilters, createTitleMatcher, errorWithFile } from '../util'; import { createFileMatcherFromFilters, createTitleMatcher, errorWithFile } from '../util';
import type { Matcher, TestFileFilter } from '../util'; import type { Matcher, TestFileFilter } from '../util';
import { collectFilesForProject, filterProjects, projectsThatAreDependencies } from './projectUtils'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
import { requireOrImport } from '../common/transform'; import { requireOrImport } from '../common/transform';
import { buildFileSuiteForProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { buildFileSuiteForProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { filterForShard } from './testGroups'; import { filterForShard } from './testGroups';
type LoadOptions = { export async function loadAllTests(config: FullConfigInternal, errors: TestError[]): Promise<Suite> {
listOnly: boolean; const projects = filterProjects(config.projects, config._internal.projectFilter);
testFileFilters: TestFileFilter[];
testTitleMatcher?: Matcher;
projectFilter?: string[];
passWithNoTests?: boolean;
};
export async function loadAllTests(config: FullConfigInternal, options: LoadOptions, errors: TestError[]): Promise<Suite> {
const projects = filterProjects(config.projects, options.projectFilter);
let filesToRunByProject = new Map<FullProjectInternal, string[]>(); let filesToRunByProject = new Map<FullProjectInternal, string[]>();
let topLevelProjects: FullProjectInternal[]; let topLevelProjects: FullProjectInternal[];
@ -54,14 +46,16 @@ export async function loadAllTests(config: FullConfigInternal, options: LoadOpti
} }
// Filter files based on the file filters, eliminate the empty projects. // Filter files based on the file filters, eliminate the empty projects.
const commandLineFileMatcher = options.testFileFilters.length ? createFileMatcherFromFilters(options.testFileFilters) : null; const commandLineFileMatcher = config._internal.testFileFilters.length ? createFileMatcherFromFilters(config._internal.testFileFilters) : null;
for (const [project, files] of allFilesForProject) { for (const [project, files] of allFilesForProject) {
const filteredFiles = commandLineFileMatcher ? files.filter(commandLineFileMatcher) : files; const filteredFiles = commandLineFileMatcher ? files.filter(commandLineFileMatcher) : files;
if (filteredFiles.length) if (filteredFiles.length)
filesToRunByProject.set(project, filteredFiles); filesToRunByProject.set(project, filteredFiles);
} }
// Remove dependency projects, they'll be added back later.
for (const project of projectsThatAreDependencies([...filesToRunByProject.keys()])) const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]);
// Remove files for dependency projects, they'll be added back later.
for (const project of projectClosure.filter(p => p._internal.type === 'dependency'))
filesToRunByProject.delete(project); filesToRunByProject.delete(project);
// Shard only the top-level projects. // Shard only the top-level projects.
@ -69,8 +63,9 @@ export async function loadAllTests(config: FullConfigInternal, options: LoadOpti
filesToRunByProject = filterForShard(config.shard, filesToRunByProject); filesToRunByProject = filterForShard(config.shard, filesToRunByProject);
// Re-build the closure, project set might have changed. // Re-build the closure, project set might have changed.
topLevelProjects = [...filesToRunByProject.keys()]; const filteredProjectClosure = buildProjectsClosure([...filesToRunByProject.keys()]);
dependencyProjects = projectsThatAreDependencies(topLevelProjects); topLevelProjects = filteredProjectClosure.filter(p => p._internal.type === 'top-level');
dependencyProjects = filteredProjectClosure.filter(p => p._internal.type === 'dependency');
// (Re-)add all files for dependent projects, disregard filters. // (Re-)add all files for dependent projects, disregard filters.
for (const project of dependencyProjects) { for (const project of dependencyProjects) {
@ -101,7 +96,7 @@ export async function loadAllTests(config: FullConfigInternal, options: LoadOpti
// First iterate leaf projects to focus only, then add all other projects. // First iterate leaf projects to focus only, then add all other projects.
for (const project of topLevelProjects) { for (const project of topLevelProjects) {
const projectSuite = await createProjectSuite(fileSuits, project, options, filesToRunByProject.get(project)!); const projectSuite = await createProjectSuite(fileSuits, project, config._internal, filesToRunByProject.get(project)!);
if (projectSuite) if (projectSuite)
rootSuite._addSuite(projectSuite); rootSuite._addSuite(projectSuite);
} }
@ -118,7 +113,7 @@ export async function loadAllTests(config: FullConfigInternal, options: LoadOpti
// Prepend the projects that are dependencies. // Prepend the projects that are dependencies.
for (const project of dependencyProjects) { for (const project of dependencyProjects) {
const projectSuite = await createProjectSuite(fileSuits, project, { ...options, testFileFilters: [], testTitleMatcher: undefined }, filesToRunByProject.get(project)!); const projectSuite = await createProjectSuite(fileSuits, project, { testFileFilters: [], testTitleMatcher: undefined }, filesToRunByProject.get(project)!);
if (projectSuite) if (projectSuite)
rootSuite._prependSuite(projectSuite); rootSuite._prependSuite(projectSuite);
} }
@ -126,14 +121,14 @@ export async function loadAllTests(config: FullConfigInternal, options: LoadOpti
return rootSuite; return rootSuite;
} }
async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: LoadOptions, files: string[]): Promise<Suite | null> { async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: { testFileFilters: TestFileFilter[], testTitleMatcher?: Matcher }, files: string[]): Promise<Suite | null> {
const fileSuitesMap = new Map<string, Suite>(); const fileSuitesMap = new Map<string, Suite>();
for (const fileSuite of fileSuits) for (const fileSuite of fileSuits)
fileSuitesMap.set(fileSuite._requireFile, fileSuite); fileSuitesMap.set(fileSuite._requireFile, fileSuite);
const projectSuite = new Suite(project.name, 'project'); const projectSuite = new Suite(project.name, 'project');
projectSuite._projectConfig = project; projectSuite._projectConfig = project;
if (project._fullyParallel) if (project._internal.fullyParallel)
projectSuite._parallelMode = 'parallel'; projectSuite._parallelMode = 'parallel';
for (const file of files) { for (const file of files) {
const fileSuite = fileSuitesMap.get(file); const fileSuite = fileSuitesMap.get(file);

View File

@ -49,7 +49,7 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
return result; return result;
} }
export function projectsThatAreDependencies(projects: FullProjectInternal[]): FullProjectInternal[] { export function buildProjectsClosure(projects: FullProjectInternal[]): FullProjectInternal[] {
const result = new Set<FullProjectInternal>(); const result = new Set<FullProjectInternal>();
const visit = (depth: number, project: FullProjectInternal) => { const visit = (depth: number, project: FullProjectInternal) => {
if (depth > 100) { if (depth > 100) {
@ -57,19 +57,22 @@ export function projectsThatAreDependencies(projects: FullProjectInternal[]): Fu
error.stack = ''; error.stack = '';
throw error; throw error;
} }
if (result.has(project)) if (depth)
return; project._internal.type = 'dependency';
project._deps.map(visit.bind(undefined, depth + 1)); result.add(project);
project._deps.forEach(dep => result.add(dep)); project._internal.deps.map(visit.bind(undefined, depth + 1));
}; };
projects.forEach(visit.bind(undefined, 0)); for (const p of projects)
p._internal.type = 'top-level';
for (const p of projects)
visit(0, p);
return [...result]; return [...result];
} }
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._respectGitIgnore, fsCache); const allFiles = await cachedCollectFiles(project.testDir, project._internal.respectGitIgnore, fsCache);
const testMatch = createFileMatcher(project.testMatch); const testMatch = createFileMatcher(project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore); const testIgnore = createFileMatcher(project.testIgnore);
const testFiles = allFiles.filter(file => { const testFiles = allFiles.filter(file => {

View File

@ -24,17 +24,8 @@ import { createReporter } from './reporters';
import { createTaskRunner, createTaskRunnerForList } from './tasks'; import { createTaskRunner, createTaskRunnerForList } from './tasks';
import type { TaskRunnerState } from './tasks'; import type { TaskRunnerState } from './tasks';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/types';
import type { Matcher, TestFileFilter } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
export type RunOptions = {
listOnly: boolean;
testFileFilters: TestFileFilter[];
testTitleMatcher: Matcher;
projectFilter?: string[];
passWithNoTests?: boolean;
};
export class Runner { export class Runner {
private _config: FullConfigInternal; private _config: FullConfigInternal;
@ -56,22 +47,22 @@ export class Runner {
return report; return report;
} }
async runAllTests(options: RunOptions): Promise<FullResult['status']> { async runAllTests(): Promise<FullResult['status']> {
const config = this._config; const config = this._config;
const listOnly = config._internal.listOnly;
const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0; const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0;
// Legacy webServer support. // Legacy webServer support.
config._pluginRegistrations.push(...webServerPluginsForConfig(config)); config._internal.pluginRegistrations.push(...webServerPluginsForConfig(config));
// Docker support. // Docker support.
config._pluginRegistrations.push(dockerPlugin); config._internal.pluginRegistrations.push(dockerPlugin);
const reporter = await createReporter(config, options.listOnly); const reporter = await createReporter(config, listOnly);
const taskRunner = options.listOnly ? createTaskRunnerForList(config, reporter) const taskRunner = listOnly ? createTaskRunnerForList(config, reporter)
: createTaskRunner(config, reporter); : createTaskRunner(config, reporter);
const context: TaskRunnerState = { const context: TaskRunnerState = {
config, config,
options,
reporter, reporter,
plugins: [], plugins: [],
phases: [], phases: [],
@ -79,7 +70,7 @@ export class Runner {
reporter.onConfigure(config); reporter.onConfigure(config);
if (!options.listOnly && config._ignoreSnapshots) { if (!listOnly && config._internal.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()',

View File

@ -28,19 +28,10 @@ import { TaskRunner } from './taskRunner';
import type { Suite } from '../common/test'; import type { Suite } from '../common/test';
import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { loadAllTests, loadGlobalHook } from './loadUtils'; import { loadAllTests, loadGlobalHook } from './loadUtils';
import type { Matcher, TestFileFilter } from '../util';
const removeFolderAsync = promisify(rimraf); const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir); const readDirAsync = promisify(fs.readdir);
type TaskRunnerOptions = {
listOnly: boolean;
testFileFilters: TestFileFilter[];
testTitleMatcher: Matcher;
projectFilter?: string[];
passWithNoTests?: boolean;
};
type ProjectWithTestGroups = { type ProjectWithTestGroups = {
project: FullProjectInternal; project: FullProjectInternal;
projectSuite: Suite; projectSuite: Suite;
@ -48,7 +39,6 @@ type ProjectWithTestGroups = {
}; };
export type TaskRunnerState = { export type TaskRunnerState = {
options: TaskRunnerOptions;
reporter: Multiplexer; reporter: Multiplexer;
config: FullConfigInternal; config: FullConfigInternal;
plugins: TestRunnerPlugin[]; plugins: TestRunnerPlugin[];
@ -62,7 +52,7 @@ export type TaskRunnerState = {
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> { export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout); const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
for (const plugin of config._pluginRegistrations) for (const plugin of config._internal.pluginRegistrations)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
if (config.globalSetup || config.globalTeardown) if (config.globalSetup || config.globalTeardown)
taskRunner.addTask('global setup', createGlobalSetupTask()); taskRunner.addTask('global setup', createGlobalSetupTask());
@ -102,7 +92,7 @@ function createPluginSetupTask(pluginRegistration: TestRunnerPluginRegistration)
else else
plugin = pluginRegistration; plugin = pluginRegistration;
plugins.push(plugin); plugins.push(plugin);
await plugin.setup?.(config, config._configDir, reporter); await plugin.setup?.(config, config._internal.configDir, reporter);
return () => plugin.teardown?.(); return () => plugin.teardown?.();
}; };
} }
@ -121,10 +111,10 @@ function createGlobalSetupTask(): Task<TaskRunnerState> {
} }
function createRemoveOutputDirsTask(): Task<TaskRunnerState> { function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
return async ({ config, options }) => { 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 (!options.projectFilter || options.projectFilter.includes(p.name)) if (!config._internal.projectFilter || config._internal.projectFilter.includes(p.name))
outputDirs.add(p.outputDir); outputDirs.add(p.outputDir);
} }
@ -144,10 +134,10 @@ function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
function createLoadTask(): Task<TaskRunnerState> { function createLoadTask(): Task<TaskRunnerState> {
return async (context, errors) => { return async (context, errors) => {
const { config, options } = context; const { config } = context;
context.rootSuite = await loadAllTests(config, options, errors); context.rootSuite = await loadAllTests(config, errors);
// Fail when no tests. // Fail when no tests.
if (!context.rootSuite.allTests().length && !context.options.passWithNoTests && !config.shard) if (!context.rootSuite.allTests().length && !config._internal.passWithNoTests && !config.shard)
throw new Error(`No tests found`); throw new Error(`No tests found`);
}; };
} }
@ -170,7 +160,7 @@ function createTestGroupsTask(): Task<TaskRunnerState> {
const testGroupsInPhase = projects.reduce((acc, project) => acc + project.testGroups.length, 0); const testGroupsInPhase = projects.reduce((acc, project) => acc + project.testGroups.length, 0);
debug('pw:test:task')(`running phase with ${projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); debug('pw:test:task')(`running phase with ${projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
context.phases.push({ dispatcher: new Dispatcher(config, reporter), projects }); context.phases.push({ dispatcher: new Dispatcher(config, reporter), projects });
context.config._maxConcurrentTestGroups = Math.max(context.config._maxConcurrentTestGroups, testGroupsInPhase); context.config._internal.maxConcurrentTestGroups = Math.max(context.config._internal.maxConcurrentTestGroups, testGroupsInPhase);
} }
return async () => { return async () => {
@ -191,7 +181,7 @@ function createRunTestsTask(): Task<TaskRunnerState> {
// that depend on the projects that failed previously. // that depend on the projects that failed previously.
const phaseTestGroups: TestGroup[] = []; const phaseTestGroups: TestGroup[] = [];
for (const { project, testGroups } of projects) { for (const { project, testGroups } of projects) {
const hasFailedDeps = project._deps.some(p => !successfulProjects.has(p)); const hasFailedDeps = project._internal.deps.some(p => !successfulProjects.has(p));
if (!hasFailedDeps) { if (!hasFailedDeps) {
phaseTestGroups.push(...testGroups); phaseTestGroups.push(...testGroups);
} else { } else {
@ -211,7 +201,7 @@ function createRunTestsTask(): Task<TaskRunnerState> {
// 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._deps.some(p => !successfulProjects.has(p)); const hasFailedDeps = project._internal.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);
} }
@ -228,7 +218,7 @@ function buildPhases(projectSuites: Suite[]): Suite[][] {
for (const projectSuite of projectSuites) { for (const projectSuite of projectSuites) {
if (processed.has(projectSuite._projectConfig!)) if (processed.has(projectSuite._projectConfig!))
continue; continue;
if (projectSuite._projectConfig!._deps.find(p => !processed.has(p))) if (projectSuite._projectConfig!._internal.deps.find(p => !processed.has(p)))
continue; continue;
phase.push(projectSuite); phase.push(projectSuite);
} }

View File

@ -194,7 +194,7 @@ export class WorkerMain extends ProcessRunner {
const configLoader = await ConfigLoader.deserialize(this._params.config); const configLoader = await ConfigLoader.deserialize(this._params.config);
this._config = configLoader.fullConfig(); 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);
} }

View File

@ -187,7 +187,10 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
name: string; name: string;
/** /**
* List of projects that need to run before any test in this project runs. Dependencies can be useful for configuring * List of projects that need to run before any test in this project runs. Dependencies can be useful for configuring
* the global setup actions in a way that every action is a test. For example: * the global setup actions in a way that every action is in a form of a test. That way one can record traces and
* other artifacts for the global setup routine, see the setup steps in the test report, etc.
*
* For example:
* *
* ```js * ```js
* // playwright.config.ts * // playwright.config.ts
@ -5036,7 +5039,10 @@ export interface TestInfoError {
interface TestProject { interface TestProject {
/** /**
* List of projects that need to run before any test in this project runs. Dependencies can be useful for configuring * List of projects that need to run before any test in this project runs. Dependencies can be useful for configuring
* the global setup actions in a way that every action is a test. For example: * the global setup actions in a way that every action is in a form of a test. That way one can record traces and
* other artifacts for the global setup routine, see the setup steps in the test report, etc.
*
* For example:
* *
* ```js * ```js
* // playwright.config.ts * // playwright.config.ts