chore: bundle common (#40120)

This commit is contained in:
Pavel Feldman
2026-04-08 18:47:24 -07:00
committed by GitHub
parent cf90d14678
commit bc5448504e
61 changed files with 477 additions and 427 deletions
+2 -2
View File
@@ -17,7 +17,7 @@
import { wrapObject } from './injected/serializers';
import { Router } from './router';
import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config';
import type { config, FullConfigInternal } from 'playwright/lib/common';
import type { RouterFixture } from '../index';
import type { ImportRef } from './injected/importRegistry';
import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } from '../types/component';
@@ -37,7 +37,7 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
};
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions;
type BaseTestFixtures = {
_optionContextReuseMode: ContextReuseMode
_optionContextReuseMode: config.ContextReuseMode
};
export const fixtures: Fixtures<TestFixtures, WorkerFixtures, BaseTestFixtures> = {
@@ -16,10 +16,9 @@
import path from 'path';
import { declare, traverse, types } from 'playwright/lib/transform/babelBundle';
import { babel } from 'playwright/lib/common';
import type { BabelAPI, PluginObj, T } from 'playwright/lib/transform/babelBundle';
const t: typeof T = types;
const t: typeof babel.T = babel.types;
let jsxComponentNames: Set<string>;
let importInfos: Map<string, ImportInfo>;
@@ -28,10 +27,10 @@ type TsxTransformOptions = {
setTransformData: (key: string, value: any) => void;
};
export default declare((api: BabelAPI, options: TsxTransformOptions) => {
export default babel.declare((api: babel.BabelAPI, options: TsxTransformOptions) => {
api.assertVersion(7);
const result: PluginObj = {
const result: babel.PluginObj = {
name: 'playwright-debug-transform',
visitor: {
Program: {
@@ -130,9 +129,9 @@ export default declare((api: BabelAPI, options: TsxTransformOptions) => {
return result;
});
function collectJsxComponentUsages(node: T.Node): Set<string> {
function collectJsxComponentUsages(node: babel.T.Node): Set<string> {
const names = new Set<string>();
traverse(node, {
babel.traverse(node, {
enter: p => {
// Treat JSX-everything as component usages.
if (t.isJSXElement(p.node)) {
@@ -153,7 +152,7 @@ export type ImportInfo = {
remoteName: string | undefined;
};
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
export function importInfo(importNode: babel.T.ImportDeclaration, specifier: babel.T.ImportSpecifier | babel.T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
const importSource = importNode.source.value;
const idPrefix = path.join(filename, '..', importSource).replace(/[^\w_\d]/g, '_');
@@ -19,8 +19,7 @@ import path from 'path';
import { iso, utils } from 'playwright-core/lib/coreBundle';
import { colors, debug, stoppable } from 'playwright-core/lib/utilsBundle';
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache';
import { resolveHook } from 'playwright/lib/transform/transform';
import { cc, transform } from 'playwright/lib/common';
import { removeDirAndLogToConsole } from 'playwright/lib/util';
import { source as injectedSource } from './generated/indexSource';
@@ -177,7 +176,7 @@ export async function buildBundle(config: FullConfig, configDir: string): Promis
for (const d of buildInfo.deps[component])
deps.add(d);
}
setExternalDependencies(importingFile, [...deps]);
cc.setExternalDependencies(importingFile, [...deps]);
}
}
@@ -255,7 +254,7 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
async writeBundle(this: PluginContext) {
for (const importInfo of importInfos.values()) {
const importPath = resolveHook(importInfo.filename, importInfo.importSource);
const importPath = transform.resolveHook(importInfo.filename, importInfo.importSource);
if (!importPath)
continue;
const deps = new Set<string>();
+5 -6
View File
@@ -17,8 +17,7 @@
import fs from 'fs';
import path from 'path';
import { getUserData } from 'playwright/lib/transform/compilationCache';
import { resolveHook } from 'playwright/lib/transform/transform';
import { cc, transform } from 'playwright/lib/common';
import { debug } from 'playwright-core/lib/utilsBundle';
import type { ImportInfo } from './tsxTransform';
@@ -145,18 +144,18 @@ export async function createConfig(dirs: ComponentDirs, config: FullConfig, fram
}
export async function populateComponentsFromTests(componentRegistry: ComponentRegistry, componentsByImportingFile?: Map<string, string[]>) {
const importInfos: Map<string, ImportInfo[]> = await getUserData('playwright-ct-core');
const importInfos: Map<string, ImportInfo[]> = await cc.getUserData('playwright-ct-core');
for (const [file, importList] of importInfos) {
for (const importInfo of importList)
componentRegistry.set(importInfo.id, importInfo);
if (componentsByImportingFile)
componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource)).filter(Boolean) as string[]);
componentsByImportingFile.set(file, importList.map(i => transform.resolveHook(i.filename, i.importSource)).filter(Boolean) as string[]);
}
}
export function hasJSComponents(components: ImportInfo[]): boolean {
for (const component of components) {
const importPath = resolveHook(component.filename, component.importSource);
const importPath = transform.resolveHook(component.filename, component.importSource);
const extname = importPath ? path.extname(importPath) : '';
if (extname === '.js' || (importPath && !extname && fs.existsSync(importPath + '.js')))
return true;
@@ -189,7 +188,7 @@ export function transformIndexFile(id: string, content: string, templateDir: str
lines.push(registerSource);
for (const value of importInfos.values()) {
const importPath = resolveHook(value.filename, value.importSource) || value.importSource;
const importPath = transform.resolveHook(value.filename, value.importSource) || value.importSource;
lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`);
}
+1 -5
View File
@@ -19,16 +19,12 @@
"default": "./index.js"
},
"./package.json": "./package.json",
"./lib/common/configLoader": "./lib/common/configLoader.js",
"./lib/common": "./lib/common/index.js",
"./lib/fsWatcher": "./lib/fsWatcher.js",
"./lib/mcp/index": "./lib/mcp/index.js",
"./lib/program": "./lib/program.js",
"./lib/reporters/base": "./lib/reporters/base.js",
"./lib/reporters/list": "./lib/reporters/list.js",
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
"./lib/transform/transform": "./lib/transform/transform.js",
"./lib/plugins": "./lib/plugins/index.js",
"./lib/runner/testRunner": "./lib/runner/testRunner.js",
"./jsx-runtime": {
@@ -21,11 +21,11 @@ import colors from 'colors/safe';
import yaml from 'yaml';
import { mkdirIfNeeded } from '@utils/fileUtils';
import { FullConfigInternal } from '../common/config';
import { defaultSeedFile, findSeedFile, seedFileContent, seedProject } from '../mcp/test/seed';
import { parseAgentSpec } from './agentParser';
import type { AgentSpec } from './agentParser';
import type { FullConfigInternal } from '../common';
/* eslint-disable no-console */
@@ -35,8 +35,8 @@ async function loadAgentSpecs(): Promise<AgentSpec[]> {
}
export class ClaudeGenerator {
static async init(config: FullConfigInternal, projectName: string, prompts: boolean) {
await initRepo(config, projectName, {
static async init(fullConfig: FullConfigInternal, projectName: string, prompts: boolean) {
await initRepo(fullConfig, projectName, {
promptsFolder: prompts ? '.claude/prompts' : undefined,
});
@@ -91,8 +91,8 @@ export class ClaudeGenerator {
}
export class OpencodeGenerator {
static async init(config: FullConfigInternal, projectName: string, prompts: boolean) {
await initRepo(config, projectName, {
static async init(fullConfig: FullConfigInternal, projectName: string, prompts: boolean) {
await initRepo(fullConfig, projectName, {
defaultAgentName: 'Build',
promptsFolder: prompts ? '.opencode/prompts' : undefined
});
@@ -156,9 +156,9 @@ export class OpencodeGenerator {
}
}
export class CopilotGenerator {
static async init(config: FullConfigInternal, projectName: string, prompts: boolean) {
static async init(fullConfig: FullConfigInternal, projectName: string, prompts: boolean) {
await initRepo(config, projectName, {
await initRepo(fullConfig, projectName, {
defaultAgentName: 'agent',
promptsFolder: prompts ? '.github/prompts' : undefined,
promptSuffix: 'prompt'
@@ -228,8 +228,8 @@ export class CopilotGenerator {
}
export class VSCodeGenerator {
static async init(config: FullConfigInternal, projectName: string) {
await initRepo(config, projectName, {
static async init(fullConfig: FullConfigInternal, projectName: string) {
await initRepo(fullConfig, projectName, {
promptsFolder: undefined
});
const agents = await loadAgentSpecs();
@@ -341,8 +341,8 @@ type RepoParams = {
defaultAgentName?: string;
};
async function initRepo(config: FullConfigInternal, projectName: string, options: RepoParams) {
const project = seedProject(config, projectName);
async function initRepo(fullConfig: FullConfigInternal, projectName: string, options: RepoParams) {
const project = seedProject(fullConfig, projectName);
console.log(` 🎭 Using project "${project.project.name}" as a primary project`);
if (!fs.existsSync('specs')) {
+3 -4
View File
@@ -18,8 +18,7 @@ import fs from 'fs';
import path from 'path';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { builtInReporters, defaultReporter } from '../common/config';
import { loadConfigFromFile, loadEmptyConfigForMergeReports } from '../common/configLoader';
import { builtInReporters, config as commonConfig, configLoader } from '../common';
import { showHTMLReport } from '../reporters/html';
import { createMergedReport } from '../reporters/merge';
@@ -31,7 +30,7 @@ export async function showReport(report: string | undefined, host: string, port:
export async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) {
const configFile = opts.config;
const config = configFile ? await loadConfigFromFile(configFile) : await loadEmptyConfigForMergeReports();
const config = configFile ? await configLoader.loadConfigFromFile(configFile) : await configLoader.loadEmptyConfigForMergeReports();
const dir = path.resolve(process.cwd(), reportDir || '');
const dirStat = await fs.promises.stat(dir).catch(e => null);
@@ -43,7 +42,7 @@ export async function mergeReports(reportDir: string | undefined, opts: { [key:
if (!reporterDescriptions && configFile)
reporterDescriptions = config.config.reporter;
if (!reporterDescriptions)
reporterDescriptions = [[defaultReporter]];
reporterDescriptions = [[commonConfig.defaultReporter]];
const rootDirOverride = configFile ? config.config.rootDir : undefined;
await createMergedReport(config, dir, reporterDescriptions!, rootDirOverride);
gracefullyProcessExitDoNotHang(0);
+8 -11
View File
@@ -20,23 +20,20 @@ import path from 'path';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { startProfiling, stopProfiling } from '@utils/profiler';
import { builtInReporters } from '../common/config';
import { loadConfigFromFile, resolveConfigLocation } from '../common/configLoader';
import { builtInReporters, configLoader, ipc } from '../common';
import { terminalScreen } from '../reporters/base';
import { filterProjects } from '../runner/projectUtils';
import * as testServer from '../runner/testServer';
import { runWatchModeLoop } from '../runner/watchMode';
import { runAllTestsWithConfig, TestRunner } from '../runner/testRunner';
import { createErrorCollectingReporter } from '../runner/reporters';
import type { ConfigCLIOverrides } from '../common/ipc';
import type { ReporterDescription } from '../../types/test';
export async function runTests(args: string[], opts: { [key: string]: any }) {
await startProfiling();
const cliOverrides = overridesFromOptions(opts);
const config = await loadConfigFromFile(opts.config, cliOverrides, opts.deps === false);
const config = await configLoader.loadConfigFromFile(opts.config, cliOverrides, opts.deps === false);
config.cliArgs = args;
config.cliGrep = opts.grep as string | undefined;
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
@@ -75,7 +72,7 @@ export async function runTests(args: string[], opts: { [key: string]: any }) {
throw new Error(`--only-changed is not supported in watch mode. If you'd like that to change, file an issue and let us know about your usecase for it.`);
const status = await runWatchModeLoop(
resolveConfigLocation(opts.config),
configLoader.resolveConfigLocation(opts.config),
{
projects: opts.project,
files: args,
@@ -103,19 +100,19 @@ export async function runTestServerAction(opts: { [key: string]: any }) {
}
export async function clearCache(opts: { [key: string]: any }) {
const runner = new TestRunner(resolveConfigLocation(opts.config), {});
const runner = new TestRunner(configLoader.resolveConfigLocation(opts.config), {});
const { status } = await runner.clearCache(createErrorCollectingReporter(terminalScreen));
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
gracefullyProcessExitDoNotHang(exitCode);
}
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
function overridesFromOptions(options: { [key: string]: any }): ipc.ConfigCLIOverrides {
if (options.ui) {
options.debug = undefined;
options.trace = undefined;
}
const overrides: ConfigCLIOverrides = {
const overrides: ipc.ConfigCLIOverrides = {
debug: options.debug,
failOnFlakyTests: options.failOnFlakyTests ? true : undefined,
forbidOnly: options.forbidOnly ? true : undefined,
@@ -172,7 +169,7 @@ function resolveReporterOption(reporter?: string): ReporterDescription[] | undef
return reporter.split(',').map((r: string) => [resolveReporter(r)]);
}
function resolveShardOption(shard?: string): ConfigCLIOverrides['shard'] {
function resolveShardOption(shard?: string): ipc.ConfigCLIOverrides['shard'] {
if (!shard)
return undefined;
@@ -200,7 +197,7 @@ function resolveShardOption(shard?: string): ConfigCLIOverrides['shard'] {
return { current, total };
}
function resolveShardWeightsOption(): ConfigCLIOverrides['shardWeights'] {
function resolveShardWeightsOption(): ipc.ConfigCLIOverrides['shardWeights'] {
const shardWeights = process.env.PWTEST_SHARD_WEIGHTS;
if (!shardWeights)
return undefined;
+2 -2
View File
@@ -3,8 +3,8 @@
@utils/**
../util.ts
../transform
../isomorphic/teleReceiver.ts
../isomorphic
../globals.ts
../package.ts
[testType.ts]
../matchers/expect.ts
+1 -9
View File
@@ -19,7 +19,7 @@ import os from 'os';
import path from 'path';
import { packageJSON } from '../package';
import { getPackageJsonPath, mergeObjects } from '../util';
import { getPackageJsonPath, mergeObjects, takeFirst } from '../util';
import type { Config, Fixtures, Metadata, Project, ReporterDescription } from '../../types/test';
import type { TestRunnerPluginRegistration } from '../plugins';
@@ -216,14 +216,6 @@ export class FullProjectInternal {
}
}
export function takeFirst<T>(...args: (T | undefined)[]): T {
for (const arg of args) {
if (arg !== undefined)
return arg;
}
return undefined as any as T;
}
function pathResolve(baseDir: string, relative: string | undefined): string | undefined {
if (!relative)
return undefined;
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * as babel from '../transform/babelBundle';
export * as cc from '../transform/compilationCache';
export * as config from './config';
export * as configLoader from './configLoader';
export * as esm from './esmLoaderHost';
export * as fixtures from './fixtures';
export * as ipc from './ipc';
export * as poolBuilder from './poolBuilder';
export * as processRunner from './process';
export * as suiteUtils from './suiteUtils';
export * as test from './test';
export * as testLoader from './testLoader';
export * as testType from './testType';
export * as transform from '../transform/transform';
export { FullConfigInternal, builtInReporters } from './config';
export { ProcessRunner, startProcessRunner } from './process';
export { defineConfig } from './configLoader';
export { mergeTests } from './testType';
export type { ConfigLocation } from './config';
+1 -1
View File
@@ -18,7 +18,7 @@ import path from 'path';
import util from 'util';
import * as esmLoaderHost from './esmLoaderHost';
import { isWorkerProcess, setCurrentlyLoadingFileSuite } from './globals';
import { isWorkerProcess, setCurrentlyLoadingFileSuite } from '../globals';
import { Suite } from './test';
import { startCollectingFileDeps, stopCollectingFileDeps } from '../transform/compilationCache';
import { requireOrImport } from '../transform/transform';
+1 -1
View File
@@ -21,7 +21,7 @@ import { raceAgainstDeadline } from '@isomorphic/timeoutRunner';
import { getPackageManagerExecCommand } from '@utils/env';
import { currentZone } from '@utils/zones';
import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from './globals';
import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from '../globals';
import { Suite, TestCase } from './test';
import { expect } from '../matchers/expect';
import { wrapFunctionWithLocation } from '../transform/transform';
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import type { Suite } from './test';
import type { TestInfoImpl } from '../worker/testInfo';
import type { Suite } from './common/test';
import type { TestInfoImpl } from './worker/testInfo';
let currentTestInfoValue: TestInfoImpl | null = null;
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
+8 -10
View File
@@ -27,13 +27,12 @@ import { debugMode } from '@utils/debug';
import { setBoxedStackPrefixes } from '@utils/nodePlatform';
import { currentZone } from '@utils/zones';
import { buildErrorContext } from './errorContext';
import { currentTestInfo } from './common/globals';
import { config, testType } from './common';
import * as globals from './globals';
import { packageRoot } from './package';
import { rootTestType } from './common/testType';
import { createCustomMessageHandler, runDaemonForContext } from './mcp/test/browserBackend';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import type { ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import type { Playwright as PlaywrightImpl } from '../../playwright-core/src/client/playwright';
@@ -45,7 +44,7 @@ import type { Page as PageImpl } from '../../playwright-core/src/client/page';
import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing } from 'playwright-core';
export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
export const _baseTest: TestType<{}, {}> = testType.rootTestType.test;
setBoxedStackPrefixes([packageRoot]);
@@ -71,7 +70,7 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
playwright: PlaywrightImpl;
_browserOptions: LaunchOptions;
_optionContextReuseMode: ContextReuseMode,
_optionContextReuseMode: config.ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
_reuseContext: boolean,
};
@@ -269,7 +268,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
const pausedContexts = new Set<BrowserContextImpl>();
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (data, channel) => {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return;
@@ -315,7 +314,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
},
onWillPause: ({ keepTestTimeout }) => {
if (!keepTestTimeout)
currentTestInfo()?._setIgnoreTimeouts(true);
globals.currentTestInfo()?._setIgnoreTimeouts(true);
},
runBeforeCreateBrowserContext: async (options: BrowserContextOptions) => {
for (const [key, value] of Object.entries(_combinedContextOptions)) {
@@ -342,7 +341,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
});
await artifactsRecorder.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
},
@@ -876,6 +875,5 @@ function tracing() {
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export { defineConfig } from './common/configLoader';
export { mergeTests } from './common/testType';
export { defineConfig, mergeTests } from './common';
export { mergeExpects } from './matchers/expect';
+9 -16
View File
@@ -14,45 +14,38 @@
* limitations under the License.
*/
import { deserializeConfig } from '../common/configLoader';
import { incorporateCompilationCache } from '../common/esmLoaderHost';
import { PoolBuilder } from '../common/poolBuilder';
import { ProcessRunner } from '../common/process';
import { loadTestFile } from '../common/testLoader';
import { serializeCompilationCache } from '../transform/compilationCache';
import { cc, configLoader, esm, FullConfigInternal, ipc, poolBuilder, ProcessRunner, testLoader } from '../common';
import type { TestError } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { SerializedConfig } from '../common/ipc';
export class LoaderMain extends ProcessRunner {
private _serializedConfig: SerializedConfig;
private _serializedConfig: ipc.SerializedConfig;
private _configPromise: Promise<FullConfigInternal> | undefined;
private _poolBuilder = PoolBuilder.createForLoader();
private _poolBuilder = poolBuilder.PoolBuilder.createForLoader();
constructor(serializedConfig: SerializedConfig) {
constructor(serializedConfig: ipc.SerializedConfig) {
super();
this._serializedConfig = serializedConfig;
}
private _config(): Promise<FullConfigInternal> {
if (!this._configPromise)
this._configPromise = deserializeConfig(this._serializedConfig);
this._configPromise = configLoader.deserializeConfig(this._serializedConfig);
return this._configPromise;
}
async loadTestFile(params: { file: string }) {
const testErrors: TestError[] = [];
const config = await this._config();
const fileSuite = await loadTestFile(params.file, config, testErrors);
const fileSuite = await testLoader.loadTestFile(params.file, config, testErrors);
this._poolBuilder.buildPools(fileSuite);
return { fileSuite: fileSuite._deepSerialize(), testErrors };
}
async getCompilationCacheFromLoader() {
await incorporateCompilationCache();
return serializeCompilationCache();
await esm.incorporateCompilationCache();
return cc.serializeCompilationCache();
}
}
export const create = (config: SerializedConfig) => new LoaderMain(config);
export const create = (config: ipc.SerializedConfig) => new LoaderMain(config);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { startProcessRunner } from '../common/process';
import { startProcessRunner } from '../common';
import { create } from './loaderMain';
startProcessRunner(create);
+3 -5
View File
@@ -1,9 +1,7 @@
[*]
@isomorphic/**
@utils/**
../common/
../mcp/test/browserBackend.ts
../util.ts
../worker/testInfo.ts
node_modules/colors/safe
../globals.ts
../package.ts
../util.ts
node_modules/colors/safe
+5 -6
View File
@@ -59,13 +59,12 @@ import { toHaveScreenshot, toMatchSnapshot } from './toMatchSnapshot';
import {
expect as expectLibrary,
} from './expectBundle';
import { currentTestInfo } from '../common/globals';
import * as globals from '../globals';
import { filteredStackTrace } from '../util';
import { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherStateInternal } from './matchers';
import type { Expect } from '../../types/test';
import type { TestStepInfoImpl } from '../worker/testInfo';
import type { TestInfoImpl, TestStepInfoImpl } from '../worker/testInfo';
type ExpectMessage = string | { message?: string };
@@ -296,7 +295,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
}
return (...args: any[]) => {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
setMatcherCallContext({ expectInfo: this._info, testInfo });
if (!testInfo)
return matcher.call(target, ...args);
@@ -364,13 +363,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
}
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
const poll = info.poll!;
const timeout = poll.timeout ?? info.timeout ?? testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
const result = await pollAgainstDeadline<Error|undefined>(async () => {
if (testInfo && currentTestInfo() !== testInfo)
if (testInfo && globals.currentTestInfo() !== testInfo)
return { continuePolling: false, result: undefined };
const innerInfo: ExpectMetaInfo = {
+4 -4
View File
@@ -30,9 +30,9 @@ import { toEqual } from './toEqual';
import { toHaveURLWithPredicate } from './toHaveURL';
import { toMatchText } from './toMatchText';
import { toHaveScreenshotStepTitle } from './toMatchSnapshot';
import { takeFirst } from '../common/config';
import { currentTestInfo } from '../common/globals';
import * as globals from '../globals';
import { MatcherResult } from './matcherHint';
import { takeFirst } from '../util';
import type { ExpectMatcherState } from '../../types/test';
import type { TestStepInfoImpl } from '../worker/testInfo';
@@ -483,13 +483,13 @@ export async function toPass(
timeout?: number,
} = {},
) {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
const timeout = takeFirst(options.timeout, testInfo?._projectInternal.expect?.toPass?.timeout, 0);
const intervals = takeFirst(options.intervals, testInfo?._projectInternal.expect?.toPass?.intervals, [100, 250, 500, 1000]);
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
const result = await pollAgainstDeadline<Error|undefined>(async () => {
if (testInfo && currentTestInfo() !== testInfo)
if (testInfo && globals.currentTestInfo() !== testInfo)
return { continuePolling: false, result: undefined };
try {
await callback();
@@ -22,7 +22,7 @@ import { escapeTemplateString, isString } from '@isomorphic/stringUtils';
import { formatMatcherMessage, printReceivedStringContainExpectedSubstring } from '@utils/expectUtils';
import { expectTypes, fileExistsAsync } from '../util';
import { currentTestInfo } from '../common/globals';
import * as globals from '../globals';
import type { MatcherResult } from './matcherHint';
import type { ExpectMatcherStateInternal, FrameEx, LocatorEx } from './matchers';
@@ -46,7 +46,7 @@ export async function toMatchAriaSnapshot(
expectTypes(receiver, ['Page', 'Locator'], matcherName);
const locator = (receiver as any)._apiName === 'Page' ? undefined : receiver as LocatorEx;
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
if (!testInfo)
throw new Error(`${matcherName}() must be called during the test`);
@@ -22,13 +22,12 @@ import { getMimeTypeForPath } from '@isomorphic/mimeType';
import { isString } from '@isomorphic/stringUtils';
import { compareBuffersOrStrings, getComparator } from '@utils/comparators';
import { callLogText, formatMatcherMessage } from '@utils/expectUtils';
import { addSuffixToFilePath, expectTypes } from '../util';
import { currentTestInfo } from '../common/globals';
import * as globals from '../globals';
import type { config } from '../common';
import type { MatcherResult } from './matcherHint';
import type { ExpectMatcherStateInternal } from './matchers';
import type { FullProjectInternal } from '../common/config';
import type { TestInfoImpl, TestStepInfoImpl } from '../worker/testInfo';
import type { Locator, Page } from 'playwright-core';
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
@@ -38,7 +37,7 @@ type NameOrSegments = string | string[];
type ImageMatcherResult = MatcherResult<string, string> & { diff?: string };
type ToHaveScreenshotConfigOptions = NonNullable<NonNullable<FullProjectInternal['expect']>['toHaveScreenshot']> & {
type ToHaveScreenshotConfigOptions = NonNullable<NonNullable<config.FullProjectInternal['expect']>['toHaveScreenshot']> & {
_comparator?: string;
};
@@ -256,7 +255,7 @@ export function toMatchSnapshot(
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
optOptions: ImageComparatorOptions = {}
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
if (!testInfo)
throw new Error(`toMatchSnapshot() must be called during the test`);
if (received instanceof Promise)
@@ -327,7 +326,7 @@ export async function toHaveScreenshot(
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
optOptions: ToHaveScreenshotOptions = {}
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
const testInfo = currentTestInfo();
const testInfo = globals.currentTestInfo();
if (!testInfo)
throw new Error(`toHaveScreenshot() must be called during the test`);
+7 -7
View File
@@ -21,28 +21,28 @@ import { mkdirIfNeeded } from '@utils/fileUtils';
import { collectFilesForProject, findTopLevelProjects } from '../../runner/projectUtils';
import type { FullConfigInternal, FullProjectInternal } from '../../common/config';
import type { config, FullConfigInternal } from '../../common';
export function seedProject(config: FullConfigInternal, projectName?: string) {
export function seedProject(fullConfig: FullConfigInternal, projectName?: string) {
if (!projectName)
return findTopLevelProjects(config)[0];
const project = config.projects.find(p => p.project.name === projectName);
return findTopLevelProjects(fullConfig)[0];
const project = fullConfig.projects.find(p => p.project.name === projectName);
if (!project)
throw new Error(`Project ${projectName} not found`);
return project;
}
export async function findSeedFile(project: FullProjectInternal) {
export async function findSeedFile(project: config.FullProjectInternal) {
const files = await collectFilesForProject(project);
return files.find(file => path.basename(file).includes('seed'));
}
export function defaultSeedFile(project: FullProjectInternal) {
export function defaultSeedFile(project: config.FullProjectInternal) {
const testDir = project.project.testDir;
return path.resolve(testDir, 'seed.spec.ts');
}
export async function ensureSeedFile(project: FullProjectInternal) {
export async function ensureSeedFile(project: config.FullProjectInternal) {
const seedFile = await findSeedFile(project);
if (seedFile)
return seedFile;
@@ -31,11 +31,11 @@ import { StringWriteStream } from './streams';
import { fileExistsAsync } from '../../util';
import { TestRunner, TestRunnerEvent } from '../../runner/testRunner';
import { ensureSeedFile, seedProject } from './seed';
import { resolveConfigLocation } from '../../common/configLoader';
import { configLoader } from '../../common';
import type { ConfigLocation } from '../../common';
import type { TerminalScreen } from '../../reporters/base';
import type { FullResultStatus, RunTestsParams } from '../../runner/testRunner';
import type { ConfigLocation } from '../../common/config';
import type { BrowserMCPRequest, BrowserMCPResponse } from './browserBackend';
export type SeedFile = {
@@ -100,7 +100,7 @@ export class TestContext {
constructor(clientInfo: tools.ClientInfo, configPath: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) {
this._clientInfo = clientInfo;
this._configLocation = resolveConfigLocation(configPath || clientInfo.cwd);
this._configLocation = configLoader.resolveConfigLocation(configPath || clientInfo.cwd);
this.rootPath = clientInfo.cwd || this._configLocation.configDir;
if (options?.headless !== undefined)
@@ -21,7 +21,7 @@ import { spawnAsync } from '@utils/spawnAsync';
import type { TestRunnerPlugin } from './';
import type { FullConfig } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { FullConfigInternal } from '../common';
import type { GitCommitInfo, CIInfo, MetadataWithCommitInfo } from '../isomorphic/types';
const GIT_OPERATIONS_TIMEOUT_MS = 3000;
@@ -26,7 +26,7 @@ import { launchProcess } from '@utils/processLauncher';
import type { TestRunnerPlugin } from '.';
import type { FullConfig } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { FullConfigInternal } from '../common';
import type { ReporterV2 } from '../reporters/reporterV2';
export type WebServerPluginOptions = {
+9 -10
View File
@@ -21,11 +21,10 @@ import 'playwright-core/lib/bootstrap';
import { libCli, tools } from 'playwright-core/lib/coreBundle';
import { program } from 'commander';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
import { builtInReporters, config, configLoader } from './common';
import { runTests, clearCache, runTestServerAction } from './cli/testActions';
import { showReport, mergeReports } from './cli/reportActions';
import { TestServerBackend, testServerBackendTools } from './mcp/test/testBackend';
import { loadConfigFromFile } from './common/configLoader';
import { ClaudeGenerator, OpencodeGenerator, VSCodeGenerator, CopilotGenerator } from './agents/generateAgents';
import { packageJSON } from './package';
@@ -119,7 +118,7 @@ function addMergeReportsCommand(program: Command) {
}
});
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`);
command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${config.defaultReporter}")`);
command.addHelpText('afterAll', `
Arguments [dir]:
Directory containing blob reports.
@@ -160,15 +159,15 @@ function addInitAgentsCommand(program: Command) {
command.option('--project <project>', 'Project to use for seed test');
command.option('--prompts', 'Whether to include prompts in the agent initialization');
command.action(async opts => {
const config = await loadConfigFromFile(opts.config);
const loadedConfig = await configLoader.loadConfigFromFile(opts.config);
if (opts.loop === 'opencode') {
await OpencodeGenerator.init(config, opts.project, opts.prompts);
await OpencodeGenerator.init(loadedConfig, opts.project, opts.prompts);
} else if (opts.loop === 'vscode-legacy') {
await VSCodeGenerator.init(config, opts.project);
await VSCodeGenerator.init(loadedConfig, opts.project);
} else if (opts.loop === 'claude') {
await ClaudeGenerator.init(config, opts.project, opts.prompts);
await ClaudeGenerator.init(loadedConfig, opts.project, opts.prompts);
} else {
await CopilotGenerator.init(config, opts.project, opts.prompts);
await CopilotGenerator.init(loadedConfig, opts.project, opts.prompts);
return;
}
});
@@ -200,13 +199,13 @@ const testOptions: [string, { description: string, choices?: string[], preset?:
['--project <project-name...>', { description: `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)` }],
['--quiet', { description: `Suppress stdio` }],
['--repeat-each <N>', { description: `Run each test N times (default: 1)` }],
['--reporter <reporter>', { description: `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")` }],
['--reporter <reporter>', { description: `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${config.defaultReporter}")` }],
['--retries <retries>', { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
['--run-agents <mode>', { description: `Run agents to generate the code for page.perform`, choices: ['missing', 'all', 'none'], preset: 'none' }],
['--shard <shard>', { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }],
['--test-list <file>', { description: `Path to a file containing a list of tests to run. See https://playwright.dev/docs/test-cli for more details.` }],
['--test-list-invert <file>', { description: `Path to a file containing a list of tests to skip. See https://playwright.dev/docs/test-cli for more details.` }],
['--timeout <timeout>', { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})` }],
['--timeout <timeout>', { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${config.defaultTimeout})` }],
['--trace <mode>', { description: `Force tracing mode`, choices: kTraceModes as string[] }],
['--tsconfig <path>', { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
['--ui', { description: `Run tests in interactive UI mode` }],
+3 -3
View File
@@ -31,7 +31,7 @@ import { HttpServer } from '@utils/httpServer';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { CommonReporterOptions, formatError, formatResultFailure, internalScreen } from './base';
import { codeFrameColumns } from '../transform/babelBundle';
import { babel } from '../common';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';
import type { ReportConfigureParams, ReportEndParams, ReporterV2 } from './reporterV2';
@@ -725,7 +725,7 @@ function createSnippets(stepsInFile: MultiMap<string, TestStep>) {
continue;
}
const lines = source.split('\n').length;
const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
const highlighted = babel.codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
const highlightedLines = highlighted.split('\n');
const lineWithArrow = highlightedLines[highlightedLines.length - 1];
for (const step of stepsInFile.get(file)) {
@@ -752,7 +752,7 @@ function createErrorCodeframe(message: string, location: Location) {
return;
}
return codeFrameColumns(
return babel.codeFrameColumns(
source,
{
start: {
@@ -20,8 +20,7 @@ import { monotonicTime } from '@isomorphic/time';
import { internalScreen, prepareErrorStack, relativeFilePath } from './base';
import { Multiplexer } from './multiplexer';
import { Suite } from '../common/test';
import { codeFrameColumns } from '../transform/babelBundle';
import { babel, test as testNs } from '../common';
import { wrapReporterAsV2 } from './reporterV2';
import type { AnyReporter, ReporterV2 } from './reporterV2';
@@ -50,7 +49,7 @@ export class InternalReporter implements ReporterV2 {
this._reporter.onConfigure?.(config);
}
onBegin(suite: Suite) {
onBegin(suite: testNs.Suite) {
this._didBegin = true;
this._reporter.onBegin?.(suite);
}
@@ -80,7 +79,7 @@ export class InternalReporter implements ReporterV2 {
async onEnd(result: { status: FullResult['status'] }) {
if (!this._didBegin) {
// onBegin was not reported, emit it.
this.onBegin(new Suite('', 'root'));
this.onBegin(new testNs.Suite('', 'root'));
}
return await this._reporter.onEnd?.({
...result,
@@ -135,7 +134,7 @@ export function addLocationAndSnippetToError(config: FullConfig, error: TestErro
try {
const tokens = [];
const source = fs.readFileSync(location.file, 'utf8');
const codeFrame = codeFrameColumns(source, { start: location }, { highlightCode: true });
const codeFrame = babel.codeFrameColumns(source, { start: location }, { highlightCode: true });
// Convert /var/folders to /private/var/folders on Mac.
if (!file || fs.realpathSync(file) !== location.file) {
tokens.push(internalScreen.colors.gray(` at `) + `${relativeFilePath(internalScreen, config, location.file)}:${location.line}`);
+3 -3
View File
@@ -21,7 +21,7 @@ import { MultiMap } from '@isomorphic/multimap';
import { toPosixPath } from '@utils/fileUtils';
import { formatError, nonTerminalScreen, prepareErrorStack, resolveOutputFile, CommonReporterOptions } from './base';
import { getProjectId } from '../common/config';
import { config } from '../common';
import type { ReporterV2 } from './reporterV2';
import type { JsonReporterOptions } from '../../types/test';
@@ -72,7 +72,7 @@ class JSONReporter implements ReporterV2 {
repeatEach: project.repeatEach,
retries: project.retries,
metadata: project.metadata,
id: getProjectId(project),
id: config.getProjectId(project),
name: project.name,
testDir: toPosixPath(project.testDir),
testIgnore: serializePatterns(project.testIgnore),
@@ -100,7 +100,7 @@ class JSONReporter implements ReporterV2 {
private _mergeSuites(suites: Suite[]): JSONReportSuite[] {
const fileSuites = new MultiMap<string, JSONReportSuite>();
for (const projectSuite of suites) {
const projectId = getProjectId(projectSuite.project()!);
const projectId = config.getProjectId(projectSuite.project()!);
const projectName = projectSuite.project()!.name;
for (const fileSuite of projectSuite.suites) {
const file = fileSuite.location!.file;
@@ -19,7 +19,7 @@ import path from 'path';
import { formatError, terminalScreen } from './base';
import type { FullConfig, TestError } from '../../types/testReporter';
import type { Suite } from '../common/test';
import type { test } from '../common';
import type { TerminalScreen } from './base';
import type { ReporterV2 } from './reporterV2';
@@ -41,7 +41,7 @@ class ListModeReporter implements ReporterV2 {
this.config = config;
}
onBegin(suite: Suite): void {
onBegin(suite: test.Suite): void {
this._writeLine(`Listing tests:`);
const tests = suite.allTests();
const files = new Set<string>();
+1 -1
View File
@@ -28,7 +28,7 @@ import { relativeFilePath } from '../util';
import type { ReporterDescription, TestAnnotation } from '../../types/test';
import type { TestError } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { FullConfigInternal } from '../common';
import type { BlobReportMetadata, JsonAttachment, JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonOnConfigureEvent, JsonOnEndEvent, JsonOnProjectEvent, JsonProject, JsonSuite, JsonTestCase } from '../isomorphic/teleReceiver';
import type * as blobV1 from './versions/blobV1';
@@ -16,7 +16,7 @@
import type { ReportConfigureParams, ReportEndParams, ReporterV2 } from './reporterV2';
import type { FullConfig, FullResult, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
import type { Suite } from '../common/test';
import type { test } from '../common';
export class Multiplexer implements ReporterV2 {
private _reporters: ReporterV2[];
@@ -34,7 +34,7 @@ export class Multiplexer implements ReporterV2 {
wrap(() => reporter.onConfigure?.(config));
}
onBegin(suite: Suite) {
onBegin(suite: test.Suite) {
for (const reporter of this._reporters)
wrap(() => reporter.onBegin?.(suite));
}
+30 -34
View File
@@ -20,7 +20,7 @@ import { eventsHelper } from '@utils/eventsHelper';
import { addSuggestedRebaseline } from './rebase';
import { WorkerHost } from './workerHost';
import { serializeConfig } from '../common/ipc';
import { FullConfigInternal, ipc, test as testNs } from '../common';
import { addLocationAndSnippetToError } from '../reporters/internalReporter';
import { serializeError } from '../util';
@@ -29,10 +29,6 @@ import type { FailureTracker } from './failureTracker';
import type { ProcessExitData } from './processHost';
import type { TestGroup } from './testGroups';
import type { TestError, TestResult, TestStep } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload, TestPausedPayload } from '../common/ipc';
import type { Suite } from '../common/test';
import type { TestCase } from '../common/test';
import type { ReporterV2 } from '../reporters/reporterV2';
export type EnvByProjectId = Map<string, Record<string, string | undefined>>;
@@ -136,7 +132,7 @@ export class Dispatcher {
// 2. Start the worker if it is down.
let startError;
if (!worker) {
worker = this._createWorker(job, index, serializeConfig(this._config, true));
worker = this._createWorker(job, index, ipc.serializeConfig(this._config, true));
this._workerSlots[index].worker = worker;
worker.on('exit', () => this._workerSlots[index].worker = undefined);
startError = await worker.start();
@@ -216,7 +212,7 @@ export class Dispatcher {
await this._finished;
}
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedConfig) {
_createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: ipc.SerializedConfig) {
const projectConfig = this._config.projects.find(p => p.id === testGroup.projectId)!;
const outputDir = projectConfig.project.outputDir;
const worker = new WorkerHost(testGroup, {
@@ -227,7 +223,7 @@ export class Dispatcher {
pauseOnError: this._failureTracker.pauseOnError(),
pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig),
});
const handleOutput = (params: TestOutputPayload) => {
const handleOutput = (params: ipc.TestOutputPayload) => {
const chunk = chunkFromParams(params);
if (worker.didFail()) {
// Note: we keep reading stdio from workers that are currently stopping after failure,
@@ -240,17 +236,17 @@ export class Dispatcher {
return { chunk };
return { chunk, test: currentlyRunning.test, result: currentlyRunning.result };
};
worker.on('stdOut', (params: TestOutputPayload) => {
worker.on('stdOut', (params: ipc.TestOutputPayload) => {
const { chunk, test, result } = handleOutput(params);
result?.stdout.push(chunk);
this._reporter.onStdOut?.(chunk, test, result);
});
worker.on('stdErr', (params: TestOutputPayload) => {
worker.on('stdErr', (params: ipc.TestOutputPayload) => {
const { chunk, test, result } = handleOutput(params);
result?.stderr.push(chunk);
this._reporter.onStdErr?.(chunk, test, result);
});
worker.on('teardownErrors', (params: TeardownErrorsPayload) => {
worker.on('teardownErrors', (params: ipc.TeardownErrorsPayload) => {
this._failureTracker.onWorkerError();
for (const error of params.fatalErrors)
this._reporter.onError?.(error);
@@ -284,13 +280,13 @@ class JobDispatcher {
private _failureTracker: FailureTracker;
private _stopCallback: () => void;
private _listeners: RegisteredListener[] = [];
private _failedTests = new Set<TestCase>();
private _failedWithNonRetriableError = new Set<TestCase|Suite>();
private _remainingByTestId = new Map<string, TestCase>();
private _dataByTestId = new Map<string, { test: TestCase, result: TestResult, steps: Map<string, TestStep> }>();
private _failedTests = new Set<testNs.TestCase>();
private _failedWithNonRetriableError = new Set<testNs.TestCase|testNs.Suite>();
private _remainingByTestId = new Map<string, testNs.TestCase>();
private _dataByTestId = new Map<string, { test: testNs.TestCase, result: TestResult, steps: Map<string, TestStep> }>();
private _parallelIndex = 0;
private _workerIndex = 0;
private _currentlyRunning: { test: TestCase, result: TestResult } | undefined;
private _currentlyRunning: { test: testNs.TestCase, result: TestResult } | undefined;
constructor(job: TestGroup, config: FullConfigInternal, reporter: ReporterV2, failureTracker: FailureTracker, stopCallback: () => void) {
this.job = job;
@@ -301,7 +297,7 @@ class JobDispatcher {
this._remainingByTestId = new Map(this.job.tests.map(e => [e.id, e]));
}
private _onTestBegin(params: TestBeginPayload) {
private _onTestBegin(params: ipc.TestBeginPayload) {
const test = this._remainingByTestId.get(params.testId);
if (!test) {
// TODO: this should never be the case, report an internal error?
@@ -316,7 +312,7 @@ class JobDispatcher {
this._currentlyRunning = { test, result };
}
private _onTestEnd(params: TestEndPayload) {
private _onTestEnd(params: ipc.TestEndPayload) {
if (this._failureTracker.hasReachedMaxFailures()) {
// Do not show more than one error to avoid confusion, but report
// as interrupted to indicate that we did actually start the test.
@@ -348,15 +344,15 @@ class JobDispatcher {
this._currentlyRunning = undefined;
}
private _addNonretriableTestAndSerialModeParents(test: TestCase) {
private _addNonretriableTestAndSerialModeParents(test: testNs.TestCase) {
this._failedWithNonRetriableError.add(test);
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) {
for (let parent: testNs.Suite | undefined = test.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial')
this._failedWithNonRetriableError.add(parent);
}
}
private _onStepBegin(params: StepBeginPayload) {
private _onStepBegin(params: ipc.StepBeginPayload) {
const data = this._dataByTestId.get(params.testId);
if (!data) {
// The test has finished, but steps are still coming. Just ignore them.
@@ -384,7 +380,7 @@ class JobDispatcher {
this._reporter.onStepBegin?.(test, result, step);
}
private _onStepEnd(params: StepEndPayload) {
private _onStepEnd(params: ipc.StepEndPayload) {
const data = this._dataByTestId.get(params.testId);
if (!data) {
// The test has finished, but steps are still coming. Just ignore them.
@@ -406,7 +402,7 @@ class JobDispatcher {
this._reporter.onStepEnd?.(test, result, step);
}
private _onAttach(params: AttachmentPayload) {
private _onAttach(params: ipc.AttachmentPayload) {
const data = this._dataByTestId.get(params.testId)!;
if (!data) {
// The test has finished, but attachments are still coming. Just ignore them.
@@ -428,7 +424,7 @@ class JobDispatcher {
}
}
private _failTestWithErrors(test: TestCase, errors: TestError[]) {
private _failTestWithErrors(test: testNs.TestCase, errors: TestError[]) {
const runData = this._dataByTestId.get(test.id);
// There might be a single test that has started but has not finished yet.
let result: TestResult;
@@ -464,7 +460,7 @@ class JobDispatcher {
}
}
private _onDone(params: DonePayload & { unexpectedExitError?: TestError }) {
private _onDone(params: ipc.DonePayload & { unexpectedExitError?: TestError }) {
// We won't file remaining if:
// - there are no remaining
// - we are here not because something failed
@@ -502,16 +498,16 @@ class JobDispatcher {
this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError]);
}
const retryCandidates = new Set<TestCase>();
const serialSuitesWithFailures = new Set<Suite>();
const retryCandidates = new Set<testNs.TestCase>();
const serialSuitesWithFailures = new Set<testNs.Suite>();
for (const failedTest of this._failedTests) {
if (this._failedWithNonRetriableError.has(failedTest))
continue;
retryCandidates.add(failedTest);
let outermostSerialSuite: Suite | undefined;
for (let parent: Suite | undefined = failedTest.parent; parent; parent = parent.parent) {
let outermostSerialSuite: testNs.Suite | undefined;
for (let parent: testNs.Suite | undefined = failedTest.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial')
outermostSerialSuite = parent;
}
@@ -522,7 +518,7 @@ class JobDispatcher {
// If we have failed tests that belong to a serial suite,
// we should skip all future tests from the same serial suite.
const testsBelongingToSomeSerialSuiteWithFailures = [...this._remainingByTestId.values()].filter(test => {
let parent: Suite | undefined = test.parent;
let parent: testNs.Suite | undefined = test.parent;
while (parent && !serialSuitesWithFailures.has(parent))
parent = parent.parent;
return !!parent;
@@ -563,7 +559,7 @@ class JobDispatcher {
this._parallelIndex = worker.parallelIndex;
this._workerIndex = worker.workerIndex;
const runPayload: RunPayload = {
const runPayload: ipc.RunPayload = {
file: this.job.requireFile,
entries: this.job.tests.map(test => {
return { testId: test.id, retry: test.results.length };
@@ -583,7 +579,7 @@ class JobDispatcher {
];
}
private _onTestPaused(worker: WorkerHost, params: TestPausedPayload) {
private _onTestPaused(worker: WorkerHost, params: ipc.TestPausedPayload) {
const data = this._dataByTestId.get(params.testId);
if (!data)
return;
@@ -642,7 +638,7 @@ class JobDispatcher {
return this._currentlyRunning;
}
private _reportTestEnd(test: TestCase, result: TestResult) {
private _reportTestEnd(test: testNs.TestCase, result: TestResult) {
this._reporter.onTestEnd?.(test, result);
const hadMaxFailures = this._failureTracker.hasReachedMaxFailures();
this._failureTracker.onTestEnd(test, result);
@@ -654,7 +650,7 @@ class JobDispatcher {
}
}
function chunkFromParams(params: TestOutputPayload): string | Buffer {
function chunkFromParams(params: ipc.TestOutputPayload): string | Buffer {
if (typeof params.text === 'string')
return params.text;
return Buffer.from(params.buffer!, 'base64');
@@ -15,8 +15,7 @@
*/
import type { TestResult, TestError } from '../../types/testReporter';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { Suite, TestCase } from '../common/test';
import type { config as commonConfig, FullConfigInternal, test } from '../common';
export type TestPausedParams = {
errors: TestError[];
@@ -27,8 +26,8 @@ export class FailureTracker {
private _config: FullConfigInternal;
private _failureCount = 0;
private _hasWorkerErrors = false;
private _rootSuite: Suite | undefined;
private _topLevelProjects: FullProjectInternal[] = [];
private _rootSuite: test.Suite | undefined;
private _topLevelProjects: commonConfig.FullProjectInternal[] = [];
private _pauseOnError: boolean;
private _pauseAtEnd: boolean;
onTestPaused?: (params: TestPausedParams) => void;
@@ -39,12 +38,12 @@ export class FailureTracker {
this._pauseAtEnd = !!options?.pauseAtEnd;
}
onRootSuite(rootSuite: Suite, topLevelProjects: FullProjectInternal[]) {
onRootSuite(rootSuite: test.Suite, topLevelProjects: commonConfig.FullProjectInternal[]) {
this._rootSuite = rootSuite;
this._topLevelProjects = topLevelProjects;
}
onTestEnd(test: TestCase, result: TestResult) {
onTestEnd(test: test.TestCase, result: TestResult) {
// Test is considered failing after the last retry.
if (test.outcome() === 'unexpected' && test.results.length > test.retries)
++this._failureCount;
@@ -58,7 +57,7 @@ export class FailureTracker {
return this._pauseOnError;
}
pauseAtEnd(inProject: FullProjectInternal) {
pauseAtEnd(inProject: commonConfig.FullProjectInternal) {
return this._topLevelProjects.includes(inProject) && this._pauseAtEnd;
}
+1 -1
View File
@@ -20,7 +20,7 @@ import path from 'path';
import { filterProjects } from './projectUtils';
import type { FullResult, Suite } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { FullConfigInternal } from '../common';
import type { ReporterV2 } from '../reporters/reporterV2';
type LastRunInfo = {
+30 -36
View File
@@ -24,18 +24,12 @@ import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost';
import { createTitleMatcher, errorWithFile, parseLocationArg } from '../util';
import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
import { createTestGroups, filterForShard } from './testGroups';
import { applyRepeatEachIndex, bindFileSuiteToProject, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { Suite } from '../common/test';
import { dependenciesForTestFile } from '../transform/compilationCache';
import { requireOrImport } from '../transform/transform';
import { cc, config as commonConfig, FullConfigInternal, suiteUtils, test as testNs, transform } from '../common';
import type { RawSourceMap } from 'playwright-core/lib/utilsBundle';
import type { TestRun } from './tasks';
import type { TestGroup } from './testGroups';
import type { FullConfig, Reporter, TestError } from '../../types/testReporter';
import type { FullProjectInternal } from '../common/config';
import type { FullConfigInternal } from '../common/config';
import type { TestCase } from '../common/test';
import type { Matcher, TestCaseFilter } from '../util';
@@ -45,7 +39,7 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
const sourceMapCache = new Map();
// First collect all files for the projects in the command line, don't apply any file filters.
const allFilesForProject = new Map<FullProjectInternal, string[]>();
const allFilesForProject = new Map<commonConfig.FullProjectInternal, string[]>();
const filteredProjects = filterProjects(config.projects, config.cliProjectFilter);
for (const project of filteredProjects) {
const files = await collectFilesForProject(project, fsCache);
@@ -53,7 +47,7 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
}
// Filter files based on the file filters, eliminate the empty projects.
const filesToRunByProject = new Map<FullProjectInternal, string[]>();
const filesToRunByProject = new Map<commonConfig.FullProjectInternal, string[]>();
for (const [project, files] of allFilesForProject) {
const matchedFiles = files.filter(file => {
if (!config.loadFileFilters.length) {
@@ -92,7 +86,7 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' |
files.forEach(file => allTestFiles.add(file));
// Load test files.
const fileSuiteByFile = new Map<string, Suite>();
const fileSuiteByFile = new Map<string, testNs.Suite>();
const loaderHost = mode === 'out-of-process' ? new OutOfProcessLoaderHost(config) : new InProcessLoaderHost(config);
if (await loaderHost.start(errors)) {
for (const file of allTestFiles) {
@@ -106,7 +100,7 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' |
// Check that no test file imports another test file.
// Loader must be stopped first, since it populates the dependency tree.
for (const file of allTestFiles) {
for (const dependency of dependenciesForTestFile(file)) {
for (const dependency of cc.dependenciesForTestFile(file)) {
if (allTestFiles.has(dependency)) {
const importer = path.relative(config.config.rootDir, file);
const importee = path.relative(config.config.rootDir, dependency);
@@ -120,17 +114,17 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' |
// Collect file suites for each project.
for (const [project, files] of testRun.projectFiles) {
const suites = files.map(file => fileSuiteByFile.get(file)).filter(Boolean) as Suite[];
const suites = files.map(file => fileSuiteByFile.get(file)).filter(Boolean) as testNs.Suite[];
testRun.projectSuites.set(project, suites);
}
}
export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise<{ rootSuite: Suite, topLevelProjects: FullProjectInternal[] }> {
export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise<{ rootSuite: testNs.Suite, topLevelProjects: commonConfig.FullProjectInternal[] }> {
const config = testRun.config;
// Create root suite, where each child will be a project suite with cloned file suites inside it.
const rootSuite = new Suite('', 'root');
const projectSuites = new Map<FullProjectInternal, Suite>();
const filteredProjectSuites = new Map<FullProjectInternal, Suite>();
const rootSuite = new testNs.Suite('', 'root');
const projectSuites = new Map<commonConfig.FullProjectInternal, testNs.Suite>();
const filteredProjectSuites = new Map<commonConfig.FullProjectInternal, testNs.Suite>();
// Filter all the projects using grep, testId, file names, etc.
{
@@ -145,10 +139,10 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
if (shouldFilterOnly) {
// Create a fake root to execute the exclusive semantics across the projects.
const filteredRoot = new Suite('', 'root');
const filteredRoot = new testNs.Suite('', 'root');
for (const filteredProjectSuite of filteredProjectSuites.values())
filteredRoot._addSuite(filteredProjectSuite);
filterOnly(filteredRoot);
suiteUtils.filterOnly(filteredRoot);
for (const [project, filteredProjectSuite] of filteredProjectSuites) {
if (!filteredRoot.suites.includes(filteredProjectSuite))
filteredProjectSuites.delete(project);
@@ -186,18 +180,18 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
// Shard test groups.
const testGroupsInThisShard = filterForShard(config.config.shard, config.configCLIOverrides.shardWeights, testGroups);
const testsInThisShard = new Set<TestCase>();
const testsInThisShard = new Set<testNs.TestCase>();
for (const group of testGroupsInThisShard) {
for (const test of group.tests)
testsInThisShard.add(test);
}
// Update project suites, removing empty ones.
filterTestsRemoveEmptySuites(rootSuite, test => testsInThisShard.has(test));
suiteUtils.filterTestsRemoveEmptySuites(rootSuite, test => testsInThisShard.has(test));
}
if (config.postShardTestFilters.length)
filterTestsRemoveEmptySuites(rootSuite, test => config.postShardTestFilters.every(filter => filter(test)));
suiteUtils.filterTestsRemoveEmptySuites(rootSuite, test => config.postShardTestFilters.every(filter => filter(test)));
const topLevelProjects = [];
// Now prepend dependency projects without filtration.
@@ -218,14 +212,14 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
return { rootSuite, topLevelProjects };
}
function createProjectSuite(project: FullProjectInternal, fileSuites: Suite[]): Suite {
const projectSuite = new Suite(project.project.name, 'project');
function createProjectSuite(project: commonConfig.FullProjectInternal, fileSuites: testNs.Suite[]): testNs.Suite {
const projectSuite = new testNs.Suite(project.project.name, 'project');
for (const fileSuite of fileSuites)
projectSuite._addSuite(bindFileSuiteToProject(project, fileSuite));
projectSuite._addSuite(suiteUtils.bindFileSuiteToProject(project, fileSuite));
const grepMatcher = createTitleMatcher(project.project.grep);
const grepInvertMatcher = project.project.grepInvert ? createTitleMatcher(project.project.grepInvert) : null;
filterTestsRemoveEmptySuites(projectSuite, (test: TestCase) => {
suiteUtils.filterTestsRemoveEmptySuites(projectSuite, (test: testNs.TestCase) => {
const grepTitle = test._grepTitleWithTags();
if (grepInvertMatcher?.(grepTitle))
return false;
@@ -234,18 +228,18 @@ function createProjectSuite(project: FullProjectInternal, fileSuites: Suite[]):
return projectSuite;
}
function filterProjectSuite(projectSuite: Suite, testFilters: TestCaseFilter[]): Suite {
function filterProjectSuite(projectSuite: testNs.Suite, testFilters: TestCaseFilter[]): testNs.Suite {
// Fast path.
if (!testFilters.length)
return projectSuite;
const result = projectSuite._deepClone();
filterTestsRemoveEmptySuites(result, test => testFilters.every(filter => filter(test)));
suiteUtils.filterTestsRemoveEmptySuites(result, test => testFilters.every(filter => filter(test)));
return result;
}
function buildProjectSuite(project: FullProjectInternal, projectSuite: Suite): Suite {
const result = new Suite(project.project.name, 'project');
function buildProjectSuite(project: commonConfig.FullProjectInternal, projectSuite: testNs.Suite): testNs.Suite {
const result = new testNs.Suite(project.project.name, 'project');
result._fullProject = project;
if (project.fullyParallel)
result._parallelMode = 'parallel';
@@ -256,14 +250,14 @@ function buildProjectSuite(project: FullProjectInternal, projectSuite: Suite): S
for (let repeatEachIndex = 1; repeatEachIndex < project.project.repeatEach; repeatEachIndex++) {
const clone = fileSuite._deepClone();
applyRepeatEachIndex(project, clone, repeatEachIndex);
suiteUtils.applyRepeatEachIndex(project, clone, repeatEachIndex);
result._addSuite(clone);
}
}
return result;
}
function createForbidOnlyErrors(onlyTestsAndSuites: (TestCase | Suite)[], forbidOnlyCLIFlag: boolean | undefined, configFilePath: string | undefined): TestError[] {
function createForbidOnlyErrors(onlyTestsAndSuites: (testNs.TestCase | testNs.Suite)[], forbidOnlyCLIFlag: boolean | undefined, configFilePath: string | undefined): TestError[] {
const errors: TestError[] = [];
for (const testOrSuite of onlyTestsAndSuites) {
// Skip root and file.
@@ -279,9 +273,9 @@ function createForbidOnlyErrors(onlyTestsAndSuites: (TestCase | Suite)[], forbid
return errors;
}
function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuite: Suite): TestError[] {
function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuite: testNs.Suite): TestError[] {
const errors: TestError[] = [];
const testsByFullTitle = new Map<string, TestCase>();
const testsByFullTitle = new Map<string, testNs.TestCase>();
for (const test of fileSuite.allTests()) {
const fullTitle = test.titlePath().slice(1).join(' › ');
const existingTest = testsByFullTitle.get(fullTitle);
@@ -297,14 +291,14 @@ function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuite: Suit
return errors;
}
function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) {
function buildItemLocation(rootDir: string, testOrSuite: testNs.Suite | testNs.TestCase) {
if (!testOrSuite.location)
return '';
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
}
async function requireOrImportDefaultFunction(file: string, expectConstructor: boolean) {
let func = await requireOrImport(file);
let func = await transform.requireOrImport(file);
if (func && typeof func === 'object' && ('default' in func))
func = func['default'];
if (typeof func !== 'function')
@@ -354,7 +348,7 @@ export async function loadTestList(config: FullConfigInternal, filePath: string)
}
return { project, file: toPosixPath(parseLocationArg(tokens[0]).file), titlePath: tokens.slice(1) };
});
const testFilter = (test: TestCase) => descriptions.some(d => {
const testFilter = (test: testNs.TestCase) => descriptions.some(d => {
// Note: there is no root yet at the time of filtering.
const [projectName, , ...titles] = test.titlePath();
if (d.project !== undefined && d.project !== projectName)
+10 -16
View File
@@ -15,38 +15,32 @@
*/
import { ProcessHost } from './processHost';
import { incorporateCompilationCache } from '../common/esmLoaderHost';
import { serializeConfig } from '../common/ipc';
import { PoolBuilder } from '../common/poolBuilder';
import { Suite } from '../common/test';
import { loadTestFile } from '../common/testLoader';
import { addToCompilationCache } from '../transform/compilationCache';
import { cc, esm, FullConfigInternal, ipc, poolBuilder, test as testNs, testLoader } from '../common';
import type { TestError } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
export class InProcessLoaderHost {
private _config: FullConfigInternal;
private _poolBuilder: PoolBuilder;
private _poolBuilder: poolBuilder.PoolBuilder;
constructor(config: FullConfigInternal) {
this._config = config;
this._poolBuilder = PoolBuilder.createForLoader();
this._poolBuilder = poolBuilder.PoolBuilder.createForLoader();
}
async start(errors: TestError[]) {
return true;
}
async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
const result = await loadTestFile(file, this._config, testErrors);
async loadTestFile(file: string, testErrors: TestError[]): Promise<testNs.Suite> {
const result = await testLoader.loadTestFile(file, this._config, testErrors);
this._poolBuilder.buildPools(result, testErrors);
return result;
}
async stop() {
await incorporateCompilationCache();
await esm.incorporateCompilationCache();
}
}
@@ -60,7 +54,7 @@ export class OutOfProcessLoaderHost {
}
async start(errors: TestError[]) {
const startError = await this._processHost.startRunner(serializeConfig(this._config, false));
const startError = await this._processHost.startRunner(ipc.serializeConfig(this._config, false));
if (startError) {
errors.push({
message: `Test loader process failed to start with code "${startError.code}" and signal "${startError.signal}"`,
@@ -70,15 +64,15 @@ export class OutOfProcessLoaderHost {
return true;
}
async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
async loadTestFile(file: string, testErrors: TestError[]): Promise<testNs.Suite> {
const result = await this._processHost.sendMessage({ method: 'loadTestFile', params: { file } }) as any;
testErrors.push(...result.testErrors);
return Suite._deepParse(result.fileSuite);
return testNs.Suite._deepParse(result.fileSuite);
}
async stop() {
const result = await this._processHost.sendMessage({ method: 'getCompilationCacheFromLoader' }) as any;
addToCompilationCache(result);
cc.addToCompilationCache(result);
await this._processHost.stop();
}
}
@@ -21,8 +21,7 @@ import debug from 'debug';
import { assert } from '@isomorphic/assert';
import { timeOrigin } from '@isomorphic/time';
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
import type { ProtocolRequest, ProtocolResponse } from '../common/process';
import type { ipc, processRunner } from '../common';
export type ProcessExitData = {
unexpectedly: boolean;
@@ -79,10 +78,10 @@ export class ProcessHost extends EventEmitter {
if (debug.enabled('pw:test:protocol'))
debug('pw:test:protocol')('◀ RECV ' + JSON.stringify(message));
if (message.method === '__env_produced__') {
const producedEnv: EnvProducedPayload = message.params;
const producedEnv: ipc.EnvProducedPayload = message.params;
this._producedEnv = Object.fromEntries(producedEnv.map(e => [e[0], e[1] ?? undefined]));
} else if (message.method === '__dispatch__') {
const { id, error, method, params, result } = message.params as ProtocolResponse;
const { id, error, method, params, result } = message.params as processRunner.ProtocolResponse;
if (id && this._callbacks.has(id)) {
const { resolve, reject } = this._callbacks.get(id)!;
this._callbacks.delete(id);
@@ -97,7 +96,7 @@ export class ProcessHost extends EventEmitter {
this.emit(method!, params);
}
} else if (message.method === '__request__') {
const { id, method, params } = message.params as ProtocolRequest;
const { id, method, params } = message.params as processRunner.ProtocolRequest;
const handler = this._requestHandlers.get(method);
if (!handler) {
this.send({ method: '__response__', params: { id, error: { message: 'Unknown method' } } });
@@ -126,7 +125,7 @@ export class ProcessHost extends EventEmitter {
if (error)
return error;
const processParams: ProcessInitParams = {
const processParams: ipc.ProcessInitParams = {
processName: this._processName,
timeOrigin: timeOrigin(),
};
+13 -13
View File
@@ -23,7 +23,7 @@ import { escapeRegExp } from '@isomorphic/stringUtils';
import { createFileMatcher } from '../util';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { config as commonConfig, FullConfigInternal } from '../common';
const readFileAsync = promisify(fs.readFile);
@@ -33,7 +33,7 @@ function wildcardPatternToRegExp(pattern: string): RegExp {
return new RegExp('^' + pattern.split('*').map(escapeRegExp).join('.*') + '$', 'ig');
}
export function filterProjects(projects: FullProjectInternal[], projectNames?: string[]): FullProjectInternal[] {
export function filterProjects(projects: commonConfig.FullProjectInternal[], projectNames?: string[]): commonConfig.FullProjectInternal[] {
if (!projectNames)
return [...projects];
@@ -78,8 +78,8 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
return result;
}
export function buildTeardownToSetupsMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal[]> {
const result = new Map<FullProjectInternal, FullProjectInternal[]>();
export function buildTeardownToSetupsMap(projects: commonConfig.FullProjectInternal[]): Map<commonConfig.FullProjectInternal, commonConfig.FullProjectInternal[]> {
const result = new Map<commonConfig.FullProjectInternal, commonConfig.FullProjectInternal[]>();
for (const project of projects) {
if (project.teardown) {
const setups = result.get(project.teardown) || [];
@@ -90,9 +90,9 @@ export function buildTeardownToSetupsMap(projects: FullProjectInternal[]): Map<F
return result;
}
export function buildProjectsClosure(projects: FullProjectInternal[], hasTests?: (project: FullProjectInternal) => boolean): Map<FullProjectInternal, 'top-level' | 'dependency'> {
const result = new Map<FullProjectInternal, 'top-level' | 'dependency'>();
const visit = (depth: number, project: FullProjectInternal) => {
export function buildProjectsClosure(projects: commonConfig.FullProjectInternal[], hasTests?: (project: commonConfig.FullProjectInternal) => boolean): Map<commonConfig.FullProjectInternal, 'top-level' | 'dependency'> {
const result = new Map<commonConfig.FullProjectInternal, 'top-level' | 'dependency'>();
const visit = (depth: number, project: commonConfig.FullProjectInternal) => {
if (depth > 100) {
const error = new Error('Circular dependency detected between projects.');
error.stack = '';
@@ -115,19 +115,19 @@ export function buildProjectsClosure(projects: FullProjectInternal[], hasTests?:
return result;
}
export function findTopLevelProjects(config: FullConfigInternal): FullProjectInternal[] {
export function findTopLevelProjects(config: FullConfigInternal): commonConfig.FullProjectInternal[] {
const closure = buildProjectsClosure(config.projects);
return [...closure].filter(entry => entry[1] === 'top-level').map(entry => entry[0]);
}
export function buildDependentProjects(forProjects: FullProjectInternal[], projects: FullProjectInternal[]): Set<FullProjectInternal> {
const reverseDeps = new Map<FullProjectInternal, FullProjectInternal[]>(projects.map(p => ([p, []])));
export function buildDependentProjects(forProjects: commonConfig.FullProjectInternal[], projects: commonConfig.FullProjectInternal[]): Set<commonConfig.FullProjectInternal> {
const reverseDeps = new Map<commonConfig.FullProjectInternal, commonConfig.FullProjectInternal[]>(projects.map(p => ([p, []])));
for (const project of projects) {
for (const dep of project.deps)
reverseDeps.get(dep)!.push(project);
}
const result = new Set<FullProjectInternal>();
const visit = (depth: number, project: FullProjectInternal) => {
const result = new Set<commonConfig.FullProjectInternal>();
const visit = (depth: number, project: commonConfig.FullProjectInternal) => {
if (depth > 100) {
const error = new Error('Circular dependency detected between projects.');
error.stack = '';
@@ -144,7 +144,7 @@ export function buildDependentProjects(forProjects: FullProjectInternal[], proje
return result;
}
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
export async function collectFilesForProject(project: commonConfig.FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
const extensions = new Set(['.js', '.ts', '.mjs', '.mts', '.cjs', '.cts', '.jsx', '.tsx', '.mjsx', '.mtsx', '.cjsx', '.ctsx']);
const testFileExtension = (file: string) => extensions.has(path.extname(file));
const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache);
+4 -5
View File
@@ -22,12 +22,11 @@ import * as diff from 'diff';
import { MultiMap } from '@isomorphic/multimap';
import { filterProjects } from './projectUtils';
import { babelParse, traverse, types } from '../transform/babelBundle';
import { babel, FullConfigInternal } from '../common';
import type { FullConfigInternal } from '../common/config';
import type { InternalReporter } from '../reporters/internalReporter';
import type { T } from '../transform/babelBundle';
const t: typeof T = types;
const t: typeof T = babel.types;
type Location = {
file: string;
@@ -70,10 +69,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
const source = await fs.promises.readFile(fileName, 'utf8');
const lines = source.split('\n');
const replacements = suggestedRebaselines.get(fileName);
const fileNode = babelParse(source, fileName, true);
const fileNode = babel.babelParse(source, fileName, true);
const ranges: { start: number, end: number, oldText: string, newText: string }[] = [];
traverse(fileNode, {
babel.traverse(fileNode, {
CallExpression: path => {
const node = path.node;
if (node.arguments.length < 1)
+2 -2
View File
@@ -32,12 +32,12 @@ import { wrapReporterAsV2 } from '../reporters/reporterV2';
import type { ReporterDescription } from '../../types/test';
import type { TestError } from '../../types/testReporter';
import type { BuiltInReporter, FullConfigInternal } from '../common/config';
import type { config as commonConfig, FullConfigInternal } from '../common';
import type { CommonReporterOptions, Screen } from '../reporters/base';
import type { ReporterV2 } from '../reporters/reporterV2';
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
const defaultReporters: { [key in BuiltInReporter]: new(arg: any) => ReporterV2 } = {
const defaultReporters: { [key in commonConfig.BuiltInReporter]: new(arg: any) => ReporterV2 } = {
blob: BlobReporter,
dot: mode === 'list' ? ListModeReporter : DotReporter,
line: mode === 'list' ? ListModeReporter : LineReporter,
+15 -18
View File
@@ -30,25 +30,22 @@ import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
import { TaskRunner } from './taskRunner';
import { detectChangedTestFiles } from './vcs';
import { Suite } from '../common/test';
import { cc, config as commonConfig, FullConfigInternal, suiteUtils, test as testNs } from '../common';
import { createTestGroups } from '../runner/testGroups';
import { cacheDir } from '../transform/compilationCache';
import { createTitleMatcher, forceRegExp, removeDirAndLogToConsole } from '../util';
import { createFiltersFromArguments } from '../common/suiteUtils';
import type { TestGroup } from '../runner/testGroups';
import type { EnvByProjectId } from './dispatcher';
import type { TestRunnerPluginRegistration } from '../plugins';
import type { Task } from './taskRunner';
import type { FullResult } from '../../types/testReporter';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { InternalReporter } from '../reporters/internalReporter';
const readDirAsync = promisify(fs.readdir);
type ProjectWithTestGroups = {
project: FullProjectInternal;
projectSuite: Suite;
project: commonConfig.FullProjectInternal;
projectSuite: testNs.Suite;
testGroups: TestGroup[];
};
@@ -61,11 +58,11 @@ export class TestRun {
readonly config: FullConfigInternal;
readonly reporter: InternalReporter;
readonly failureTracker: FailureTracker;
rootSuite: Suite | undefined = undefined;
rootSuite: testNs.Suite | undefined = undefined;
readonly phases: Phase[] = [];
projectFiles: Map<FullProjectInternal, string[]> = new Map();
projectSuites: Map<FullProjectInternal, Suite[]> = new Map();
topLevelProjects: FullProjectInternal[] = [];
projectFiles: Map<commonConfig.FullProjectInternal, string[]> = new Map();
projectSuites: Map<commonConfig.FullProjectInternal, testNs.Suite[]> = new Map();
topLevelProjects: commonConfig.FullProjectInternal[] = [];
constructor(config: FullConfigInternal, reporter: InternalReporter, options?: { pauseOnError?: boolean, pauseAtEnd?: boolean }) {
this.config = config;
@@ -128,7 +125,7 @@ export function createClearCacheTask(config: FullConfigInternal): Task<TestRun>
return {
title: 'clear cache',
setup: async () => {
await removeDirAndLogToConsole(cacheDir);
await removeDirAndLogToConsole(cc.cacheDir);
for (const plugin of config.plugins)
await plugin.instance?.clearCache?.();
},
@@ -240,12 +237,12 @@ export function createListFilesTask(): Task<TestRun> {
testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects);
await collectProjectsAndTestFiles(testRun, false);
for (const [project, files] of testRun.projectFiles) {
const projectSuite = new Suite(project.project.name, 'project');
const projectSuite = new testNs.Suite(project.project.name, 'project');
projectSuite._fullProject = project;
testRun.rootSuite._addSuite(projectSuite);
const suites = files.map(file => {
const title = path.relative(testRun.config.config.rootDir, file);
const suite = new Suite(title, 'file');
const suite = new testNs.Suite(title, 'file');
suite.location = { file, line: 0, column: 0 };
projectSuite._addSuite(suite);
return suite;
@@ -261,7 +258,7 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: {
title: 'load tests',
setup: async (testRun, errors, softErrors) => {
if (testRun.config.cliArgs.length) {
const { testFilter, fileFilter } = createFiltersFromArguments(testRun.config.cliArgs);
const { testFilter, fileFilter } = suiteUtils.createFiltersFromArguments(testRun.config.cliArgs);
testRun.config.loadFileFilters.push(fileFilter);
testRun.config.preOnlyTestFilters.push(testFilter);
}
@@ -342,11 +339,11 @@ function createPhasesTask(): Task<TestRun> {
setup: async testRun => {
let maxConcurrentTestGroups = 0;
const processed = new Set<FullProjectInternal>();
const processed = new Set<commonConfig.FullProjectInternal>();
const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite]));
const allProjects = [...projectToSuite.keys()];
const teardownToSetups = buildTeardownToSetupsMap(allProjects);
const teardownToSetupsDependents = new Map<FullProjectInternal, FullProjectInternal[]>();
const teardownToSetupsDependents = new Map<commonConfig.FullProjectInternal, commonConfig.FullProjectInternal[]>();
for (const [teardown, setups] of teardownToSetups) {
const closure = buildDependentProjects(setups, allProjects);
closure.delete(teardown);
@@ -355,7 +352,7 @@ function createPhasesTask(): Task<TestRun> {
for (let i = 0; i < projectToSuite.size; i++) {
// Find all projects that have all their dependencies processed by previous phases.
const phaseProjects: FullProjectInternal[] = [];
const phaseProjects: commonConfig.FullProjectInternal[] = [];
for (const project of projectToSuite.keys()) {
if (processed.has(project))
continue;
@@ -392,7 +389,7 @@ function createRunTestsTask(): Task<TestRun> {
return {
title: 'test suite',
setup: async ({ phases, failureTracker }) => {
const successfulProjects = new Set<FullProjectInternal>();
const successfulProjects = new Set<commonConfig.FullProjectInternal>();
const extraEnvByProjectId: EnvByProjectId = new Map();
const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
+7 -7
View File
@@ -14,17 +14,17 @@
* limitations under the License.
*/
import type { Suite, TestCase } from '../common/test';
import type { test } from '../common';
export type TestGroup = {
workerHash: string;
requireFile: string;
repeatEachIndex: number;
projectId: string;
tests: TestCase[];
tests: test.TestCase[];
};
export function createTestGroups(projectSuite: Suite, expectedParallelism: number): TestGroup[] {
export function createTestGroups(projectSuite: test.Suite, expectedParallelism: number): TestGroup[] {
// This function groups tests that can be run together.
// Tests cannot be run together when:
// - They belong to different projects - requires different workers.
@@ -47,11 +47,11 @@ export function createTestGroups(projectSuite: Suite, expectedParallelism: numbe
// We'll divide them into equally-sized groups later.
// - Tests belonging to serial suites inside parallel suites.
// These should run as a serial group, each group is independent, key === serial suite.
parallel: Map<Suite | TestCase, TestGroup>,
parallel: Map<test.Suite | test.TestCase, TestGroup>,
parallelWithHooks: TestGroup,
}>>();
const createGroup = (test: TestCase): TestGroup => {
const createGroup = (test: test.TestCase): TestGroup => {
return {
workerHash: test._workerHash,
requireFile: test._requireFile,
@@ -79,9 +79,9 @@ export function createTestGroups(projectSuite: Suite, expectedParallelism: numbe
// Note that a parallel suite cannot be inside a serial suite. This is enforced in TestType.
let insideParallel = false;
let outerMostSequentialSuite: Suite | undefined;
let outerMostSequentialSuite: test.Suite | undefined;
let hasAllHooks = false;
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) {
for (let parent: test.Suite | undefined = test.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' || parent._parallelMode === 'default')
outerMostSequentialSuite = parent;
insideParallel = insideParallel || parent._parallelMode === 'parallel';
+12 -14
View File
@@ -24,22 +24,20 @@ import { ManualPromise } from '@isomorphic/manualPromise';
import { setPlaywrightTestProcessEnv } from '@utils/env';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { loadConfig } from '../common/configLoader';
import { cc, configLoader, FullConfigInternal, ipc } from '../common';
import { FSWatcher } from './fsWatcher';
import { baseFullConfig } from '../isomorphic/teleReceiver';
import { addGitCommitInfoPlugin } from '../plugins/gitCommitInfoPlugin';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { internalScreen } from '../reporters/base';
import { InternalReporter } from '../reporters/internalReporter';
import { affectedTestFiles, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
import { serializeError } from '../util';
import { createErrorCollectingReporter, createReporters } from './reporters';
import { TestRun, createApplyRebaselinesTask, createClearCacheTask, createGlobalSetupTasks, createListFilesTask, createLoadTask, createPluginSetupTasks, createReportBeginTask, createRunTestsTasks, runTasks, runTasksDeferCleanup } from './tasks';
import { LastRunReporter } from './lastRun';
import type * as reporterTypes from '../../types/testReporter';
import type { ConfigLocation, FullConfigInternal } from '../common/config';
import type { ConfigCLIOverrides } from '../common/ipc';
import type { ConfigLocation } from '../common';
import type { TestRunnerPluginRegistration } from '../plugins';
import type { AnyReporter } from '../reporters/reporterV2';
import type { TestPausedParams } from './failureTracker';
@@ -90,7 +88,7 @@ export type FullResultStatus = reporterTypes.FullResult['status'];
export class TestRunner extends EventEmitter<TestRunnerEventMap> {
readonly configLocation: ConfigLocation;
private _configCLIOverrides: ConfigCLIOverrides;
private _configCLIOverrides: ipc.ConfigCLIOverrides;
private _watcher: FSWatcher;
private _watchedProjectDirs = new Set<string>();
@@ -105,13 +103,13 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
private _populateDependenciesOnList = false;
private _startingEnv: NodeJS.ProcessEnv = {};
constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) {
constructor(configLocation: ConfigLocation, configCLIOverrides: ipc.ConfigCLIOverrides) {
super();
this.configLocation = configLocation;
this._configCLIOverrides = configCLIOverrides;
this._watcher = new FSWatcher(events => {
const collector = new Set<string>();
events.forEach(f => collectAffectedTestFiles(f.file, collector));
events.forEach(f => cc.collectAffectedTestFiles(f.file, collector));
this.emit(TestRunnerEvent.TestFilesChanged, [...collector]);
});
}
@@ -232,7 +230,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
status: reporterTypes.FullResult['status'],
config?: FullConfigInternal,
}> {
const overrides: ConfigCLIOverrides = {
const overrides: ipc.ConfigCLIOverrides = {
...this._configCLIOverrides,
repeatEach: 1,
retries: 0,
@@ -289,7 +287,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
private async _innerRunTests(userReporter: AnyReporter, params: RunTestsParams): Promise<{ status: FullResultStatus }> {
await this.stopTests();
const overrides: ConfigCLIOverrides = {
const overrides: ipc.ConfigCLIOverrides = {
...this._configCLIOverrides,
repeatEach: 1,
retries: 0,
@@ -349,7 +347,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
this._watchedTestDependencies = new Set();
for (const fileName of fileNames) {
this._watchedTestDependencies.add(fileName);
dependenciesForTestFile(fileName).forEach(file => this._watchedTestDependencies.add(file));
cc.dependenciesForTestFile(fileName).forEach(file => this._watchedTestDependencies.add(file));
}
await this._updateWatcher(true);
}
@@ -366,7 +364,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
]);
if (status !== 'passed')
return { errors: errorReporter.errors(), testFiles: [] };
return { testFiles: affectedTestFiles(files) };
return { testFiles: cc.affectedTestFiles(files) };
}
async stopTests() {
@@ -382,9 +380,9 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
await this.runGlobalTeardown();
}
private async _loadConfig(overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> {
private async _loadConfig(overrides?: ipc.ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> {
try {
const config = await loadConfig(this.configLocation, overrides);
const config = await configLoader.loadConfig(this.configLocation, overrides);
// Preserve plugin instances between setup and build.
if (!this._plugins) {
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
@@ -399,7 +397,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
}
}
private async _loadConfigOrReportError(reporter: InternalReporter, overrides?: ConfigCLIOverrides): Promise<FullConfigInternal | null> {
private async _loadConfigOrReportError(reporter: InternalReporter, overrides?: ipc.ConfigCLIOverrides): Promise<FullConfigInternal | null> {
const { config, error } = await this._loadConfig(overrides);
if (config)
return config;
+12 -13
View File
@@ -24,7 +24,7 @@ import { isUnderTest } from '@utils/debug';
import { HttpServer } from '@utils/httpServer';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { loadConfig, resolveConfigLocation } from '../common/configLoader';
import { configLoader, ipc } from '../common';
import ListReporter from '../reporters/list';
import { createReporterForTestServer } from './reporters';
import { SigIntWatcher } from './sigIntWatcher';
@@ -32,8 +32,7 @@ import { TestRunner, TestRunnerEvent } from './testRunner';
import type { Transport } from '@utils/httpServer';
import type * as reporterTypes from '../../types/testReporter';
import type { ConfigLocation } from '../common/config';
import type { ConfigCLIOverrides } from '../common/ipc';
import type { ConfigLocation } from '../common';
import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
import type { ReporterV2 } from '../reporters/reporterV2';
@@ -50,10 +49,10 @@ const originalStdinIsTTY = process.stdin.isTTY;
class TestServer {
private _configLocation: ConfigLocation;
private _configCLIOverrides: ConfigCLIOverrides;
private _configCLIOverrides: ipc.ConfigCLIOverrides;
private _dispatcher: TestServerDispatcher | undefined;
constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) {
constructor(configLocation: ConfigLocation, configCLIOverrides: ipc.ConfigCLIOverrides) {
this._configLocation = configLocation;
this._configCLIOverrides = configCLIOverrides;
}
@@ -100,7 +99,7 @@ export class TestServerDispatcher implements TestServerInterface {
private _globalSetupReport: ReportEntry[] | undefined;
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) {
constructor(configLocation: ConfigLocation, configCLIOverrides: ipc.ConfigCLIOverrides) {
this._testRunner = new TestRunner(configLocation, configCLIOverrides);
this.transport = {
onconnect: () => {},
@@ -259,8 +258,8 @@ export class TestServerDispatcher implements TestServerInterface {
}
}
export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
const configLocation = resolveConfigLocation(configFile);
export async function runUIMode(configFile: string | undefined, configCLIOverrides: ipc.ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
const configLocation = configLoader.resolveConfigLocation(configFile);
return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
await sever.installRootRedirect(server, undefined, { ...options, webApp: 'uiMode.html' });
if (options.host !== undefined || options.port !== undefined) {
@@ -280,8 +279,8 @@ export async function runUIMode(configFile: string | undefined, configCLIOverrid
}
// Pick first channel that is used by one of the projects, to ensure it is installed on the machine.
async function installedChromiumChannelForUI(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) {
const config = await loadConfig(configLocation, configCLIOverrides).catch(e => null);
async function installedChromiumChannelForUI(configLocation: ConfigLocation, configCLIOverrides: ipc.ConfigCLIOverrides) {
const config = await configLoader.loadConfig(configLocation, configCLIOverrides).catch(e => null);
if (!config)
return undefined;
if (config.projects.some(p => (!p.project.use.browserName || p.project.use.browserName === 'chromium') && !p.project.use.channel))
@@ -293,15 +292,15 @@ async function installedChromiumChannelForUI(configLocation: ConfigLocation, con
return undefined;
}
export async function runTestServer(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status']> {
const configLocation = resolveConfigLocation(configFile);
export async function runTestServer(configFile: string | undefined, configCLIOverrides: ipc.ConfigCLIOverrides, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status']> {
const configLocation = configLoader.resolveConfigLocation(configFile);
return await innerRunTestServer(configLocation, configCLIOverrides, options, async server => {
// eslint-disable-next-line no-console
console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid());
});
}
async function innerRunTestServer(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
async function innerRunTestServer(configLocation: ConfigLocation, configCLIOverrides: ipc.ConfigCLIOverrides, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
const testServer = new TestServer(configLocation, configCLIOverrides);
const cancelPromise = new ManualPromise<void>();
const sigintWatcher = new SigIntWatcher();
+2 -2
View File
@@ -17,7 +17,7 @@
import childProcess from 'child_process';
import path from 'path';
import { affectedTestFiles } from '../transform/compilationCache';
import { cc } from '../common';
export async function detectChangedTestFiles(baseCommit: string, configDir: string): Promise<Set<string>> {
function gitFileList(command: string) {
@@ -54,5 +54,5 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri
const [gitRoot] = gitFileList('rev-parse --show-toplevel');
const trackedFilesWithChanges = gitFileList(`diff ${baseCommit} --name-only`).map(file => path.join(gitRoot, file));
return new Set(affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges]));
return new Set(cc.affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges]));
}
+1 -1
View File
@@ -32,7 +32,7 @@ import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater';
import { TestServerConnection } from '../isomorphic/testServerConnection';
import type * as reporterTypes from '../../types/testReporter';
import type { ConfigLocation } from '../common/config';
import type { ConfigLocation } from '../common';
import type { TestServerTransport } from '../isomorphic/testServerConnection';
/* eslint-disable no-restricted-properties */
+9 -10
View File
@@ -20,18 +20,17 @@ import path from 'path';
import { removeFolders } from '@utils/fileUtils';
import { ProcessHost } from './processHost';
import { stdioChunkToParams } from '../common/ipc';
import { ipc } from '../common';
import { artifactsFolderName } from '../isomorphic/folders';
import type { TestGroup } from './testGroups';
import type { CustomMessageRequestPayload, CustomMessageResponsePayload, RunPayload, SerializedConfig, ResumePayload, WorkerInitParams } from '../common/ipc';
let lastWorkerIndex = 0;
type WorkerHostOptions = {
parallelIndex: number;
config: SerializedConfig;
config: ipc.SerializedConfig;
extraEnv: Record<string, string | undefined>;
outputDir: string;
pauseOnError: boolean;
@@ -42,7 +41,7 @@ export class WorkerHost extends ProcessHost {
readonly parallelIndex: number;
readonly workerIndex: number;
private _hash: string;
private _params: WorkerInitParams;
private _params: ipc.WorkerInitParams;
private _didFail = false;
constructor(testGroup: TestGroup, options: WorkerHostOptions) {
@@ -71,8 +70,8 @@ export class WorkerHost extends ProcessHost {
async start() {
await fs.promises.mkdir(this._params.artifactsDir, { recursive: true });
return await this.startRunner(this._params, {
onStdOut: chunk => this.emit('stdOut', stdioChunkToParams(chunk)),
onStdErr: chunk => this.emit('stdErr', stdioChunkToParams(chunk)),
onStdOut: chunk => this.emit('stdOut', ipc.stdioChunkToParams(chunk)),
onStdErr: chunk => this.emit('stdErr', ipc.stdioChunkToParams(chunk)),
});
}
@@ -86,15 +85,15 @@ export class WorkerHost extends ProcessHost {
await super.stop();
}
runTestGroup(runPayload: RunPayload) {
runTestGroup(runPayload: ipc.RunPayload) {
this.sendMessageNoReply({ method: 'runTestGroup', params: runPayload });
}
async sendCustomMessage(payload: CustomMessageRequestPayload) {
return await this.sendMessage({ method: 'customMessage', params: payload }) as CustomMessageResponsePayload;
async sendCustomMessage(payload: ipc.CustomMessageRequestPayload) {
return await this.sendMessage({ method: 'customMessage', params: payload }) as ipc.CustomMessageResponsePayload;
}
sendResume(payload: ResumePayload) {
sendResume(payload: ipc.ResumePayload) {
this.sendMessageNoReply({ method: 'resume', params: payload });
}
+1 -1
View File
@@ -1,6 +1,6 @@
[*]
../util.ts
../common/globals.ts
../globals.ts
../package.ts
@utils/**
node_modules/json5
@@ -20,7 +20,7 @@ import path from 'path';
import { calculateSha1 } from '@utils/crypto';
import sourceMapSupport from 'source-map-support';
import { isWorkerProcess } from '../common/globals';
import { isWorkerProcess } from '../globals';
import { packageRoot } from '../package';
export type MemoryCache = {
+8
View File
@@ -401,4 +401,12 @@ export async function removeDirAndLogToConsole(dir: string) {
}
}
export function takeFirst<T>(...args: (T | undefined)[]): T {
for (const arg of args) {
if (arg !== undefined)
return arg;
}
return undefined as any as T;
}
export { ansiRegex, stripAnsiEscapes };
+1 -1
View File
@@ -1,8 +1,8 @@
[*]
@isomorphic/**
@utils/**
../globals.ts
../common/
../transform/
../util.ts
../matchers/**
../isomorphic/util.ts
+14 -15
View File
@@ -17,18 +17,17 @@
import { ManualPromise } from '@isomorphic/manualPromise';
import { escapeWithQuotes } from '@isomorphic/stringUtils';
import { fixtureParameterNames } from '../common/fixtures';
import { fixtures } from '../common';
import { filterStackFile, formatLocation } from '../util';
import type { TestInfoImpl } from './testInfo';
import type { FixtureDescription, RunnableDescription } from './timeoutManager';
import type { WorkerInfo } from '../../types/test';
import type { Location } from '../../types/testReporter';
import type { FixturePool, FixtureRegistration, FixtureScope } from '../common/fixtures';
class Fixture {
runner: FixtureRunner;
registration: FixtureRegistration;
registration: fixtures.FixtureRegistration;
value: any;
failed = false;
@@ -40,7 +39,7 @@ class Fixture {
_deps = new Set<Fixture>();
_usages = new Set<Fixture>();
constructor(runner: FixtureRunner, registration: FixtureRegistration) {
constructor(runner: FixtureRunner, registration: fixtures.FixtureRegistration) {
this.runner = runner;
this.registration = registration;
this.value = null;
@@ -170,7 +169,7 @@ class Fixture {
}
}
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) {
_collectFixturesInTeardownOrder(scope: fixtures.FixtureScope, collector: Set<Fixture>) {
if (this.registration.scope !== scope)
return;
for (const fixture of this._usages)
@@ -181,11 +180,11 @@ class Fixture {
export class FixtureRunner {
private testScopeClean = true;
pool: FixturePool | undefined;
pool: fixtures.FixturePool | undefined;
instanceForId = new Map<string, Fixture>();
workerFixtureTimeout = 0;
setPool(pool: FixturePool) {
setPool(pool: fixtures.FixturePool) {
if (!this.testScopeClean)
throw new Error('Did not teardown test scope');
if (this.pool && pool.digest !== this.pool.digest) {
@@ -199,7 +198,7 @@ export class FixtureRunner {
this.pool = pool;
}
private _collectFixturesInSetupOrder(registration: FixtureRegistration, collector: Set<FixtureRegistration>) {
private _collectFixturesInSetupOrder(registration: fixtures.FixtureRegistration, collector: Set<fixtures.FixtureRegistration>) {
if (collector.has(registration))
return;
for (const name of registration.deps) {
@@ -209,11 +208,11 @@ export class FixtureRunner {
collector.add(registration);
}
async teardownScope(scope: FixtureScope, testInfo: TestInfoImpl, runnable: RunnableDescription) {
async teardownScope(scope: fixtures.FixtureScope, testInfo: TestInfoImpl, runnable: RunnableDescription) {
// Teardown fixtures in the reverse order.
const fixtures = Array.from(this.instanceForId.values()).reverse();
const allFixtures = Array.from(this.instanceForId.values()).reverse();
const collector = new Set<Fixture>();
for (const fixture of fixtures)
for (const fixture of allFixtures)
fixture._collectFixturesInTeardownOrder(scope, collector);
let firstError: Error | undefined;
for (const fixture of collector) {
@@ -230,10 +229,10 @@ export class FixtureRunner {
}
async resolveParametersForFunction(fn: Function, testInfo: TestInfoImpl, autoFixtures: 'worker' | 'test' | 'all-hooks-only', runnable: RunnableDescription): Promise<{ result: object } | null> {
const collector = new Set<FixtureRegistration>();
const collector = new Set<fixtures.FixtureRegistration>();
// Collect automatic fixtures.
const auto: FixtureRegistration[] = [];
const auto: fixtures.FixtureRegistration[] = [];
for (const registration of this.pool!.autoFixtures()) {
let shouldRun = true;
if (autoFixtures === 'all-hooks-only')
@@ -278,7 +277,7 @@ export class FixtureRunner {
await testInfo._runWithTimeout(runnable, () => fn(params.result, testInfo));
}
private async _setupFixtureForRegistration(registration: FixtureRegistration, testInfo: TestInfoImpl, runnable: RunnableDescription): Promise<Fixture> {
private async _setupFixtureForRegistration(registration: fixtures.FixtureRegistration, testInfo: TestInfoImpl, runnable: RunnableDescription): Promise<Fixture> {
if (registration.scope === 'test')
this.testScopeClean = false;
@@ -303,7 +302,7 @@ export class FixtureRunner {
}
function getRequiredFixtureNames(fn: Function, location?: Location) {
return fixtureParameterNames(fn, location ?? { file: '<unknown>', line: 1, column: 1 }, e => {
return fixtures.fixtureParameterNames(fn, location ?? { file: '<unknown>', line: 1, column: 1 }, e => {
throw new Error(`${formatLocation(e.location!)}: ${e.message}`);
});
}
+10 -12
View File
@@ -29,14 +29,12 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing';
import { testInfoError } from './util';
import { wrapFunctionWithLocation } from '../transform/transform';
import { transform } from '../common';
import type { RunnableDescription } from './timeoutManager';
import type { FullProject, TestInfo, TestStatus, TestStepInfo, TestAnnotation } from '../../types/test';
import type { FullConfig, Location } from '../../types/testReporter';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type * as ipc from '../common/ipc';
import type { TestCase } from '../common/test';
import type { config as commonConfig, FullConfigInternal, ipc, test as testNs } from '../common';
import type { StackFrame } from '@protocol/channels';
export type TestStepCategory = 'expect' | 'fixture' | 'hook' | 'pw:api' | 'test.step' | 'test.attach';
@@ -98,7 +96,7 @@ export class TestInfoImpl implements TestInfo {
private _interruptedPromise = new ManualPromise<void>();
_lastStepId = 0;
private readonly _requireFile: string;
readonly _projectInternal: FullProjectInternal;
readonly _projectInternal: commonConfig.FullProjectInternal;
readonly _configInternal: FullConfigInternal;
private readonly _steps: TestStepInternal[] = [];
private readonly _stepMap = new Map<string, TestStepInternal>();
@@ -173,9 +171,9 @@ export class TestInfoImpl implements TestInfo {
constructor(
configInternal: FullConfigInternal,
projectInternal: FullProjectInternal,
projectInternal: commonConfig.FullProjectInternal,
workerParams: ipc.WorkerInitParams,
test: TestCase | undefined,
test: testNs.TestCase | undefined,
retry: number,
callbacks: TestInfoCallbacks
) {
@@ -243,10 +241,10 @@ export class TestInfoImpl implements TestInfo {
this._tracing = new TestTracing(this, workerParams.artifactsDir);
this.skip = wrapFunctionWithLocation((location, ...args) => this._modifier('skip', location, args));
this.fixme = wrapFunctionWithLocation((location, ...args) => this._modifier('fixme', location, args));
this.fail = wrapFunctionWithLocation((location, ...args) => this._modifier('fail', location, args));
this.slow = wrapFunctionWithLocation((location, ...args) => this._modifier('slow', location, args));
this.skip = transform.wrapFunctionWithLocation((location, ...args) => this._modifier('skip', location, args));
this.fixme = transform.wrapFunctionWithLocation((location, ...args) => this._modifier('fixme', location, args));
this.fail = transform.wrapFunctionWithLocation((location, ...args) => this._modifier('fail', location, args));
this.slow = transform.wrapFunctionWithLocation((location, ...args) => this._modifier('slow', location, args));
}
_modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', location: Location, modifierArgs: [arg?: any, description?: string]) {
@@ -674,7 +672,7 @@ export class TestStepInfoImpl implements TestStepInfo {
this._stepId = stepId;
this._title = title;
this._parentStep = parentStep;
this.skip = wrapFunctionWithLocation((location: Location, ...args: unknown[]) => {
this.skip = transform.wrapFunctionWithLocation((location: Location, ...args: unknown[]) => {
// skip();
// skip(condition: boolean, description: string);
if (args.length > 0 && !args[0])
@@ -29,7 +29,7 @@ import { filteredStackTrace } from '../util';
import type { TestStepCategory, TestInfoImpl } from './testInfo';
import type { PlaywrightWorkerOptions, TestInfo, TraceMode } from '../../types/test';
import type { TestInfoErrorImpl } from '../common/ipc';
import type { ipc } from '../common';
import type { SerializedError, StackFrame } from '@protocol/channels';
import type * as trace from '@trace/trace';
import type EventEmitter from 'events';
@@ -250,7 +250,7 @@ export class TestTracing {
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
}
appendForError(error: TestInfoErrorImpl) {
appendForError(error: ipc.TestInfoErrorImpl) {
const rawStack = error.stack?.split('\n') || [];
const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({
@@ -260,7 +260,7 @@ export class TestTracing {
});
}
_formatError(error: TestInfoErrorImpl) {
_formatError(error: ipc.TestInfoErrorImpl) {
const parts: string[] = [error.message || String(error.value)];
if (error.cause)
parts.push('[cause]: ' + this._formatError(error.cause));
+2 -2
View File
@@ -16,8 +16,8 @@
import { serializeError } from '../util';
import type { TestInfoErrorImpl } from '../common/ipc';
import type { ipc } from '../common';
export function testInfoError(error: Error | any): TestInfoErrorImpl {
export function testInfoError(error: Error | any): ipc.TestInfoErrorImpl {
return serializeError(error);
}
+30 -38
View File
@@ -19,38 +19,30 @@ import { ManualPromise } from '@isomorphic/manualPromise';
import { removeFolders } from '@utils/fileUtils';
import { gracefullyCloseAll } from '@utils/processLauncher';
import { deserializeConfig } from '../common/configLoader';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { stdioChunkToParams } from '../common/ipc';
import { configLoader, fixtures, ipc, poolBuilder, ProcessRunner, suiteUtils, testLoader } from '../common';
import * as globals from '../globals';
import { debugTest, relativeFilePath } from '../util';
import { FixtureRunner } from './fixtureRunner';
import { TestSkipError, TestInfoImpl, emtpyTestInfoCallbacks } from './testInfo';
import { testInfoError } from './util';
import { inheritFixtureNames } from '../common/fixtures';
import { PoolBuilder } from '../common/poolBuilder';
import { ProcessRunner } from '../common/process';
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { loadTestFile } from '../common/testLoader';
import type { TimeSlot } from './timeoutManager';
import type { Location } from '../../types/testReporter';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type * as ipc from '../common/ipc';
import type { Suite, TestCase } from '../common/test';
import type { config as commonConfig, FullConfigInternal, test as testNs } from '../common';
import type { TestAnnotation } from '../../types/test';
export class WorkerMain extends ProcessRunner {
private _params: ipc.WorkerInitParams;
private _config!: FullConfigInternal;
private _project!: FullProjectInternal;
private _poolBuilder!: PoolBuilder;
private _project!: commonConfig.FullProjectInternal;
private _poolBuilder!: poolBuilder.PoolBuilder;
private _fixtureRunner: FixtureRunner;
// Accumulated fatal errors that cannot be attributed to a test.
private _fatalErrors: ipc.TestInfoErrorImpl[] = [];
// Whether we should skip running remaining tests in this suite because
// of a setup error, usually beforeAll hook.
private _skipRemainingTestsInSuite: Suite | undefined;
private _skipRemainingTestsInSuite: testNs.Suite | undefined;
// The stage of the full cleanup. Once "finished", we can safely stop running anything.
private _didRunFullCleanup = false;
// Whether the worker was stopped due to an unhandled error in a test marked with test.fail().
@@ -61,19 +53,19 @@ export class WorkerMain extends ProcessRunner {
// This promise resolves once the single "run test group" call finishes.
private _runFinished = new ManualPromise<void>();
private _currentTest: TestInfoImpl | null = null;
private _lastRunningTests: TestCase[] = [];
private _lastRunningTests: testNs.TestCase[] = [];
private _totalRunningTests = 0;
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
// These suites still need afterAll hooks to be executed for the proper cleanup.
// Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`.
private _activeSuites = new Map<Suite, TestAnnotation[]>();
private _activeSuites = new Map<testNs.Suite, TestAnnotation[]>();
private _resumePromise?: ManualPromise<ipc.ResumePayload>;
constructor(params: ipc.WorkerInitParams) {
super();
process.env.TEST_WORKER_INDEX = String(params.workerIndex);
process.env.TEST_PARALLEL_INDEX = String(params.parallelIndex);
setIsWorkerProcess();
globals.setIsWorkerProcess();
this._params = params;
this._fixtureRunner = new FixtureRunner();
@@ -86,7 +78,7 @@ export class WorkerMain extends ProcessRunner {
process.on('uncaughtException', error => this.unhandledError(error));
// eslint-disable-next-line no-restricted-properties
process.stdout.write = (chunk: string | Buffer, cb?: any) => {
this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
this.dispatchEvent('stdOut', ipc.stdioChunkToParams(chunk));
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
if (typeof cb === 'function')
process.nextTick(cb);
@@ -96,7 +88,7 @@ export class WorkerMain extends ProcessRunner {
if (!process.env.PW_RUNNER_DEBUG) {
// eslint-disable-next-line no-restricted-properties
process.stderr.write = (chunk: string | Buffer, cb?: any) => {
this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
this.dispatchEvent('stdErr', ipc.stdioChunkToParams(chunk));
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
if (typeof cb === 'function')
process.nextTick(cb);
@@ -207,13 +199,13 @@ export class WorkerMain extends ProcessRunner {
if (this._config)
return;
const config = await deserializeConfig(this._params.config);
const config = await configLoader.deserializeConfig(this._params.config);
const project = config.projects.find(p => p.id === this._params.projectId);
if (!project)
throw new Error(`Project "${this._params.projectId}" not found in the worker process. Make sure project name does not change.`);
this._config = config;
this._project = project;
this._poolBuilder = PoolBuilder.createForWorker(this._project);
this._poolBuilder = poolBuilder.PoolBuilder.createForWorker(this._project);
this._fixtureRunner.workerFixtureTimeout = this._project.project.timeout;
}
@@ -223,11 +215,11 @@ export class WorkerMain extends ProcessRunner {
let fatalUnknownTestIds: string[] | undefined;
try {
await this._loadIfNeeded();
const fileSuite = await loadTestFile(runPayload.file, this._config);
const suite = bindFileSuiteToProject(this._project, fileSuite);
const fileSuite = await testLoader.loadTestFile(runPayload.file, this._config);
const suite = suiteUtils.bindFileSuiteToProject(this._project, fileSuite);
if (this._params.repeatEachIndex)
applyRepeatEachIndex(this._project, suite, this._params.repeatEachIndex);
filterTestsRemoveEmptySuites(suite, test => entries.has(test.id));
suiteUtils.applyRepeatEachIndex(this._project, suite, this._params.repeatEachIndex);
suiteUtils.filterTestsRemoveEmptySuites(suite, test => entries.has(test.id));
const tests = suite.allTests();
// Collect test IDs that were not found in the worker
@@ -293,7 +285,7 @@ export class WorkerMain extends ProcessRunner {
this._resumePromise?.resolve(payload);
}
private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) {
private async _runTest(test: testNs.TestCase, retry: number, nextTest: testNs.TestCase | undefined) {
const testInfo = new TestInfoImpl(this._config, this._project, this._params, test, retry, {
onStepBegin: payload => this.dispatchEvent('stepBegin', payload),
onStepEnd: payload => this.dispatchEvent('stepEnd', payload),
@@ -340,7 +332,7 @@ export class WorkerMain extends ProcessRunner {
}
this._currentTest = testInfo;
setCurrentTestInfo(testInfo);
globals.setCurrentTestInfo(testInfo);
this.dispatchEvent('testBegin', buildTestBeginPayload(testInfo));
const isSkipped = testInfo.expectedStatus === 'skipped';
@@ -518,7 +510,7 @@ export class WorkerMain extends ProcessRunner {
testInfo.duration = (testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed) | 0;
this._currentTest = null;
setCurrentTestInfo(null);
globals.setCurrentTestInfo(null);
this.dispatchEvent('testEnd', buildTestEndPayload(testInfo));
const preserveOutput = this._config.config.preserveOutput === 'always' ||
@@ -527,7 +519,7 @@ export class WorkerMain extends ProcessRunner {
await removeFolders([testInfo.outputDir]);
}
private _collectHooksAndModifiers(suite: Suite, type: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', testInfo: TestInfoImpl) {
private _collectHooksAndModifiers(suite: testNs.Suite, type: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', testInfo: TestInfoImpl) {
type Runnable = { type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll' | 'fixme' | 'skip' | 'slow' | 'fail', fn: Function, title: string, location: Location };
const runnables: Runnable[] = [];
for (const modifier of suite._modifiers) {
@@ -538,7 +530,7 @@ export class WorkerMain extends ProcessRunner {
const result = await modifier.fn(fixtures);
testInfo._modifier(modifier.type, modifier.location, [!!result, modifier.description]);
};
inheritFixtureNames(modifier.fn, fn);
fixtures.inheritFixtureNames(modifier.fn, fn);
runnables.push({
title: `${modifier.type} modifier`,
location: modifier.location,
@@ -551,7 +543,7 @@ export class WorkerMain extends ProcessRunner {
return runnables;
}
private async _runBeforeAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
private async _runBeforeAllHooksForSuite(suite: testNs.Suite, testInfo: TestInfoImpl) {
if (this._activeSuites.has(suite))
return;
const extraAnnotations: TestAnnotation[] = [];
@@ -559,7 +551,7 @@ export class WorkerMain extends ProcessRunner {
await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations);
}
private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: TestAnnotation[]) {
private async _runAllHooksForSuite(suite: testNs.Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: TestAnnotation[]) {
// Always run all the hooks, and capture the first error.
let firstError: Error | undefined;
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
@@ -599,14 +591,14 @@ export class WorkerMain extends ProcessRunner {
throw firstError;
}
private async _runAfterAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
private async _runAfterAllHooksForSuite(suite: testNs.Suite, testInfo: TestInfoImpl) {
if (!this._activeSuites.has(suite))
return;
this._activeSuites.delete(suite);
await this._runAllHooksForSuite(suite, testInfo, 'afterAll');
}
private async _runEachHooksForSuites(suites: Suite[], type: 'beforeEach' | 'afterEach', testInfo: TestInfoImpl, slot?: TimeSlot) {
private async _runEachHooksForSuites(suites: testNs.Suite[], type: 'beforeEach' | 'afterEach', testInfo: TestInfoImpl, slot?: TimeSlot) {
// Always run all the hooks, unless one of the times out, and capture the first error.
let firstError: Error | undefined;
const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat();
@@ -652,15 +644,15 @@ function buildTestEndPayload(testInfo: TestInfoImpl): ipc.TestEndPayload {
};
}
function getSuites(test: TestCase | undefined): Suite[] {
const suites: Suite[] = [];
for (let suite: Suite | undefined = test?.parent; suite; suite = suite.parent)
function getSuites(test: testNs.TestCase | undefined): testNs.Suite[] {
const suites: testNs.Suite[] = [];
for (let suite: testNs.Suite | undefined = test?.parent; suite; suite = suite.parent)
suites.push(suite);
suites.reverse(); // Put root suite first.
return suites;
}
function formatTestTitle(test: TestCase, projectName: string) {
function formatTestTitle(test: testNs.TestCase, projectName: string) {
// file, ...describes, test
const [, ...titles] = test.titlePath();
const location = `${relativeFilePath(test.location.file)}:${test.location.line}:${test.location.column}`;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { startProcessRunner } from '../common/process';
import { startProcessRunner } from '../common';
import { create } from './workerMain';
startProcessRunner(create);
+8 -8
View File
@@ -41,9 +41,9 @@ test('should print dependencies in CJS mode', async ({ runInlineTest }) => {
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependenciesForTest } from 'playwright/lib/transform/compilationCache';
import { cc } from 'playwright/lib/common';
export default () => {
console.log('###' + JSON.stringify(fileDependenciesForTest()) + '###');
console.log('###' + JSON.stringify(cc.fileDependenciesForTest()) + '###');
};
`
}, {});
@@ -80,9 +80,9 @@ test('should print dependencies in ESM mode', async ({ runInlineTest }) => {
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependenciesForTest } from 'playwright/lib/transform/compilationCache';
import { cc } from 'playwright/lib/common';
export default () => {
console.log('###' + JSON.stringify(fileDependenciesForTest()) + '###');
console.log('###' + JSON.stringify(cc.fileDependenciesForTest()) + '###');
};
`
}, {});
@@ -119,9 +119,9 @@ test('should print dependencies in mixed CJS/ESM mode 1', async ({ runInlineTest
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependenciesForTest } from 'playwright/lib/transform/compilationCache';
import { cc } from 'playwright/lib/common';
export default () => {
console.log('###' + JSON.stringify(fileDependenciesForTest()) + '###');
console.log('###' + JSON.stringify(cc.fileDependenciesForTest()) + '###');
};
`
}, {});
@@ -157,9 +157,9 @@ test('should print dependencies in mixed CJS/ESM mode 2', async ({ runInlineTest
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependenciesForTest } from 'playwright/lib/transform/compilationCache';
import { cc } from 'playwright/lib/common';
export default () => {
console.log('###' + JSON.stringify(fileDependenciesForTest()) + '###');
console.log('###' + JSON.stringify(cc.fileDependenciesForTest()) + '###');
};
`
}, {});
+84 -6
View File
@@ -365,6 +365,10 @@ class EsbuildStep extends Step {
super({ concurrent: false });
this._options = options;
this._watchPaths = watchPaths;
// For bundled outputs we always want a metafile so we can emit a
// sidecar .bundle.txt report next to each output.
if (options.bundle && !options.metafile)
options.metafile = true;
}
/** @override */
@@ -374,7 +378,8 @@ class EsbuildStep extends Step {
} else {
console.log('==== Running esbuild:', this._relativeEntryPoints().join(', '));
const start = Date.now();
await build(this._options);
const result = await build(this._options);
this._writeBundleReport(result);
console.log('==== Done in', Date.now() - start, 'ms');
}
}
@@ -403,7 +408,9 @@ class EsbuildStep extends Step {
this._sourcesChanged = false;
this._rebuilding = true;
try {
await this._context?.rebuild();
const result = await this._context?.rebuild();
if (result)
this._writeBundleReport(result);
} catch (e) {
// Ignore. Esbuild inherits stderr and already logs nicely formatted errors
// before throwing.
@@ -413,6 +420,56 @@ class EsbuildStep extends Step {
} while (this._sourcesChanged);
}
/**
* @param {import('esbuild').BuildResult} result
*/
_writeBundleReport(result) {
if (!this._options.bundle || !result.metafile)
return;
const { outputs } = result.metafile;
for (const [outFile, outInfo] of Object.entries(outputs)) {
if (outFile.endsWith('.map'))
continue;
const inputs = Object.keys(outInfo.inputs)
.filter(p => !p.startsWith('(disabled):'))
.sort();
const externals = new Set();
for (const [, meta] of Object.entries(outInfo.inputs)) {
// imports field is per-input via metafile.inputs, not outputs.
}
for (const inFile of inputs) {
const meta = result.metafile.inputs[inFile];
if (!meta) continue;
for (const imp of meta.imports || []) {
if (!imp.external)
continue;
if (imp.path.startsWith('node:'))
continue;
if (require('module').isBuiltin?.(imp.path) || require('module').builtinModules.includes(imp.path))
continue;
externals.add(imp.path);
}
}
const sortedExternals = [...externals].sort();
const lines = [];
lines.push(`# ${path.relative(ROOT, outFile)}`);
lines.push(`# size: ${(outInfo.bytes / 1024).toFixed(1)} KB`);
lines.push('');
lines.push(`## Inlined (${inputs.length})`);
for (const f of inputs)
lines.push(` ${f}`);
lines.push('');
lines.push(`## External (${sortedExternals.length})`);
for (const e of sortedExternals)
lines.push(` ${e}`);
lines.push('');
const reportPath = outFile + '.txt';
fs.writeFileSync(reportPath, lines.join('\n'));
const rel = path.relative(ROOT, outFile);
console.log(` bundle: ${rel} (${inputs.length} files, ${sortedExternals.length} external, ${(outInfo.bytes / 1024).toFixed(1)} KB)`);
}
}
_relativeEntryPoints() {
return this._options.entryPoints.map(e => path.relative(ROOT, e));
}
@@ -689,8 +746,8 @@ steps.push(new CustomCallbackStep(assertCoreBundleHasNoNodeModules));
}, [playwrightSrc]));
}
// Build playwright entry points (per-file), excluding matchers/* which is
// produced by the bundle step below.
// Build playwright entry points (per-file), excluding matchers/* and
// common/* + transform/* — all of those are produced by bundle steps below.
steps.push(new EsbuildStep({
entryPoints: [
filePath('packages/playwright/src/*.ts'),
@@ -724,13 +781,34 @@ steps.push(new EsbuildStep({
external: [
'playwright-core',
'playwright-core/*',
'../common/*',
'../util',
'../globals',
'../package',
],
plugins: [dynamicImportToRequirePlugin],
}, [filePath('packages/playwright/src')]));
// playwright/lib/common/index.js — bundled common barrel. Inlines all of
// common/* and transform/* (the barrel re-exports both). Externalizes
// sibling lib/ files so state-holding singletons (currentTestInfo, config,
// compilation cache) live exactly once at runtime.
steps.push(new EsbuildStep({
bundle: true,
entryPoints: [filePath('packages/playwright/src/common/index.ts')],
outfile: filePath('packages/playwright/lib/common/index.js'),
sourcemap: withSourceMaps ? 'linked' : false,
platform: 'node',
format: 'cjs',
external: [
'playwright-core',
'playwright-core/*',
'../globals',
'../package',
'../utils',
'../matchers/expect',
],
plugins: [dynamicImportToRequirePlugin],
}, [filePath('packages/playwright/src')]));
// Build the Electron preload loader as a standalone CJS file. It runs inside
// the Electron process (via `electron -r loader.js`) and must not depend on
// coreBundle. `electron` is resolved at runtime by the Electron process.