base.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var base_exports = {};
  30. __export(base_exports, {
  31. TerminalReporter: () => TerminalReporter,
  32. fitToWidth: () => fitToWidth,
  33. formatError: () => formatError,
  34. formatFailure: () => formatFailure,
  35. formatResultFailure: () => formatResultFailure,
  36. formatRetry: () => formatRetry,
  37. internalScreen: () => internalScreen,
  38. kOutputSymbol: () => kOutputSymbol,
  39. nonTerminalScreen: () => nonTerminalScreen,
  40. prepareErrorStack: () => prepareErrorStack,
  41. relativeFilePath: () => relativeFilePath,
  42. resolveOutputFile: () => resolveOutputFile,
  43. separator: () => separator,
  44. stepSuffix: () => stepSuffix,
  45. terminalScreen: () => terminalScreen
  46. });
  47. module.exports = __toCommonJS(base_exports);
  48. var import_path = __toESM(require("path"));
  49. var import_utils = require("playwright-core/lib/utils");
  50. var import_utilsBundle = require("playwright-core/lib/utilsBundle");
  51. var import_utils2 = require("playwright-core/lib/utils");
  52. var import_util = require("../util");
  53. var import_utilsBundle2 = require("../utilsBundle");
  54. const kOutputSymbol = Symbol("output");
  55. const DEFAULT_TTY_WIDTH = 100;
  56. const DEFAULT_TTY_HEIGHT = 40;
  57. const originalProcessStdout = process.stdout;
  58. const originalProcessStderr = process.stderr;
  59. const terminalScreen = (() => {
  60. let isTTY = !!originalProcessStdout.isTTY;
  61. let ttyWidth = originalProcessStdout.columns || 0;
  62. let ttyHeight = originalProcessStdout.rows || 0;
  63. if (process.env.PLAYWRIGHT_FORCE_TTY === "false" || process.env.PLAYWRIGHT_FORCE_TTY === "0") {
  64. isTTY = false;
  65. ttyWidth = 0;
  66. ttyHeight = 0;
  67. } else if (process.env.PLAYWRIGHT_FORCE_TTY === "true" || process.env.PLAYWRIGHT_FORCE_TTY === "1") {
  68. isTTY = true;
  69. ttyWidth = originalProcessStdout.columns || DEFAULT_TTY_WIDTH;
  70. ttyHeight = originalProcessStdout.rows || DEFAULT_TTY_HEIGHT;
  71. } else if (process.env.PLAYWRIGHT_FORCE_TTY) {
  72. isTTY = true;
  73. const sizeMatch = process.env.PLAYWRIGHT_FORCE_TTY.match(/^(\d+)x(\d+)$/);
  74. if (sizeMatch) {
  75. ttyWidth = +sizeMatch[1];
  76. ttyHeight = +sizeMatch[2];
  77. } else {
  78. ttyWidth = +process.env.PLAYWRIGHT_FORCE_TTY;
  79. ttyHeight = DEFAULT_TTY_HEIGHT;
  80. }
  81. if (isNaN(ttyWidth))
  82. ttyWidth = DEFAULT_TTY_WIDTH;
  83. if (isNaN(ttyHeight))
  84. ttyHeight = DEFAULT_TTY_HEIGHT;
  85. }
  86. let useColors = isTTY;
  87. if (process.env.DEBUG_COLORS === "0" || process.env.DEBUG_COLORS === "false" || process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false")
  88. useColors = false;
  89. else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR)
  90. useColors = true;
  91. const colors = useColors ? import_utils2.colors : import_utils2.noColors;
  92. return {
  93. resolveFiles: "cwd",
  94. isTTY,
  95. ttyWidth,
  96. ttyHeight,
  97. colors,
  98. stdout: originalProcessStdout,
  99. stderr: originalProcessStderr
  100. };
  101. })();
  102. const nonTerminalScreen = {
  103. colors: terminalScreen.colors,
  104. isTTY: false,
  105. ttyWidth: 0,
  106. ttyHeight: 0,
  107. resolveFiles: "rootDir"
  108. };
  109. const internalScreen = {
  110. colors: import_utils2.colors,
  111. isTTY: false,
  112. ttyWidth: 0,
  113. ttyHeight: 0,
  114. resolveFiles: "rootDir"
  115. };
  116. class TerminalReporter {
  117. constructor(options = {}) {
  118. this.totalTestCount = 0;
  119. this.fileDurations = /* @__PURE__ */ new Map();
  120. this._fatalErrors = [];
  121. this._failureCount = 0;
  122. this.screen = options.screen ?? terminalScreen;
  123. this._omitFailures = options.omitFailures || false;
  124. }
  125. version() {
  126. return "v2";
  127. }
  128. onConfigure(config) {
  129. this.config = config;
  130. }
  131. onBegin(suite) {
  132. this.suite = suite;
  133. this.totalTestCount = suite.allTests().length;
  134. }
  135. onStdOut(chunk, test, result) {
  136. this._appendOutput({ chunk, type: "stdout" }, result);
  137. }
  138. onStdErr(chunk, test, result) {
  139. this._appendOutput({ chunk, type: "stderr" }, result);
  140. }
  141. _appendOutput(output, result) {
  142. if (!result)
  143. return;
  144. result[kOutputSymbol] = result[kOutputSymbol] || [];
  145. result[kOutputSymbol].push(output);
  146. }
  147. onTestEnd(test, result) {
  148. if (result.status !== "skipped" && result.status !== test.expectedStatus)
  149. ++this._failureCount;
  150. const projectName = test.titlePath()[1];
  151. const relativePath = relativeTestPath(this.screen, this.config, test);
  152. const fileAndProject = (projectName ? `[${projectName}] \u203A ` : "") + relativePath;
  153. const entry = this.fileDurations.get(fileAndProject) || { duration: 0, workers: /* @__PURE__ */ new Set() };
  154. entry.duration += result.duration;
  155. entry.workers.add(result.workerIndex);
  156. this.fileDurations.set(fileAndProject, entry);
  157. }
  158. onError(error) {
  159. this._fatalErrors.push(error);
  160. }
  161. async onEnd(result) {
  162. this.result = result;
  163. }
  164. fitToScreen(line, prefix) {
  165. if (!this.screen.ttyWidth) {
  166. return line;
  167. }
  168. return fitToWidth(line, this.screen.ttyWidth, prefix);
  169. }
  170. generateStartingMessage() {
  171. const jobs = this.config.metadata.actualWorkers ?? this.config.workers;
  172. const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : "";
  173. if (!this.totalTestCount)
  174. return "";
  175. 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}`);
  176. }
  177. getSlowTests() {
  178. if (!this.config.reportSlowTests)
  179. return [];
  180. const fileDurations = [...this.fileDurations.entries()].filter(([key, value]) => value.workers.size === 1).map(([key, value]) => [key, value.duration]);
  181. fileDurations.sort((a, b) => b[1] - a[1]);
  182. const count = Math.min(fileDurations.length, this.config.reportSlowTests.max || Number.POSITIVE_INFINITY);
  183. const threshold = this.config.reportSlowTests.threshold;
  184. return fileDurations.filter(([, duration]) => duration > threshold).slice(0, count);
  185. }
  186. generateSummaryMessage({ didNotRun, skipped, expected, interrupted, unexpected, flaky, fatalErrors }) {
  187. const tokens = [];
  188. if (unexpected.length) {
  189. tokens.push(this.screen.colors.red(` ${unexpected.length} failed`));
  190. for (const test of unexpected)
  191. tokens.push(this.screen.colors.red(this.formatTestHeader(test, { indent: " " })));
  192. }
  193. if (interrupted.length) {
  194. tokens.push(this.screen.colors.yellow(` ${interrupted.length} interrupted`));
  195. for (const test of interrupted)
  196. tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
  197. }
  198. if (flaky.length) {
  199. tokens.push(this.screen.colors.yellow(` ${flaky.length} flaky`));
  200. for (const test of flaky)
  201. tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
  202. }
  203. if (skipped)
  204. tokens.push(this.screen.colors.yellow(` ${skipped} skipped`));
  205. if (didNotRun)
  206. tokens.push(this.screen.colors.yellow(` ${didNotRun} did not run`));
  207. if (expected)
  208. tokens.push(this.screen.colors.green(` ${expected} passed`) + this.screen.colors.dim(` (${(0, import_utilsBundle.ms)(this.result.duration)})`));
  209. if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0)
  210. 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`));
  211. return tokens.join("\n");
  212. }
  213. generateSummary() {
  214. let didNotRun = 0;
  215. let skipped = 0;
  216. let expected = 0;
  217. const interrupted = [];
  218. const interruptedToPrint = [];
  219. const unexpected = [];
  220. const flaky = [];
  221. this.suite.allTests().forEach((test) => {
  222. switch (test.outcome()) {
  223. case "skipped": {
  224. if (test.results.some((result) => result.status === "interrupted")) {
  225. if (test.results.some((result) => !!result.error))
  226. interruptedToPrint.push(test);
  227. interrupted.push(test);
  228. } else if (!test.results.length || test.expectedStatus !== "skipped") {
  229. ++didNotRun;
  230. } else {
  231. ++skipped;
  232. }
  233. break;
  234. }
  235. case "expected":
  236. ++expected;
  237. break;
  238. case "unexpected":
  239. unexpected.push(test);
  240. break;
  241. case "flaky":
  242. flaky.push(test);
  243. break;
  244. }
  245. });
  246. const failuresToPrint = [...unexpected, ...flaky, ...interruptedToPrint];
  247. return {
  248. didNotRun,
  249. skipped,
  250. expected,
  251. interrupted,
  252. unexpected,
  253. flaky,
  254. failuresToPrint,
  255. fatalErrors: this._fatalErrors
  256. };
  257. }
  258. epilogue(full) {
  259. const summary = this.generateSummary();
  260. const summaryMessage = this.generateSummaryMessage(summary);
  261. if (full && summary.failuresToPrint.length && !this._omitFailures)
  262. this._printFailures(summary.failuresToPrint);
  263. this._printSlowTests();
  264. this._printSummary(summaryMessage);
  265. }
  266. _printFailures(failures) {
  267. this.writeLine("");
  268. failures.forEach((test, index) => {
  269. this.writeLine(this.formatFailure(test, index + 1));
  270. });
  271. }
  272. _printSlowTests() {
  273. const slowTests = this.getSlowTests();
  274. slowTests.forEach(([file, duration]) => {
  275. this.writeLine(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${(0, import_utilsBundle.ms)(duration)})`));
  276. });
  277. if (slowTests.length)
  278. this.writeLine(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
  279. }
  280. _printSummary(summary) {
  281. if (summary.trim())
  282. this.writeLine(summary);
  283. }
  284. willRetry(test) {
  285. return test.outcome() === "unexpected" && test.results.length <= test.retries;
  286. }
  287. formatTestTitle(test, step, omitLocation = false) {
  288. return formatTestTitle(this.screen, this.config, test, step, omitLocation);
  289. }
  290. formatTestHeader(test, options = {}) {
  291. return formatTestHeader(this.screen, this.config, test, options);
  292. }
  293. formatFailure(test, index) {
  294. return formatFailure(this.screen, this.config, test, index);
  295. }
  296. formatError(error) {
  297. return formatError(this.screen, error);
  298. }
  299. writeLine(line) {
  300. this.screen.stdout?.write(line ? line + "\n" : "\n");
  301. }
  302. }
  303. function formatFailure(screen, config, test, index) {
  304. const lines = [];
  305. const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
  306. lines.push(screen.colors.red(header));
  307. for (const result of test.results) {
  308. const resultLines = [];
  309. const errors = formatResultFailure(screen, test, result, " ");
  310. if (!errors.length)
  311. continue;
  312. if (result.retry) {
  313. resultLines.push("");
  314. resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
  315. }
  316. resultLines.push(...errors.map((error) => "\n" + error.message));
  317. const attachmentGroups = groupAttachments(result.attachments);
  318. for (let i = 0; i < attachmentGroups.length; ++i) {
  319. const attachment = attachmentGroups[i];
  320. if (attachment.name === "error-context" && attachment.path) {
  321. resultLines.push("");
  322. resultLines.push(screen.colors.dim(` Error Context: ${relativeFilePath(screen, config, attachment.path)}`));
  323. continue;
  324. }
  325. if (attachment.name.startsWith("_"))
  326. continue;
  327. const hasPrintableContent = attachment.contentType.startsWith("text/");
  328. if (!attachment.path && !hasPrintableContent)
  329. continue;
  330. resultLines.push("");
  331. resultLines.push(screen.colors.dim(separator(screen, ` attachment #${i + 1}: ${screen.colors.bold(attachment.name)} (${attachment.contentType})`)));
  332. if (attachment.actual?.path) {
  333. if (attachment.expected?.path) {
  334. const expectedPath = relativeFilePath(screen, config, attachment.expected.path);
  335. resultLines.push(screen.colors.dim(` Expected: ${expectedPath}`));
  336. }
  337. const actualPath = relativeFilePath(screen, config, attachment.actual.path);
  338. resultLines.push(screen.colors.dim(` Received: ${actualPath}`));
  339. if (attachment.previous?.path) {
  340. const previousPath = relativeFilePath(screen, config, attachment.previous.path);
  341. resultLines.push(screen.colors.dim(` Previous: ${previousPath}`));
  342. }
  343. if (attachment.diff?.path) {
  344. const diffPath = relativeFilePath(screen, config, attachment.diff.path);
  345. resultLines.push(screen.colors.dim(` Diff: ${diffPath}`));
  346. }
  347. } else if (attachment.path) {
  348. const relativePath = relativeFilePath(screen, config, attachment.path);
  349. resultLines.push(screen.colors.dim(` ${relativePath}`));
  350. if (attachment.name === "trace") {
  351. const packageManagerCommand = (0, import_utils.getPackageManagerExecCommand)();
  352. resultLines.push(screen.colors.dim(` Usage:`));
  353. resultLines.push("");
  354. resultLines.push(screen.colors.dim(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
  355. resultLines.push("");
  356. }
  357. } else {
  358. if (attachment.contentType.startsWith("text/") && attachment.body) {
  359. let text = attachment.body.toString();
  360. if (text.length > 300)
  361. text = text.slice(0, 300) + "...";
  362. for (const line of text.split("\n"))
  363. resultLines.push(screen.colors.dim(` ${line}`));
  364. }
  365. }
  366. resultLines.push(screen.colors.dim(separator(screen, " ")));
  367. }
  368. lines.push(...resultLines);
  369. }
  370. lines.push("");
  371. return lines.join("\n");
  372. }
  373. function formatRetry(screen, result) {
  374. const retryLines = [];
  375. if (result.retry) {
  376. retryLines.push("");
  377. retryLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
  378. }
  379. return retryLines;
  380. }
  381. function quotePathIfNeeded(path2) {
  382. if (/\s/.test(path2))
  383. return `"${path2}"`;
  384. return path2;
  385. }
  386. function formatResultFailure(screen, test, result, initialIndent) {
  387. const errorDetails = [];
  388. if (result.status === "passed" && test.expectedStatus === "failed") {
  389. errorDetails.push({
  390. message: indent(screen.colors.red(`Expected to fail, but passed.`), initialIndent)
  391. });
  392. }
  393. if (result.status === "interrupted") {
  394. errorDetails.push({
  395. message: indent(screen.colors.red(`Test was interrupted.`), initialIndent)
  396. });
  397. }
  398. for (const error of result.errors) {
  399. const formattedError = formatError(screen, error);
  400. errorDetails.push({
  401. message: indent(formattedError.message, initialIndent),
  402. location: formattedError.location
  403. });
  404. }
  405. return errorDetails;
  406. }
  407. function relativeFilePath(screen, config, file) {
  408. if (screen.resolveFiles === "cwd")
  409. return import_path.default.relative(process.cwd(), file);
  410. return import_path.default.relative(config.rootDir, file);
  411. }
  412. function relativeTestPath(screen, config, test) {
  413. return relativeFilePath(screen, config, test.location.file);
  414. }
  415. function stepSuffix(step) {
  416. const stepTitles = step ? step.titlePath() : [];
  417. return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
  418. }
  419. function formatTestTitle(screen, config, test, step, omitLocation = false) {
  420. const [, projectName, , ...titles] = test.titlePath();
  421. let location;
  422. if (omitLocation)
  423. location = `${relativeTestPath(screen, config, test)}`;
  424. else
  425. location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
  426. const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
  427. const testTitle = `${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
  428. const extraTags = test.tags.filter((t) => !testTitle.includes(t));
  429. return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
  430. }
  431. function formatTestHeader(screen, config, test, options = {}) {
  432. const title = formatTestTitle(screen, config, test);
  433. const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
  434. let fullHeader = header;
  435. if (options.mode === "error") {
  436. const stepPaths = /* @__PURE__ */ new Set();
  437. for (const result of test.results.filter((r) => !!r.errors.length)) {
  438. const stepPath = [];
  439. const visit = (steps) => {
  440. const errors = steps.filter((s) => s.error);
  441. if (errors.length > 1)
  442. return;
  443. if (errors.length === 1 && errors[0].category === "test.step") {
  444. stepPath.push(errors[0].title);
  445. visit(errors[0].steps);
  446. }
  447. };
  448. visit(result.steps);
  449. stepPaths.add(["", ...stepPath].join(" \u203A "));
  450. }
  451. fullHeader = header + (stepPaths.size === 1 ? stepPaths.values().next().value : "");
  452. }
  453. return separator(screen, fullHeader);
  454. }
  455. function formatError(screen, error) {
  456. const message = error.message || error.value || "";
  457. const stack = error.stack;
  458. if (!stack && !error.location)
  459. return { message };
  460. const tokens = [];
  461. const parsedStack = stack ? prepareErrorStack(stack) : void 0;
  462. tokens.push(parsedStack?.message || message);
  463. if (error.snippet) {
  464. let snippet = error.snippet;
  465. if (!screen.colors.enabled)
  466. snippet = (0, import_util.stripAnsiEscapes)(snippet);
  467. tokens.push("");
  468. tokens.push(snippet);
  469. }
  470. if (parsedStack && parsedStack.stackLines.length)
  471. tokens.push(screen.colors.dim(parsedStack.stackLines.join("\n")));
  472. let location = error.location;
  473. if (parsedStack && !location)
  474. location = parsedStack.location;
  475. if (error.cause)
  476. tokens.push(screen.colors.dim("[cause]: ") + formatError(screen, error.cause).message);
  477. return {
  478. location,
  479. message: tokens.join("\n")
  480. };
  481. }
  482. function separator(screen, text = "") {
  483. if (text)
  484. text += " ";
  485. const columns = Math.min(100, screen.ttyWidth || 100);
  486. return text + screen.colors.dim("\u2500".repeat(Math.max(0, columns - (0, import_util.stripAnsiEscapes)(text).length)));
  487. }
  488. function indent(lines, tab) {
  489. return lines.replace(/^(?=.+$)/gm, tab);
  490. }
  491. function prepareErrorStack(stack) {
  492. return (0, import_utils.parseErrorStack)(stack, import_path.default.sep, !!process.env.PWDEBUGIMPL);
  493. }
  494. function characterWidth(c) {
  495. return import_utilsBundle2.getEastAsianWidth.eastAsianWidth(c.codePointAt(0));
  496. }
  497. function stringWidth(v) {
  498. let width = 0;
  499. for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(v))
  500. width += characterWidth(segment);
  501. return width;
  502. }
  503. function suffixOfWidth(v, width) {
  504. const segments = [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(v)];
  505. let suffixBegin = v.length;
  506. for (const { segment, index } of segments.reverse()) {
  507. const segmentWidth = stringWidth(segment);
  508. if (segmentWidth > width)
  509. break;
  510. width -= segmentWidth;
  511. suffixBegin = index;
  512. }
  513. return v.substring(suffixBegin);
  514. }
  515. function fitToWidth(line, width, prefix) {
  516. const prefixLength = prefix ? (0, import_util.stripAnsiEscapes)(prefix).length : 0;
  517. width -= prefixLength;
  518. if (stringWidth(line) <= width)
  519. return line;
  520. const parts = line.split(import_util.ansiRegex);
  521. const taken = [];
  522. for (let i = parts.length - 1; i >= 0; i--) {
  523. if (i % 2) {
  524. taken.push(parts[i]);
  525. } else {
  526. let part = suffixOfWidth(parts[i], width);
  527. const wasTruncated = part.length < parts[i].length;
  528. if (wasTruncated && parts[i].length > 0) {
  529. part = "\u2026" + suffixOfWidth(parts[i], width - 1);
  530. }
  531. taken.push(part);
  532. width -= stringWidth(part);
  533. }
  534. }
  535. return taken.reverse().join("");
  536. }
  537. function resolveFromEnv(name) {
  538. const value = process.env[name];
  539. if (value)
  540. return import_path.default.resolve(process.cwd(), value);
  541. return void 0;
  542. }
  543. function resolveOutputFile(reporterName, options) {
  544. const name = reporterName.toUpperCase();
  545. let outputFile = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_FILE`);
  546. if (!outputFile && options.outputFile)
  547. outputFile = import_path.default.resolve(options.configDir, options.outputFile);
  548. if (outputFile)
  549. return { outputFile };
  550. let outputDir = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_DIR`);
  551. if (!outputDir && options.outputDir)
  552. outputDir = import_path.default.resolve(options.configDir, options.outputDir);
  553. if (!outputDir && options.default)
  554. outputDir = (0, import_util.resolveReporterOutputPath)(options.default.outputDir, options.configDir, void 0);
  555. if (!outputDir)
  556. outputDir = options.configDir;
  557. const reportName = process.env[`PLAYWRIGHT_${name}_OUTPUT_NAME`] ?? options.fileName ?? options.default?.fileName;
  558. if (!reportName)
  559. return void 0;
  560. outputFile = import_path.default.resolve(outputDir, reportName);
  561. return { outputFile, outputDir };
  562. }
  563. function groupAttachments(attachments) {
  564. const result = [];
  565. const attachmentsByPrefix = /* @__PURE__ */ new Map();
  566. for (const attachment of attachments) {
  567. if (!attachment.path) {
  568. result.push(attachment);
  569. continue;
  570. }
  571. const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
  572. if (!match) {
  573. result.push(attachment);
  574. continue;
  575. }
  576. const [, name, category] = match;
  577. let group = attachmentsByPrefix.get(name);
  578. if (!group) {
  579. group = { ...attachment, name };
  580. attachmentsByPrefix.set(name, group);
  581. result.push(group);
  582. }
  583. if (category === "expected")
  584. group.expected = attachment;
  585. else if (category === "actual")
  586. group.actual = attachment;
  587. else if (category === "diff")
  588. group.diff = attachment;
  589. else if (category === "previous")
  590. group.previous = attachment;
  591. }
  592. return result;
  593. }
  594. // Annotate the CommonJS export names for ESM import in node:
  595. 0 && (module.exports = {
  596. TerminalReporter,
  597. fitToWidth,
  598. formatError,
  599. formatFailure,
  600. formatResultFailure,
  601. formatRetry,
  602. internalScreen,
  603. kOutputSymbol,
  604. nonTerminalScreen,
  605. prepareErrorStack,
  606. relativeFilePath,
  607. resolveOutputFile,
  608. separator,
  609. stepSuffix,
  610. terminalScreen
  611. });