mirror of
https://github.com/hanzoai/playwright.git
synced 2026-09-24 17:01:53 +00:00
chore: bundle common (#40120)
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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'});`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
@utils/**
|
||||
../util.ts
|
||||
../transform
|
||||
../isomorphic/teleReceiver.ts
|
||||
../isomorphic
|
||||
../globals.ts
|
||||
../package.ts
|
||||
|
||||
[testType.ts]
|
||||
../matchers/expect.ts
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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` }],
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
[*]
|
||||
@isomorphic/**
|
||||
@utils/**
|
||||
../globals.ts
|
||||
../common/
|
||||
../transform/
|
||||
../util.ts
|
||||
../matchers/**
|
||||
../isomorphic/util.ts
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user