 aacb45156b
			
		
	
	aacb45156b
	
	
	
		
			
			- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			605 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			605 lines
		
	
	
		
			22 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 terminalScreen = (() => {
 | |
|   let isTTY = !!process.stdout.isTTY;
 | |
|   let ttyWidth = process.stdout.columns || 0;
 | |
|   let ttyHeight = process.stdout.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 = process.stdout.columns || DEFAULT_TTY_WIDTH;
 | |
|     ttyHeight = process.stdout.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
 | |
|   };
 | |
| })();
 | |
| 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.screen = terminalScreen;
 | |
|     this.totalTestCount = 0;
 | |
|     this.fileDurations = /* @__PURE__ */ new Map();
 | |
|     this._fatalErrors = [];
 | |
|     this._failureCount = 0;
 | |
|     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) {
 | |
|     console.log("");
 | |
|     failures.forEach((test, index) => {
 | |
|       console.log(this.formatFailure(test, index + 1));
 | |
|     });
 | |
|   }
 | |
|   _printSlowTests() {
 | |
|     const slowTests = this.getSlowTests();
 | |
|     slowTests.forEach(([file, duration]) => {
 | |
|       console.log(this.screen.colors.yellow("  Slow test file: ") + file + this.screen.colors.yellow(` (${(0, import_utilsBundle.ms)(duration)})`));
 | |
|     });
 | |
|     if (slowTests.length)
 | |
|       console.log(this.screen.colors.yellow("  Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
 | |
|   }
 | |
|   _printSummary(summary) {
 | |
|     if (summary.trim())
 | |
|       console.log(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);
 | |
|   }
 | |
| }
 | |
| 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
 | |
| });
 |