UNPKG

playwright

Version:

A high-level API to automate web browsers

1,406 lines (1,398 loc) • 295 kB
"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