playwright
Version:
A high-level API to automate web browsers
1,406 lines (1,398 loc) • 295 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// packages/playwright/src/runner/index.ts
var index_exports = {};
__export(index_exports, {
ListModeReporter: () => listModeReporter_default,
ListReporter: () => list_default,
TestServerConnection: () => TestServerConnection,
base: () => base_exports,
html: () => html_exports,
merge: () => merge_exports,
projectUtils: () => projectUtils_exports,
runnerReporters: () => reporters_exports,
testRunner: () => testRunner_exports,
testServer: () => testServer_exports,
watchMode: () => watchMode_exports,
webServer: () => webServer
});
module.exports = __toCommonJS(index_exports);
// packages/playwright/src/runner/testRunner.ts
var testRunner_exports = {};
__export(testRunner_exports, {
TestRunner: () => TestRunner,
TestRunnerEvent: () => TestRunnerEvent,
runAllTestsWithConfig: () => runAllTestsWithConfig
});
var import_events2 = __toESM(require("events"));
var import_fs13 = __toESM(require("fs"));
var import_path17 = __toESM(require("path"));
var import_coreBundle2 = require("playwright-core/lib/coreBundle");
var import_common9 = require("../common");
// packages/playwright/src/runner/fsWatcher.ts
var chokidar = require("playwright-core/lib/utilsBundle").chokidar;
var FSWatcher = class {
constructor(onChange) {
this._watchedPaths = [];
this._ignoredFolders = [];
this._collector = [];
this._onChange = onChange;
}
async update(watchedPaths, ignoredFolders, reportPending) {
if (JSON.stringify([this._watchedPaths, this._ignoredFolders]) === JSON.stringify([watchedPaths, ignoredFolders]))
return;
if (reportPending)
this._reportEventsIfAny();
this._watchedPaths = watchedPaths;
this._ignoredFolders = ignoredFolders;
void this._fsWatcher?.close();
this._fsWatcher = void 0;
this._collector.length = 0;
clearTimeout(this._throttleTimer);
this._throttleTimer = void 0;
if (!this._watchedPaths.length)
return;
const ignored = [...this._ignoredFolders, "**/node_modules/**"];
this._fsWatcher = chokidar.watch(watchedPaths, { ignoreInitial: true, ignored }).on("all", async (event, file) => {
if (this._throttleTimer)
clearTimeout(this._throttleTimer);
this._collector.push({ event, file });
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
});
await new Promise((resolve, reject) => this._fsWatcher.once("ready", resolve).once("error", reject));
}
async close() {
await this._fsWatcher?.close();
}
_reportEventsIfAny() {
if (this._collector.length)
this._onChange(this._collector.slice());
this._collector.length = 0;
}
};
// packages/playwright/src/isomorphic/teleReceiver.ts
var TeleReporterReceiver = class {
constructor(reporter, options = {}) {
this.isListing = false;
this._tests = /* @__PURE__ */ new Map();
this._rootSuite = new TeleSuite("", "root");
this._options = options;
this._reporter = reporter;
}
reset() {
this._rootSuite._entries = [];
this._tests.clear();
}
dispatch(message) {
const { method, params } = message;
if (method === "onConfigure") {
this._onConfigure(params.config);
return;
}
if (method === "onProject") {
this._onProject(params.project);
return;
}
if (method === "onBegin") {
this._onBegin();
return;
}
if (method === "onTestBegin") {
this._onTestBegin(params.testId, params.result);
return;
}
if (method === "onTestPaused") {
this._onTestPaused(params.testId, params.resultId, params.errors);
return;
}
if (method === "onTestEnd") {
this._onTestEnd(params.test, params.result);
return;
}
if (method === "onStepBegin") {
this._onStepBegin(params.testId, params.resultId, params.step);
return;
}
if (method === "onAttach") {
this._onAttach(params.testId, params.resultId, params.attachments);
return;
}
if (method === "onStepEnd") {
this._onStepEnd(params.testId, params.resultId, params.step);
return;
}
if (method === "onError") {
this._onError(params.error, params.workerInfo);
return;
}
if (method === "onStdIO") {
this._onStdIO(params.type, params.testId, params.resultId, params.data, params.isBase64);
return;
}
if (method === "onEnd")
return this._onEnd(params.result);
if (method === "onExit")
return this._onExit();
}
_onConfigure(config2) {
this._rootDir = config2.rootDir;
this._config = this._parseConfig(config2);
this._reporter.onConfigure?.(this._config);
}
_onProject(project) {
let projectSuite = this._options.mergeProjects ? this._rootSuite.suites.find((suite) => suite.project().name === project.name) : void 0;
if (!projectSuite) {
projectSuite = new TeleSuite(project.name, "project");
this._rootSuite._addSuite(projectSuite);
}
const parsed = this._parseProject(project);
projectSuite._project = parsed;
let index = -1;
if (this._options.mergeProjects)
index = this._config.projects.findIndex((p) => p.name === project.name);
if (index === -1)
this._config.projects.push(parsed);
else
this._config.projects[index] = parsed;
for (const suite of project.suites)
this._mergeSuiteInto(suite, projectSuite);
}
_onBegin() {
this._reporter.onBegin?.(this._rootSuite);
}
_onTestBegin(testId, payload) {
const test = this._tests.get(testId);
if (this._options.clearPreviousResultsWhenTestBegins)
test.results = [];
const testResult = test._createTestResult(payload.id);
testResult.retry = payload.retry;
testResult.workerIndex = payload.workerIndex;
testResult.parallelIndex = payload.parallelIndex;
testResult.setStartTimeNumber(payload.startTime);
this._reporter.onTestBegin?.(test, testResult);
}
_onTestPaused(testId, resultId, errors) {
const test = this._tests.get(testId);
const result = test.results.find((r) => r._id === resultId);
result.errors.push(...errors);
result.error = result.errors[0];
void this._reporter.onTestPaused?.(test, result);
}
_onTestEnd(testEndPayload, payload) {
const test = this._tests.get(testEndPayload.testId);
test.timeout = testEndPayload.timeout;
test.expectedStatus = testEndPayload.expectedStatus;
const result = test.results.find((r) => r._id === payload.id);
result.duration = payload.duration;
result.status = payload.status;
result.errors.push(...payload.errors ?? []);
result.error = result.errors[0];
if (!!payload.attachments)
result.attachments = this._parseAttachments(payload.attachments);
if (payload.annotations) {
this._absoluteAnnotationLocationsInplace(payload.annotations);
result.annotations = payload.annotations;
test.annotations = payload.annotations;
}
this._reporter.onTestEnd?.(test, result);
result._stepMap = /* @__PURE__ */ new Map();
}
_onStepBegin(testId, resultId, payload) {
const test = this._tests.get(testId);
const result = test.results.find((r) => r._id === resultId);
const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : void 0;
const location = this._absoluteLocation(payload.location);
const step = new TeleTestStep(payload, parentStep, location, result);
if (parentStep)
parentStep.steps.push(step);
else
result.steps.push(step);
result._stepMap.set(payload.id, step);
this._reporter.onStepBegin?.(test, result, step);
}
_onStepEnd(testId, resultId, payload) {
const test = this._tests.get(testId);
const result = test.results.find((r) => r._id === resultId);
const step = result._stepMap.get(payload.id);
step._endPayload = payload;
step.duration = payload.duration;
step.error = payload.error;
this._reporter.onStepEnd?.(test, result, step);
}
_onAttach(testId, resultId, attachments) {
const test = this._tests.get(testId);
const result = test.results.find((r) => r._id === resultId);
result.attachments.push(...attachments.map((a) => ({
name: a.name,
contentType: a.contentType,
path: a.path,
body: a.base64 && globalThis.Buffer ? Buffer.from(a.base64, "base64") : void 0
})));
}
_onError(error, workerInfo) {
let fullWorkerInfo;
if (workerInfo) {
const project = this._config.projects.find((p) => p.name === workerInfo.projectName);
if (project) {
fullWorkerInfo = {
workerIndex: workerInfo.workerIndex,
parallelIndex: workerInfo.parallelIndex,
config: this._config,
project
};
}
}
this._reporter.onError?.(error, fullWorkerInfo);
}
_onStdIO(type, testId, resultId, data, isBase64) {
const chunk = isBase64 ? globalThis.Buffer ? Buffer.from(data, "base64") : atob(data) : data;
const test = testId ? this._tests.get(testId) : void 0;
const result = test && resultId ? test.results.find((r) => r._id === resultId) : void 0;
if (type === "stdout") {
result?.stdout.push(chunk);
this._reporter.onStdOut?.(chunk, test, result);
} else {
result?.stderr.push(chunk);
this._reporter.onStdErr?.(chunk, test, result);
}
}
async _onEnd(result) {
await this._reporter.onEnd?.(asFullResult(result));
}
_onExit() {
return this._reporter.onExit?.();
}
_parseConfig(config2) {
const result = asFullConfig(config2);
if (this._options.configOverrides) {
result.configFile = this._options.configOverrides.configFile;
result.reportSlowTests = this._options.configOverrides.reportSlowTests;
result.quiet = this._options.configOverrides.quiet;
result.reporter = [...this._options.configOverrides.reporter];
}
return result;
}
_parseProject(project) {
return {
metadata: project.metadata,
name: project.name,
outputDir: this._absolutePath(project.outputDir),
repeatEach: project.repeatEach,
retries: project.retries,
testDir: this._absolutePath(project.testDir),
testIgnore: parseRegexPatterns(project.testIgnore),
testMatch: parseRegexPatterns(project.testMatch),
timeout: project.timeout,
grep: parseRegexPatterns(project.grep),
grepInvert: parseRegexPatterns(project.grepInvert),
dependencies: project.dependencies,
teardown: project.teardown,
snapshotDir: this._absolutePath(project.snapshotDir),
ignoreSnapshots: project.ignoreSnapshots ?? false,
use: project.use
};
}
_parseAttachments(attachments) {
return attachments.map((a) => {
return {
...a,
body: a.base64 && globalThis.Buffer ? Buffer.from(a.base64, "base64") : void 0
};
});
}
_mergeSuiteInto(jsonSuite, parent) {
let targetSuite = parent.suites.find((s) => s.title === jsonSuite.title);
if (!targetSuite) {
targetSuite = new TeleSuite(jsonSuite.title, parent.type === "project" ? "file" : "describe");
parent._addSuite(targetSuite);
}
targetSuite.location = this._absoluteLocation(jsonSuite.location);
jsonSuite.entries.forEach((e) => {
if ("testId" in e)
this._mergeTestInto(e, targetSuite);
else
this._mergeSuiteInto(e, targetSuite);
});
}
_mergeTestInto(jsonTest, parent) {
let targetTest = this._options.mergeTestCases ? parent.tests.find((s) => s.title === jsonTest.title && s.repeatEachIndex === jsonTest.repeatEachIndex) : void 0;
if (!targetTest) {
targetTest = new TeleTestCase(jsonTest.testId, jsonTest.title, this._absoluteLocation(jsonTest.location), jsonTest.repeatEachIndex);
parent._addTest(targetTest);
this._tests.set(targetTest.id, targetTest);
}
this._updateTest(jsonTest, targetTest);
}
_updateTest(payload, test) {
test.id = payload.testId;
test.location = this._absoluteLocation(payload.location);
test.retries = payload.retries;
test.tags = payload.tags ?? [];
test.annotations = payload.annotations ?? [];
this._absoluteAnnotationLocationsInplace(test.annotations);
return test;
}
_absoluteAnnotationLocationsInplace(annotations) {
for (const annotation of annotations) {
if (annotation.location)
annotation.location = this._absoluteLocation(annotation.location);
}
}
_absoluteLocation(location) {
if (!location)
return location;
return {
...location,
file: this._absolutePath(location.file)
};
}
_absolutePath(relativePath) {
if (relativePath === void 0)
return;
return this._options.resolvePath ? this._options.resolvePath(this._rootDir, relativePath) : this._rootDir + "/" + relativePath;
}
};
var TeleSuite = class {
constructor(title, type) {
this._entries = [];
this._requireFile = "";
this._parallelMode = "none";
this.title = title;
this._type = type;
}
get type() {
return this._type;
}
get suites() {
return this._entries.filter((e) => e.type !== "test");
}
get tests() {
return this._entries.filter((e) => e.type === "test");
}
entries() {
return this._entries;
}
allTests() {
const result = [];
const visit = (suite) => {
for (const entry of suite.entries()) {
if (entry.type === "test")
result.push(entry);
else
visit(entry);
}
};
visit(this);
return result;
}
titlePath() {
const titlePath = this.parent ? this.parent.titlePath() : [];
if (this.title || this._type !== "describe")
titlePath.push(this.title);
return titlePath;
}
project() {
return this._project ?? this.parent?.project();
}
_addTest(test) {
test.parent = this;
this._entries.push(test);
}
_addSuite(suite) {
suite.parent = this;
this._entries.push(suite);
}
};
var TeleTestCase = class {
constructor(id, title, location, repeatEachIndex) {
this.fn = () => {
};
this.results = [];
this.type = "test";
this.expectedStatus = "passed";
this.timeout = 0;
this.annotations = [];
this.retries = 0;
this.tags = [];
this.repeatEachIndex = 0;
this.id = id;
this.title = title;
this.location = location;
this.repeatEachIndex = repeatEachIndex;
}
titlePath() {
const titlePath = this.parent ? this.parent.titlePath() : [];
titlePath.push(this.title);
return titlePath;
}
outcome() {
return computeTestCaseOutcome(this);
}
ok() {
const status = this.outcome();
return status === "expected" || status === "flaky" || status === "skipped";
}
_createTestResult(id) {
const result = new TeleTestResult(this.results.length, id);
this.results.push(result);
return result;
}
};
var TeleTestStep = class {
constructor(payload, parentStep, location, result) {
this.duration = -1;
this.steps = [];
this._startTime = 0;
this.title = payload.title;
this.category = payload.category;
this.location = location;
this.parent = parentStep;
this._startTime = payload.startTime;
this._result = result;
}
titlePath() {
const parentPath = this.parent?.titlePath() || [];
return [...parentPath, this.title];
}
get startTime() {
return new Date(this._startTime);
}
set startTime(value) {
this._startTime = +value;
}
get attachments() {
return this._endPayload?.attachments?.map((index) => this._result.attachments[index]) ?? [];
}
get annotations() {
return this._endPayload?.annotations ?? [];
}
};
var TeleTestResult = class {
constructor(retry, id) {
this.parallelIndex = -1;
this.workerIndex = -1;
this.duration = -1;
this.stdout = [];
this.stderr = [];
this.attachments = [];
this.annotations = [];
this.status = "skipped";
this.steps = [];
this.errors = [];
this._stepMap = /* @__PURE__ */ new Map();
this._startTime = 0;
this.retry = retry;
this._id = id;
}
setStartTimeNumber(startTime) {
this._startTime = startTime;
}
get startTime() {
return new Date(this._startTime);
}
set startTime(value) {
this._startTime = +value;
}
};
var baseFullConfig = {
forbidOnly: false,
fullyParallel: false,
globalSetup: null,
globalTeardown: null,
globalTimeout: 0,
grep: /.*/,
grepInvert: null,
maxFailures: 0,
metadata: {},
preserveOutput: "always",
projects: [],
reporter: [[process.env.CI ? "dot" : "list"]],
reportSlowTests: {
max: 5,
threshold: 3e5
/* 5 minutes */
},
configFile: "",
rootDir: "",
quiet: false,
shard: null,
tags: [],
updateSnapshots: "missing",
updateSourceMethod: "patch",
version: "",
workers: 0,
webServer: null
};
function serializeRegexPatterns(patterns) {
if (!Array.isArray(patterns))
patterns = [patterns];
return patterns.map((s) => {
if (typeof s === "string")
return { s };
return { r: { source: s.source, flags: s.flags } };
});
}
function parseRegexPatterns(patterns) {
return patterns.map((p) => {
if (p.s !== void 0)
return p.s;
return new RegExp(p.r.source, p.r.flags);
});
}
function computeTestCaseOutcome(test) {
let skipped = 0;
let didNotRun = 0;
let expected = 0;
let interrupted = 0;
let unexpected = 0;
for (const result of test.results) {
if (result.status === "interrupted") {
++interrupted;
} else if (result.status === "skipped" && test.expectedStatus === "skipped") {
++skipped;
} else if (result.status === "skipped") {
++didNotRun;
} else if (result.status === test.expectedStatus) {
++expected;
} else {
++unexpected;
}
}
if (expected === 0 && unexpected === 0)
return "skipped";
if (unexpected === 0)
return "expected";
if (expected === 0 && skipped === 0)
return "unexpected";
return "flaky";
}
function asFullResult(result) {
return {
status: result.status,
startTime: new Date(result.startTime),
duration: result.duration
};
}
function asFullConfig(config2) {
return { ...baseFullConfig, ...config2 };
}
// packages/playwright/src/plugins/gitCommitInfoPlugin.ts
var fs = __toESM(require("fs"));
var { monotonicTime } = require("playwright-core/lib/coreBundle").iso;
var { spawnAsync } = require("playwright-core/lib/coreBundle").utils;
var GIT_OPERATIONS_TIMEOUT_MS = 3e3;
var addGitCommitInfoPlugin = (fullConfig) => {
fullConfig.plugins.push({ factory: gitCommitInfoPlugin.bind(null, fullConfig) });
};
function print(s, ...args) {
console.log("GitCommitInfo: " + s, ...args);
}
function debug(s, ...args) {
if (!process.env.DEBUG_GIT_COMMIT_INFO)
return;
print(s, ...args);
}
var gitCommitInfoPlugin = (fullConfig) => {
return {
name: "playwright:git-commit-info",
setup: async (config2, configDir) => {
const metadata = config2.metadata;
const ci = await ciInfo();
if (!metadata.ci && ci) {
debug("ci info", ci);
metadata.ci = ci;
}
if (fullConfig.captureGitInfo?.commit || fullConfig.captureGitInfo?.commit === void 0 && ci) {
const git = await gitCommitInfo(configDir).catch((e) => print("failed to get git commit info", e));
if (git) {
debug("commit info", git);
metadata.gitCommit = git;
}
}
if (fullConfig.captureGitInfo?.diff || fullConfig.captureGitInfo?.diff === void 0 && ci) {
const diffResult = await gitDiff(configDir, ci).catch((e) => print("failed to get git diff", e));
if (diffResult) {
debug(`diff length ${diffResult.length}`);
metadata.gitDiff = diffResult;
}
}
}
};
};
async function ciInfo() {
if (process.env.GITHUB_ACTIONS) {
let pr;
try {
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, "utf8"));
pr = { title: json.pull_request.title, number: json.pull_request.number, baseHash: json.pull_request.base.sha };
} catch {
}
return {
commitHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`,
commitHash: process.env.GITHUB_SHA,
prHref: pr ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${pr.number}` : void 0,
prTitle: pr?.title,
prBaseHash: pr?.baseHash,
buildHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
};
}
if (process.env.GITLAB_CI) {
return {
commitHref: `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`,
commitHash: process.env.CI_COMMIT_SHA,
buildHref: process.env.CI_JOB_URL,
branch: process.env.CI_COMMIT_REF_NAME
};
}
if (process.env.JENKINS_URL && process.env.BUILD_URL) {
return {
commitHref: process.env.BUILD_URL,
commitHash: process.env.GIT_COMMIT,
branch: process.env.GIT_BRANCH
};
}
}
async function gitCommitInfo(gitDir) {
const separator2 = `---786eec917292---`;
const tokens = [
"%H",
// commit hash
"%h",
// abbreviated commit hash
"%s",
// subject
"%B",
// raw body (unwrapped subject and body)
"%an",
// author name
"%ae",
// author email
"%at",
// author date, UNIX timestamp
"%cn",
// committer name
"%ce",
// committer email
"%ct",
// committer date, UNIX timestamp
""
// branch
];
const output = await runGit(`git log -1 --pretty=format:"${tokens.join(separator2)}" && git rev-parse --abbrev-ref HEAD`, gitDir);
if (!output)
return void 0;
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = output.split(separator2);
return {
shortHash,
hash,
subject,
body,
author: {
name: authorName,
email: authorEmail,
time: +authorTime * 1e3
},
committer: {
name: committerName,
email: committerEmail,
time: +committerTime * 1e3
},
branch: branch.trim()
};
}
async function gitDiff(gitDir, ci) {
const diffLimit = 1e5;
if (ci?.prBaseHash) {
await runGit(`git fetch origin ${ci.prBaseHash} --depth=1 --no-auto-maintenance --no-auto-gc --no-tags --no-recurse-submodules`, gitDir);
const diff3 = await runGit(`git diff ${ci.prBaseHash} HEAD`, gitDir);
if (diff3)
return diff3.substring(0, diffLimit);
}
if (ci)
return;
const uncommitted = await runGit("git diff", gitDir);
if (uncommitted === void 0) {
return;
}
if (uncommitted)
return uncommitted.substring(0, diffLimit);
const diff2 = await runGit("git diff HEAD~1", gitDir);
return diff2?.substring(0, diffLimit);
}
async function runGit(command, cwd) {
debug(`running "${command}"`);
const start = monotonicTime();
const result = await spawnAsync(
command,
[],
{ stdio: "pipe", cwd, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
);
if (monotonicTime() - start > GIT_OPERATIONS_TIMEOUT_MS) {
print(`timeout of ${GIT_OPERATIONS_TIMEOUT_MS}ms exceeded while running "${command}"`);
return;
}
if (result.code)
debug(`failure, code=${result.code}
${result.stderr}`);
else
debug(`success`);
return result.code ? void 0 : result.stdout.trim();
}
// packages/playwright/src/plugins/webServerPlugin.ts
var import_net = __toESM(require("net"));
var import_path = __toESM(require("path"));
var colors = require("playwright-core/lib/utilsBundle").colors;
var debug2 = require("playwright-core/lib/utilsBundle").debug;
var { ManualPromise } = require("playwright-core/lib/coreBundle").iso;
var { monotonicTime: monotonicTime2 } = require("playwright-core/lib/coreBundle").iso;
var { raceAgainstDeadline } = require("playwright-core/lib/coreBundle").iso;
var { isURLAvailable } = require("playwright-core/lib/coreBundle").utils;
var { launchProcess } = require("playwright-core/lib/coreBundle").utils;
var DEFAULT_ENVIRONMENT_VARIABLES = {
"BROWSER": "none",
// Disable that create-react-app will open the page in the browser
"FORCE_COLOR": "1",
"DEBUG_COLORS": "1"
};
var debugWebServer = debug2("pw:webserver");
var WebServerPlugin = class {
constructor(options, checkPortOnly) {
this.name = "playwright:webserver";
this._options = options;
this._checkPortOnly = checkPortOnly;
}
async setup(config2, configDir, reporter) {
this._reporter = reporter;
if (this._options.url)
this._isAvailableCallback = getIsAvailableFunction(this._options.url, this._checkPortOnly, !!this._options.ignoreHTTPSErrors, this._reporter.onStdErr?.bind(this._reporter));
this._options.cwd = this._options.cwd ? import_path.default.resolve(configDir, this._options.cwd) : configDir;
try {
await this._startProcess();
await this._waitForProcess();
} catch (error) {
await this.teardown();
throw error;
}
}
async teardown() {
debugWebServer(`Terminating the WebServer`);
await this._killProcess?.();
debugWebServer(`Terminated the WebServer`);
}
async _startProcess() {
let processExitedReject = (error) => {
};
this._processExitedPromise = new Promise((_, reject) => processExitedReject = reject);
const isAlreadyAvailable = await this._isAvailableCallback?.();
if (isAlreadyAvailable) {
debugWebServer(`WebServer is already available`);
if (this._options.reuseExistingServer)
return;
const port = new URL(this._options.url).port;
throw new Error(`${this._options.url ?? `http://localhost${port ? ":" + port : ""}`} is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer.`);
}
if (!this._options.command)
throw new Error("config.webServer.command cannot be empty");
debugWebServer(`Starting WebServer process ${this._options.command}...`);
const { launchedProcess, gracefullyClose } = await launchProcess({
command: this._options.command,
env: {
...DEFAULT_ENVIRONMENT_VARIABLES,
...process.env,
...this._options.env
},
cwd: this._options.cwd,
stdio: "stdin",
shell: true,
attemptToGracefullyClose: async () => {
if (process.platform === "win32")
throw new Error("Graceful shutdown is not supported on Windows");
if (!this._options.gracefulShutdown)
throw new Error("skip graceful shutdown");
const { signal, timeout = 0 } = this._options.gracefulShutdown;
process.kill(-launchedProcess.pid, signal);
return new Promise((resolve, reject) => {
const timer = timeout !== 0 ? setTimeout(() => reject(new Error(`process didn't close gracefully within timeout`)), timeout) : void 0;
launchedProcess.once("close", (...args) => {
clearTimeout(timer);
resolve();
});
});
},
log: () => {
},
onExit: (code) => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : "Process from config.webServer exited early.")),
tempDirectories: []
});
this._killProcess = gracefullyClose;
debugWebServer(`Process started`);
if (this._options.wait?.stdout || this._options.wait?.stderr)
this._waitForStdioPromise = new ManualPromise();
const stdioWaitCollectors = {
stdout: this._options.wait?.stdout ? "" : void 0,
stderr: this._options.wait?.stderr ? "" : void 0
};
launchedProcess.stdout.on("data", (data) => {
if (debugWebServer.enabled || this._options.stdout === "pipe")
this._reporter.onStdOut?.(prefixOutputLines(data.toString(), this._options.name));
});
launchedProcess.stderr.on("data", (data) => {
if (debugWebServer.enabled || (this._options.stderr === "pipe" || !this._options.stderr))
this._reporter.onStdErr?.(prefixOutputLines(data.toString(), this._options.name));
});
const resolveStdioPromise = () => {
stdioWaitCollectors.stdout = void 0;
stdioWaitCollectors.stderr = void 0;
this._waitForStdioPromise?.resolve();
};
for (const stdio of ["stdout", "stderr"]) {
launchedProcess[stdio].on("data", (data) => {
if (!this._options.wait?.[stdio] || stdioWaitCollectors[stdio] === void 0)
return;
stdioWaitCollectors[stdio] += data.toString();
this._options.wait[stdio].lastIndex = 0;
const result = this._options.wait[stdio].exec(stdioWaitCollectors[stdio]);
if (result) {
for (const [key, value] of Object.entries(result.groups || {}))
process.env[key.toUpperCase()] = value;
resolveStdioPromise();
}
});
}
}
async _waitForProcess() {
if (!this._isAvailableCallback && !this._waitForStdioPromise) {
this._processExitedPromise.catch(() => {
});
return;
}
debugWebServer(`Waiting for availability...`);
const launchTimeout = this._options.timeout || 60 * 1e3;
const cancellationToken = { canceled: false };
const deadline = monotonicTime2() + launchTimeout;
const racingPromises = [this._processExitedPromise];
if (this._isAvailableCallback)
racingPromises.push(raceAgainstDeadline(() => waitFor(this._isAvailableCallback, cancellationToken), deadline));
if (this._waitForStdioPromise)
racingPromises.push(raceAgainstDeadline(() => this._waitForStdioPromise, deadline));
const { timedOut } = await Promise.race(racingPromises);
cancellationToken.canceled = true;
if (timedOut)
throw new Error(`Timed out waiting ${launchTimeout}ms from config.webServer.`);
debugWebServer(`WebServer available`);
}
};
async function isPortUsed(port) {
const innerIsPortUsed = (host) => new Promise((resolve) => {
const conn = import_net.default.connect(port, host).on("error", () => {
resolve(false);
}).on("connect", () => {
conn.end();
resolve(true);
});
});
return new Promise((resolve) => {
let pending = 2;
const onResult = (result) => {
if (result)
resolve(true);
else if (--pending === 0)
resolve(false);
};
void innerIsPortUsed("127.0.0.1").then(onResult);
void innerIsPortUsed("::1").then(onResult);
});
}
async function waitFor(waitFn, cancellationToken) {
const logScale = [100, 250, 500];
while (!cancellationToken.canceled) {
const connected = await waitFn();
if (connected)
return;
const delay = logScale.shift() || 1e3;
debugWebServer(`Waiting ${delay}ms`);
await new Promise((x) => setTimeout(x, delay));
}
}
function getIsAvailableFunction(url, checkPortOnly, ignoreHTTPSErrors, onStdErr) {
const urlObject = new URL(url);
if (!checkPortOnly)
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, debugWebServer, onStdErr);
const port = urlObject.port;
return () => isPortUsed(+port);
}
var webServer = (options) => {
return new WebServerPlugin(options, false);
};
var webServerPluginsForConfig = (config2) => {
const shouldSetBaseUrl = !!config2.config.webServer;
const webServerPlugins = [];
for (const webServerConfig of config2.webServers) {
if (webServerConfig.port && webServerConfig.url)
throw new Error(`Either 'port' or 'url' should be specified in config.webServer.`);
let url;
if (webServerConfig.port || webServerConfig.url) {
url = webServerConfig.url || `http://localhost:${webServerConfig.port}`;
if (shouldSetBaseUrl && !webServerConfig.url)
process.env.PLAYWRIGHT_TEST_BASE_URL = url;
}
webServerPlugins.push(new WebServerPlugin({ ...webServerConfig, url }, webServerConfig.port !== void 0));
}
return webServerPlugins;
};
function prefixOutputLines(output, prefixName = "WebServer") {
const lastIsNewLine = output[output.length - 1] === "\n";
let lines = output.split("\n");
if (lastIsNewLine)
lines.pop();
lines = lines.map((line) => colors.dim(`[${prefixName}] `) + line);
if (lastIsNewLine)
lines.push("");
return lines.join("\n");
}
// packages/playwright/src/reporters/base.ts
var base_exports = {};
__export(base_exports, {
TerminalReporter: () => TerminalReporter,
formatError: () => formatError,
formatFailure: () => formatFailure,
formatResultFailure: () => formatResultFailure,
formatRetry: () => formatRetry,
internalScreen: () => internalScreen,
kOutputSymbol: () => kOutputSymbol,
markErrorsAsReported: () => markErrorsAsReported,
nonTerminalScreen: () => nonTerminalScreen,
prepareErrorStack: () => prepareErrorStack,
relativeFilePath: () => relativeFilePath,
resolveOutputFile: () => resolveOutputFile,
separator: () => separator,
stepSuffix: () => stepSuffix,
terminalScreen: () => terminalScreen
});
var import_path2 = __toESM(require("path"));
var import_util = require("../util");
var realColors = require("playwright-core/lib/utilsBundle").colors;
var { noColors } = require("playwright-core/lib/coreBundle").iso;
var { msToString } = require("playwright-core/lib/coreBundle").iso;
var { parseErrorStack } = require("playwright-core/lib/coreBundle").iso;
var { getPackageManagerExecCommand } = require("playwright-core/lib/coreBundle").utils;
var { fitToWidth } = require("playwright-core/lib/coreBundle").utils;
var kOutputSymbol = Symbol("output");
var DEFAULT_TTY_WIDTH = 100;
var DEFAULT_TTY_HEIGHT = 40;
var originalProcessStdout = process.stdout;
var originalProcessStderr = process.stderr;
var terminalScreen = (() => {
let isTTY = !!originalProcessStdout.isTTY;
let ttyWidth = originalProcessStdout.columns || 0;
let ttyHeight = originalProcessStdout.rows || 0;
if (process.env.PLAYWRIGHT_FORCE_TTY === "false" || process.env.PLAYWRIGHT_FORCE_TTY === "0") {
isTTY = false;
ttyWidth = 0;
ttyHeight = 0;
} else if (process.env.PLAYWRIGHT_FORCE_TTY === "true" || process.env.PLAYWRIGHT_FORCE_TTY === "1") {
isTTY = true;
ttyWidth = originalProcessStdout.columns || DEFAULT_TTY_WIDTH;
ttyHeight = originalProcessStdout.rows || DEFAULT_TTY_HEIGHT;
} else if (process.env.PLAYWRIGHT_FORCE_TTY) {
isTTY = true;
const sizeMatch = process.env.PLAYWRIGHT_FORCE_TTY.match(/^(\d+)x(\d+)$/);
if (sizeMatch) {
ttyWidth = +sizeMatch[1];
ttyHeight = +sizeMatch[2];
} else {
ttyWidth = +process.env.PLAYWRIGHT_FORCE_TTY;
ttyHeight = DEFAULT_TTY_HEIGHT;
}
if (isNaN(ttyWidth))
ttyWidth = DEFAULT_TTY_WIDTH;
if (isNaN(ttyHeight))
ttyHeight = DEFAULT_TTY_HEIGHT;
}
let useColors = isTTY;
if (process.env.DEBUG_COLORS === "0" || process.env.DEBUG_COLORS === "false" || process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false")
useColors = false;
else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR)
useColors = true;
const colors7 = useColors ? realColors : noColors;
return {
resolveFiles: "cwd",
isTTY,
ttyWidth,
ttyHeight,
colors: colors7,
stdout: originalProcessStdout,
stderr: originalProcessStderr
};
})();
var nonTerminalScreen = {
colors: terminalScreen.colors,
isTTY: false,
ttyWidth: 0,
ttyHeight: 0,
resolveFiles: "rootDir"
};
var internalScreen = {
colors: realColors,
isTTY: false,
ttyWidth: 0,
ttyHeight: 0,
resolveFiles: "rootDir"
};
var TerminalReporter = class {
constructor(options = {}) {
this.totalTestCount = 0;
this.fileDurations = /* @__PURE__ */ new Map();
this._fatalErrors = [];
this._failureCount = 0;
this.screen = options.screen ?? terminalScreen;
this._options = options;
}
version() {
return "v2";
}
onConfigure(config2) {
this.config = config2;
}
onBegin(suite) {
this.suite = suite;
this.totalTestCount = suite.allTests().length;
}
onStdOut(chunk, test, result) {
this._appendOutput({ chunk, type: "stdout" }, result);
}
onStdErr(chunk, test, result) {
this._appendOutput({ chunk, type: "stderr" }, result);
}
_appendOutput(output, result) {
if (!result)
return;
result[kOutputSymbol] = result[kOutputSymbol] || [];
result[kOutputSymbol].push(output);
}
onTestEnd(test, result) {
if (result.status !== "skipped" && result.status !== test.expectedStatus)
++this._failureCount;
const projectName = test.titlePath()[1];
const relativePath = relativeTestPath(this.screen, this.config, test);
const fileAndProject = (projectName ? `[${projectName}] \u203A ` : "") + relativePath;
const entry = this.fileDurations.get(fileAndProject) || { duration: 0, workers: /* @__PURE__ */ new Set() };
entry.duration += result.duration;
entry.workers.add(result.workerIndex);
this.fileDurations.set(fileAndProject, entry);
}
onError(error) {
this._fatalErrors.push(error);
}
async onEnd(result) {
this.result = result;
}
fitToScreen(line, prefix) {
if (!this.screen.ttyWidth) {
return line;
}
return fitToWidth(line, this.screen.ttyWidth, prefix);
}
generateStartingMessage() {
const jobs = this.config.metadata.actualWorkers ?? this.config.workers;
const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : "";
if (!this.totalTestCount)
return "";
return "\n" + this.screen.colors.dim("Running ") + this.totalTestCount + this.screen.colors.dim(` test${this.totalTestCount !== 1 ? "s" : ""} using `) + jobs + this.screen.colors.dim(` worker${jobs !== 1 ? "s" : ""}${shardDetails}`);
}
getSlowTests() {
if (!this.config.reportSlowTests)
return [];
const fileDurations = [...this.fileDurations.entries()].filter(([key, value]) => value.workers.size === 1).map(([key, value]) => [key, value.duration]);
fileDurations.sort((a, b) => b[1] - a[1]);
const count = Math.min(fileDurations.length, this.config.reportSlowTests.max || Number.POSITIVE_INFINITY);
const threshold = this.config.reportSlowTests.threshold;
return fileDurations.filter(([, duration]) => duration > threshold).slice(0, count);
}
generateSummaryMessage({ didNotRun, skipped, expected, interrupted, unexpected, flaky, fatalErrors }) {
const tokens = [];
if (unexpected.length) {
tokens.push(this.screen.colors.red(` ${unexpected.length} failed`));
for (const test of unexpected)
tokens.push(this.screen.colors.red(this.formatTestHeader(test, { indent: " " })));
}
if (interrupted.length) {
tokens.push(this.screen.colors.yellow(` ${interrupted.length} interrupted`));
for (const test of interrupted)
tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
}
if (flaky.length) {
tokens.push(this.screen.colors.yellow(` ${flaky.length} flaky`));
for (const test of flaky)
tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
}
if (skipped)
tokens.push(this.screen.colors.yellow(` ${skipped} skipped`));
if (didNotRun)
tokens.push(this.screen.colors.yellow(` ${didNotRun} did not run`));
if (expected)
tokens.push(this.screen.colors.green(` ${expected} passed`) + this.screen.colors.dim(` (${msToString(this.result.duration)})`));
if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0)
tokens.push(this.screen.colors.red(` ${fatalErrors.length === 1 ? "1 error was not a part of any test" : fatalErrors.length + " errors were not a part of any test"}, see above for details`));
return tokens.join("\n");
}
generateSummary() {
let didNotRun = 0;
let skipped = 0;
let expected = 0;
const interrupted = [];
const interruptedToPrint = [];
const unexpected = [];
const flaky = [];
this.suite.allTests().forEach((test) => {
switch (test.outcome()) {
case "skipped": {
if (test.results.some((result) => result.status === "interrupted")) {
if (test.results.some((result) => !!result.error))
interruptedToPrint.push(test);
interrupted.push(test);
} else if (!test.results.length || test.expectedStatus !== "skipped") {
++didNotRun;
} else {
++skipped;
}
break;
}
case "expected":
++expected;
break;
case "unexpected":
unexpected.push(test);
break;
case "flaky":
flaky.push(test);
break;
}
});
const failuresToPrint = [...unexpected, ...flaky, ...interruptedToPrint];
return {
didNotRun,
skipped,
expected,
interrupted,
unexpected,
flaky,
failuresToPrint,
fatalErrors: this._fatalErrors
};
}
epilogue(full) {
const summary = this.generateSummary();
const summaryMessage = this.generateSummaryMessage(summary);
if (full && summary.failuresToPrint.length && !this._options.omitFailures)
this._printFailures(summary.failuresToPrint);
this._printSlowTests();
this._printSummary(summaryMessage);
}
_printFailures(failures) {
this.writeLine("");
failures.forEach((test, index) => {
this.writeLine(this.formatFailure(test, index + 1));
});
}
_printSlowTests() {
const slowTests = this.getSlowTests();
slowTests.forEach(([file, duration]) => {
this.writeLine(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${msToString(duration)})`));
});
if (slowTests.length)
this.writeLine(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
}
_printSummary(summary) {
if (summary.trim())
this.writeLine(summary);
}
willRetry(test) {
return test.outcome() === "unexpected" && test.results.length <= test.retries;
}
formatTestTitle(test, step) {
return formatTestTitle(this.screen, this.config, test, step, this._options);
}
formatTestHeader(test, options = {}) {
return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId });
}
formatFailure(test, index) {
return formatFailure(this.screen, this.config, test, index, this._options);
}
formatError(error) {
return formatError(this.screen, error);
}
formatResultErrors(test, result) {
return formatResultErrors(this.screen, test, result);
}
writeLine(line) {
this.screen.stdout?.write(line ? line + "\n" : "\n");
}
};
function formatResultErrors(screen, test, result) {
const lines = [];
if (test.outcome() === "unexpected") {
const errorDetails = formatResultFailure(screen, test, result, " ");
if (errorDetails.length > 0)
lines.push("");
for (const error of errorDetails)
lines.push(error.message, "");
}
return lines.join("\n");
}
function formatFailure(screen, config2, test, index, options) {
const lines = [];
let printedHeader = false;
for (const result of test.results) {
const resultLines = [];
const errors = formatResultFailure(screen, test, result, " ");
if (!errors.length)
continue;
if (!printedHeader) {
const header = formatTestHeader(screen, config2, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
lines.push(screen.colors.red(header));
printedHeader = true;
}
if (result.retry) {
resultLines.push("");
resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
}
resultLines.push(...errors.map((error) => "\n" + error.message));
const attachmentGroups = groupAttachments(result.attachments);
for (let i = 0; i < attachmentGroups.length; ++i) {
const attachment = attachmentGroups[i];
if (attachment.name === "error-context" && attachment.path) {
resultLines.push("");
resultLines.push(screen.colors.dim(` Error Context: ${relativeFilePath(screen, config2, attachment.path)}`));
continue;
}
if (attachment.name.startsWith("_"))
continue;
const hasPrintableContent = attachment.contentType.startsWith("text/");
if (!attachment.path && !hasPrintableContent)
continue;
resultLines.push("");
resultLines.push(screen.colors.dim(separator(screen, ` attachment #${i + 1}: ${screen.colors.bold(attachment.name)} (${attachment.contentType})`)));
if (attachment.actual?.path) {
if (attachment.expected?.path) {
const expectedPath = relativeFilePath(screen, config2, attachment.expected.path);
resultLines.push(screen.colors.dim(` Expected: ${expectedPath}`));
}
const actualPath = relativeFilePath(screen, config2, attachment.actual.path);
resultLines.push(screen.colors.dim(` Received: ${actualPath}`));
if (attachment.previous?.path) {
const previousPath = relativeFilePath(screen, config2, attachment.previous.path);
resultLines.push(screen.colors.dim(` Previous: ${previousPath}`));
}
if (attachment.diff?.path) {
const diffPath = relativeFilePath(screen, config2, attachment.diff.path);
resultLines.push(screen.colors.dim(` Diff: ${diffPath}`));
}
} else if (attachment.path) {
const relativePath = relativeFilePath(screen, config2, attachment.path);
resultLines.push(screen.colors.dim(` ${relativePath}`));
if (attachment.name === "trace") {
const packageManagerCommand = getPackageManagerExecCommand();
resultLines.push(screen.colors.dim(` Usage:`));
resultLines.push("");
resultLines.push(screen.colors.dim(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
resultLines.push("");
}
} else {
if (attachment.contentType.startsWith("text/") && attachment.body) {
let text = attachment.body.toString();
if (text.length > 300)
text = text.slice(0, 300) + "...";
for (const line of text.split("\n"))
resultLines.push(screen.colors.dim(` ${line}`));
}
}
resultLines.push(screen.colors.dim(separator(screen, " ")));
}
lines.push(...resultLines);
}
lines.push("");
return lines.join("\n");
}
function formatRetry(screen, result) {
const retryLines = [];
if (result.retry) {
retryLines.push("");
retryLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
}
return retryLines;
}
function quotePathIfNeeded(path20) {
if (/\s/.test(path20))
return `"${path20}"`;
return path20;
}
var kReportedSymbol = Symbol("reported");
function markErrorsAsReported(result) {
result[kReportedSymbol] = result.errors.length;
}
function formatResultFailure(screen, test, result, initialIndent) {
const errorDetails = [];
if (result.status === "passed" && test.expectedStatus === "failed") {
errorDetails.push({
message: indent(screen.colors.red(`Expected to fail, but passed.`), initialIndent)
});
}
if (result.status === "interrupted") {
errorDetails.push({
message: indent(screen.colors.red(`Test was interrupted.`), initialIndent)
});
}
const reportedIndex = result[kReportedSymbol] || 0;
for (const error of result.errors.slice(reportedIndex)) {
const formattedError = formatError(screen, error);
errorDetails.push({
message: indent(formattedError.message, initialIndent),
location: formattedError.location
});
}
return errorDetails;
}
function relativeFilePath(screen, config2, file) {
if (screen.resolveFiles === "cwd")
return import_path2.default.relative(process.cwd(), file);
return import_path2.default.relative(config2.rootDir, file);
}
function relativeTestPath(screen, config2, test) {
return relativeFilePath(screen, config2, test.location.file);
}
function stepSuffix(step) {
const stepTitles = step ? step.titlePath() : [];
return stepTitles.map((t2) => t2.split("\n")[0]).map((t2) => " \u203A " + t2).join("");
}
function formatTestTitle(screen, config2, test, step, options = {}) {
const [, projectName, , ...titles] = test.titlePath();
const location = `${relativeTestPath(screen, config2, test)}:${test.location.line}:${test.location.column}`;
const testId = options.includeTestId ? `[id=${test.id}] ` : "";
const