mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(test): move run options into config (#20568)
This commit is contained in:
parent
6ad4687f4d
commit
cb9ace6035
@ -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
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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]) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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.`);
|
||||||
|
|
||||||
|
|||||||
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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()',
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
packages/playwright-test/types/test.d.ts
vendored
10
packages/playwright-test/types/test.d.ts
vendored
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user