Files
2025-08-28 19:35:28 -07:00

612 lines
23 KiB
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);
var base_exports = {};
__export(base_exports, {
TerminalReporter: () => TerminalReporter,
fitToWidth: () => fitToWidth,
formatError: () => formatError,
formatFailure: () => formatFailure,
formatResultFailure: () => formatResultFailure,
formatRetry: () => formatRetry,
internalScreen: () => internalScreen,
kOutputSymbol: () => kOutputSymbol,
nonTerminalScreen: () => nonTerminalScreen,
prepareErrorStack: () => prepareErrorStack,
relativeFilePath: () => relativeFilePath,
resolveOutputFile: () => resolveOutputFile,
separator: () => separator,
stepSuffix: () => stepSuffix,
terminalScreen: () => terminalScreen
});
module.exports = __toCommonJS(base_exports);
var import_path = __toESM(require("path"));
var import_utils = require("playwright-core/lib/utils");
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_utils2 = require("playwright-core/lib/utils");
var import_util = require("../util");
var import_utilsBundle2 = require("../utilsBundle");
const kOutputSymbol = Symbol("output");
const DEFAULT_TTY_WIDTH = 100;
const DEFAULT_TTY_HEIGHT = 40;
const originalProcessStdout = process.stdout;
const originalProcessStderr = process.stderr;
const 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 colors = useColors ? import_utils2.colors : import_utils2.noColors;
return {
resolveFiles: "cwd",
isTTY,
ttyWidth,
ttyHeight,
colors,
stdout: originalProcessStdout,
stderr: originalProcessStderr
};
})();
const nonTerminalScreen = {
colors: terminalScreen.colors,
isTTY: false,
ttyWidth: 0,
ttyHeight: 0,
resolveFiles: "rootDir"
};
const internalScreen = {
colors: import_utils2.colors,
isTTY: false,
ttyWidth: 0,
ttyHeight: 0,
resolveFiles: "rootDir"
};
class TerminalReporter {
constructor(options = {}) {
this.totalTestCount = 0;
this.fileDurations = /* @__PURE__ */ new Map();
this._fatalErrors = [];
this._failureCount = 0;
this.screen = options.screen ?? terminalScreen;
this._omitFailures = options.omitFailures || false;
}
version() {
return "v2";
}
onConfigure(config) {
this.config = config;
}
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(` (${(0, import_utilsBundle.ms)(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._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(` (${(0, import_utilsBundle.ms)(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, omitLocation = false) {
return formatTestTitle(this.screen, this.config, test, step, omitLocation);
}
formatTestHeader(test, options = {}) {
return formatTestHeader(this.screen, this.config, test, options);
}
formatFailure(test, index) {
return formatFailure(this.screen, this.config, test, index);
}
formatError(error) {
return formatError(this.screen, error);
}
writeLine(line) {
this.screen.stdout?.write(line ? line + "\n" : "\n");
}
}
function formatFailure(screen, config, test, index) {
const lines = [];
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
lines.push(screen.colors.red(header));
for (const result of test.results) {
const resultLines = [];
const errors = formatResultFailure(screen, test, result, " ");
if (!errors.length)
continue;
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, config, 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, config, attachment.expected.path);
resultLines.push(screen.colors.dim(` Expected: ${expectedPath}`));
}
const actualPath = relativeFilePath(screen, config, attachment.actual.path);
resultLines.push(screen.colors.dim(` Received: ${actualPath}`));
if (attachment.previous?.path) {
const previousPath = relativeFilePath(screen, config, attachment.previous.path);
resultLines.push(screen.colors.dim(` Previous: ${previousPath}`));
}
if (attachment.diff?.path) {
const diffPath = relativeFilePath(screen, config, attachment.diff.path);
resultLines.push(screen.colors.dim(` Diff: ${diffPath}`));
}
} else if (attachment.path) {
const relativePath = relativeFilePath(screen, config, attachment.path);
resultLines.push(screen.colors.dim(` ${relativePath}`));
if (attachment.name === "trace") {
const packageManagerCommand = (0, import_utils.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(path2) {
if (/\s/.test(path2))
return `"${path2}"`;
return path2;
}
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)
});
}
for (const error of result.errors) {
const formattedError = formatError(screen, error);
errorDetails.push({
message: indent(formattedError.message, initialIndent),
location: formattedError.location
});
}
return errorDetails;
}
function relativeFilePath(screen, config, file) {
if (screen.resolveFiles === "cwd")
return import_path.default.relative(process.cwd(), file);
return import_path.default.relative(config.rootDir, file);
}
function relativeTestPath(screen, config, test) {
return relativeFilePath(screen, config, test.location.file);
}
function stepSuffix(step) {
const stepTitles = step ? step.titlePath() : [];
return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
}
function formatTestTitle(screen, config, test, step, omitLocation = false) {
const [, projectName, , ...titles] = test.titlePath();
let location;
if (omitLocation)
location = `${relativeTestPath(screen, config, test)}`;
else
location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
const testTitle = `${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
const extraTags = test.tags.filter((t) => !testTitle.includes(t));
return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
}
function formatTestHeader(screen, config, test, options = {}) {
const title = formatTestTitle(screen, config, test);
const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
let fullHeader = header;
if (options.mode === "error") {
const stepPaths = /* @__PURE__ */ new Set();
for (const result of test.results.filter((r) => !!r.errors.length)) {
const stepPath = [];
const visit = (steps) => {
const errors = steps.filter((s) => s.error);
if (errors.length > 1)
return;
if (errors.length === 1 && errors[0].category === "test.step") {
stepPath.push(errors[0].title);
visit(errors[0].steps);
}
};
visit(result.steps);
stepPaths.add(["", ...stepPath].join(" \u203A "));
}
fullHeader = header + (stepPaths.size === 1 ? stepPaths.values().next().value : "");
}
return separator(screen, fullHeader);
}
function formatError(screen, error) {
const message = error.message || error.value || "";
const stack = error.stack;
if (!stack && !error.location)
return { message };
const tokens = [];
const parsedStack = stack ? prepareErrorStack(stack) : void 0;
tokens.push(parsedStack?.message || message);
if (error.snippet) {
let snippet = error.snippet;
if (!screen.colors.enabled)
snippet = (0, import_util.stripAnsiEscapes)(snippet);
tokens.push("");
tokens.push(snippet);
}
if (parsedStack && parsedStack.stackLines.length)
tokens.push(screen.colors.dim(parsedStack.stackLines.join("\n")));
let location = error.location;
if (parsedStack && !location)
location = parsedStack.location;
if (error.cause)
tokens.push(screen.colors.dim("[cause]: ") + formatError(screen, error.cause).message);
return {
location,
message: tokens.join("\n")
};
}
function separator(screen, text = "") {
if (text)
text += " ";
const columns = Math.min(100, screen.ttyWidth || 100);
return text + screen.colors.dim("\u2500".repeat(Math.max(0, columns - (0, import_util.stripAnsiEscapes)(text).length)));
}
function indent(lines, tab) {
return lines.replace(/^(?=.+$)/gm, tab);
}
function prepareErrorStack(stack) {
return (0, import_utils.parseErrorStack)(stack, import_path.default.sep, !!process.env.PWDEBUGIMPL);
}
function characterWidth(c) {
return import_utilsBundle2.getEastAsianWidth.eastAsianWidth(c.codePointAt(0));
}
function stringWidth(v) {
let width = 0;
for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(v))
width += characterWidth(segment);
return width;
}
function suffixOfWidth(v, width) {
const segments = [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(v)];
let suffixBegin = v.length;
for (const { segment, index } of segments.reverse()) {
const segmentWidth = stringWidth(segment);
if (segmentWidth > width)
break;
width -= segmentWidth;
suffixBegin = index;
}
return v.substring(suffixBegin);
}
function fitToWidth(line, width, prefix) {
const prefixLength = prefix ? (0, import_util.stripAnsiEscapes)(prefix).length : 0;
width -= prefixLength;
if (stringWidth(line) <= width)
return line;
const parts = line.split(import_util.ansiRegex);
const taken = [];
for (let i = parts.length - 1; i >= 0; i--) {
if (i % 2) {
taken.push(parts[i]);
} else {
let part = suffixOfWidth(parts[i], width);
const wasTruncated = part.length < parts[i].length;
if (wasTruncated && parts[i].length > 0) {
part = "\u2026" + suffixOfWidth(parts[i], width - 1);
}
taken.push(part);
width -= stringWidth(part);
}
}
return taken.reverse().join("");
}
function resolveFromEnv(name) {
const value = process.env[name];
if (value)
return import_path.default.resolve(process.cwd(), value);
return void 0;
}
function resolveOutputFile(reporterName, options) {
const name = reporterName.toUpperCase();
let outputFile = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_FILE`);
if (!outputFile && options.outputFile)
outputFile = import_path.default.resolve(options.configDir, options.outputFile);
if (outputFile)
return { outputFile };
let outputDir = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_DIR`);
if (!outputDir && options.outputDir)
outputDir = import_path.default.resolve(options.configDir, options.outputDir);
if (!outputDir && options.default)
outputDir = (0, import_util.resolveReporterOutputPath)(options.default.outputDir, options.configDir, void 0);
if (!outputDir)
outputDir = options.configDir;
const reportName = process.env[`PLAYWRIGHT_${name}_OUTPUT_NAME`] ?? options.fileName ?? options.default?.fileName;
if (!reportName)
return void 0;
outputFile = import_path.default.resolve(outputDir, reportName);
return { outputFile, outputDir };
}
function groupAttachments(attachments) {
const result = [];
const attachmentsByPrefix = /* @__PURE__ */ new Map();
for (const attachment of attachments) {
if (!attachment.path) {
result.push(attachment);
continue;
}
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
if (!match) {
result.push(attachment);
continue;
}
const [, name, category] = match;
let group = attachmentsByPrefix.get(name);
if (!group) {
group = { ...attachment, name };
attachmentsByPrefix.set(name, group);
result.push(group);
}
if (category === "expected")
group.expected = attachment;
else if (category === "actual")
group.actual = attachment;
else if (category === "diff")
group.diff = attachment;
else if (category === "previous")
group.previous = attachment;
}
return result;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TerminalReporter,
fitToWidth,
formatError,
formatFailure,
formatResultFailure,
formatRetry,
internalScreen,
kOutputSymbol,
nonTerminalScreen,
prepareErrorStack,
relativeFilePath,
resolveOutputFile,
separator,
stepSuffix,
terminalScreen
});