Spaces:
Paused
Paused
Download node_modules/playwright/lib/runner/index.js from ThomasHeisig/Brain-5D-Space: direct link, hf CLI and curl.
- Browser
- Download file 315 kB
-
https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/node_modules/playwright/lib/runner/index.js
- Command line
-
hf download hf://spaces/ThomasHeisig/Brain-5D-Space/node_modules/playwright/lib/runner/index.js
-
curl -L -o index.js https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/node_modules/playwright/lib/runner/index.js
315 kB
| ; | |
| 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_coreBundle3 = require("playwright-core/lib/coreBundle"); | |
| var import_common10 = 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 ignoredPrefixes = this._ignoredFolders.map((folder) => folder.replace(/\\/g, "/") + "/"); | |
| const ignored = (file) => { | |
| if (file.split("/").includes("node_modules")) | |
| return true; | |
| return ignoredPrefixes.some((prefix) => (file + "/").startsWith(prefix)); | |
| }; | |
| this._fsWatcher = chokidar.watch(watchedPaths, { ignoreInitial: true, ignored }).on("all", async (event, file) => { | |
| if (event !== "add" && event !== "addDir" && event !== "change" && event !== "unlink" && event !== "unlinkDir") | |
| return; | |
| 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); | |
| } | |
| skip(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleSuite (read-only)."); | |
| } | |
| fixme(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleSuite (read-only)."); | |
| } | |
| fail(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleSuite (read-only)."); | |
| } | |
| exclude() { | |
| throw new Error("Disposition methods are not supported on a TeleSuite (read-only)."); | |
| } | |
| }; | |
| 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; | |
| } | |
| skip(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleTestCase (read-only)."); | |
| } | |
| fixme(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleTestCase (read-only)."); | |
| } | |
| fail(_reason) { | |
| throw new Error("Disposition methods are not supported on a TeleTestCase (read-only)."); | |
| } | |
| exclude() { | |
| throw new Error("Disposition methods are not supported on a TeleTestCase (read-only)."); | |
| } | |
| }; | |
| var TeleTestStep = class { | |
| constructor(payload, parentStep, location, result) { | |
| this.duration = -1; | |
| this.steps = []; | |
| this._startTime = 0; | |
| this.title = payload.title; | |
| this.subtitle = payload.subtitle; | |
| this.category = payload.category; | |
| this.params = payload.params; | |
| 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 = { | |
| argv: [], | |
| failOnFlakyTests: false, | |
| 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 didNotRun2 = 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") { | |
| ++didNotRun2; | |
| } 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 | |
| ]; | |
| const logOutput = await runGit(["log", "-1", `--pretty=format:${tokens.join(separator2)}`], gitDir); | |
| if (!logOutput) | |
| return void 0; | |
| const branchOutput = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], gitDir); | |
| const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime] = logOutput.split(separator2); | |
| return { | |
| shortHash, | |
| hash, | |
| subject, | |
| body, | |
| author: { | |
| name: authorName, | |
| email: authorEmail, | |
| time: +authorTime * 1e3 | |
| }, | |
| committer: { | |
| name: committerName, | |
| email: committerEmail, | |
| time: +committerTime * 1e3 | |
| }, | |
| branch: branchOutput?.trim() ?? "" | |
| }; | |
| } | |
| async function gitDiff(gitDir, ci) { | |
| const diffLimit = 1e5; | |
| if (ci?.prBaseHash) { | |
| const hasBaseCommit = await runGit(["cat-file", "-e", `${ci.prBaseHash}^{commit}`], gitDir) !== void 0; | |
| if (!hasBaseCommit) { | |
| const isShallow = await runGit(["rev-parse", "--is-shallow-repository"], gitDir) === "true"; | |
| const fetchArgs = ["fetch", "origin", ci.prBaseHash, "--no-auto-maintenance", "--no-auto-gc", "--no-tags", "--no-recurse-submodules"]; | |
| if (isShallow) | |
| fetchArgs.push("--depth=1"); | |
| await runGit(fetchArgs, gitDir); | |
| } | |
| const diff3 = await runGit(["diff", ci.prBaseHash, "HEAD"], gitDir); | |
| if (diff3) | |
| return diff3.substring(0, diffLimit); | |
| } | |
| if (ci) | |
| return; | |
| const uncommitted = await runGit(["diff"], gitDir); | |
| if (uncommitted === void 0) { | |
| return; | |
| } | |
| if (uncommitted) | |
| return uncommitted.substring(0, diffLimit); | |
| const diff2 = await runGit(["diff", "HEAD~1"], gitDir); | |
| return diff2?.substring(0, diffLimit); | |
| } | |
| async function runGit(args, cwd) { | |
| debug(`running "git ${args.join(" ")}"`); | |
| const start = monotonicTime(); | |
| const result = await spawnAsync( | |
| "git", | |
| args, | |
| { stdio: "pipe", cwd, timeout: GIT_OPERATIONS_TIMEOUT_MS } | |
| ); | |
| if (monotonicTime() - start > GIT_OPERATIONS_TIMEOUT_MS) { | |
| print(`timeout of ${GIT_OPERATIONS_TIMEOUT_MS}ms exceeded while running "git ${args.join(" ")}"`); | |
| 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 import_util = require("util"); | |
| 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] += (0, import_util.stripVTControlCharacters)(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_stream = require("stream"); | |
| var import_util2 = 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").utils; | |
| var { getPackageManagerExecCommand } = require("playwright-core/lib/coreBundle").utils; | |
| var { fitToWidth } = require("playwright-core/lib/coreBundle").utils; | |
| var kOutputSymbol = /* @__PURE__ */ Symbol("output"); | |
| var DEFAULT_TTY_WIDTH = 100; | |
| var DEFAULT_TTY_HEIGHT = 40; | |
| var originalProcessStdout = process.stdout; | |
| var originalProcessStderr = process.stderr; | |
| var StripAnsiStream = class extends import_stream.Writable { | |
| constructor(target) { | |
| super(); | |
| this._target = target; | |
| } | |
| write(chunk, encodingOrCallback, callback) { | |
| const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback; | |
| return this._target.write((0, import_util2.stripAnsiEscapes)(chunk.toString()), cb); | |
| } | |
| }; | |
| 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" || process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") | |
| 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: useColors ? originalProcessStdout : new StripAnsiStream(originalProcessStdout), | |
| stderr: useColors ? originalProcessStderr : new StripAnsiStream(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: didNotRun2, 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 (didNotRun2) | |
| tokens.push(this.screen.colors.yellow(` ${didNotRun2} 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 didNotRun2 = 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") { | |
| ++didNotRun2; | |
| } 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: didNotRun2, | |
| 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, omitTags: this._options.omitTags }); | |
| } | |
| 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, omitTags: options?.omitTags }); | |
| 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 = /* @__PURE__ */ 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 = []; | |
| for (let current = step; current; current = current.parent) | |
| stepTitles.unshift(current.subtitle ? `${current.title} ${current.subtitle}` : current.title); | |
| 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 projectLabel = options.includeTestId ? `project=` : ""; | |
| const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : ""; | |
| const testTitle = `${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`; | |
| const extraTags = options.omitTags ? [] : test.tags.filter((t2) => !testTitle.includes(t2) && !config2.tags.includes(t2)); | |
| return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`; | |
| } | |
| function formatTestHeader(screen, config2, test, options = {}) { | |
| const title = formatTestTitle(screen, config2, test, void 0, options); | |
| 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_util2.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_util2.stripAnsiEscapes)(text).length))); | |
| } | |
| function indent(lines, tab) { | |
| return lines.replace(/^(?=.+$)/gm, tab); | |
| } | |
| function prepareErrorStack(stack) { | |
| return parseErrorStack(stack); | |
| } | |
| function resolveFromEnv(name) { | |
| const value = process.env[name]; | |
| if (value) | |
| return import_path2.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_path2.default.resolve(options.configDir, options.outputFile); | |
| if (outputFile) | |
| return { outputFile }; | |
| let outputDir = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_DIR`); | |
| if (!outputDir && options.outputDir) | |
| outputDir = import_path2.default.resolve(options.configDir, options.outputDir); | |
| if (!outputDir && options.default) | |
| outputDir = (0, import_util2.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_path2.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; | |
| } | |
| // packages/playwright/src/reporters/internalReporter.ts | |
| var import_fs = __toESM(require("fs")); | |
| // packages/playwright/src/reporters/multiplexer.ts | |
| var import_util3 = require("../util"); | |
| var Multiplexer = class { | |
| constructor(reporters) { | |
| this._hasReporterErrors = false; | |
| this._reporters = reporters; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| hasReporterErrors() { | |
| return this._hasReporterErrors; | |
| } | |
| onConfigure(config2) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onConfigure?.(config2)); | |
| } | |
| async preprocess(params) { | |
| for (const reporter of this._reporters) | |
| await reporter.preprocess?.(params); | |
| } | |
| onBegin(suite) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onBegin?.(suite)); | |
| } | |
| onTestBegin(test, result) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onTestBegin?.(test, result)); | |
| } | |
| onStdOut(chunk, test, result) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onStdOut?.(chunk, test, result)); | |
| } | |
| onStdErr(chunk, test, result) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onStdErr?.(chunk, test, result)); | |
| } | |
| async onTestPaused(test, result) { | |
| for (const reporter of this._reporters) | |
| await this._wrapAsync(() => reporter.onTestPaused?.(test, result)); | |
| } | |
| onTestEnd(test, result) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onTestEnd?.(test, result)); | |
| } | |
| onReportConfigure(params) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onReportConfigure?.(params)); | |
| } | |
| onReportEnd(params) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onReportEnd?.(params)); | |
| } | |
| async onEnd(result) { | |
| for (const reporter of this._reporters) { | |
| const outResult = await this._wrapAsync(() => reporter.onEnd?.(result)); | |
| if (outResult?.status) | |
| result.status = outResult.status; | |
| } | |
| return result; | |
| } | |
| async onExit() { | |
| for (const reporter of this._reporters) | |
| await this._wrapAsync(() => reporter.onExit?.()); | |
| } | |
| onError(error, workerInfo) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onError?.(error, workerInfo), false); | |
| } | |
| onStepBegin(test, result, step) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onStepBegin?.(test, result, step)); | |
| } | |
| onStepEnd(test, result, step) { | |
| for (const reporter of this._reporters) | |
| this._wrap(() => reporter.onStepEnd?.(test, result, step)); | |
| } | |
| printsToStdio() { | |
| return this._reporters.some((r) => { | |
| let prints = false; | |
| this._wrap(() => prints = r.printsToStdio ? r.printsToStdio() : true, false); | |
| return prints; | |
| }); | |
| } | |
| _wrap(callback, redispatch = true) { | |
| try { | |
| callback(); | |
| } catch (e) { | |
| this._hasReporterErrors = true; | |
| if (redispatch) | |
| this.onError((0, import_util3.serializeError)(e)); | |
| } | |
| } | |
| async _wrapAsync(callback) { | |
| try { | |
| return await callback(); | |
| } catch (e) { | |
| this._hasReporterErrors = true; | |
| this.onError((0, import_util3.serializeError)(e)); | |
| } | |
| } | |
| }; | |
| // packages/playwright/src/reporters/internalReporter.ts | |
| var import_common = require("../common"); | |
| var babel = __toESM(require("../transform/babelBundle")); | |
| // packages/playwright/src/reporters/reporterV2.ts | |
| function wrapReporterAsV2(reporter) { | |
| try { | |
| if ("version" in reporter && reporter.version() === "v2") | |
| return reporter; | |
| } catch (e) { | |
| } | |
| return new ReporterV2Wrapper(reporter); | |
| } | |
| var ReporterV2Wrapper = class { | |
| constructor(reporter) { | |
| this._deferred = []; | |
| this._reporter = reporter; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| onConfigure(config2) { | |
| this._config = config2; | |
| } | |
| preprocess(params) { | |
| return this._reporter.preprocess?.(params); | |
| } | |
| onBegin(suite) { | |
| this._reporter.onBegin?.(this._config, suite); | |
| const deferred = this._deferred; | |
| this._deferred = null; | |
| for (const item of deferred) { | |
| if (item.error) | |
| this.onError(item.error.error, item.error.workerInfo); | |
| if (item.stdout) | |
| this.onStdOut(item.stdout.chunk, item.stdout.test, item.stdout.result); | |
| if (item.stderr) | |
| this.onStdErr(item.stderr.chunk, item.stderr.test, item.stderr.result); | |
| } | |
| } | |
| onTestBegin(test, result) { | |
| this._reporter.onTestBegin?.(test, result); | |
| } | |
| onStdOut(chunk, test, result) { | |
| if (this._deferred) { | |
| this._deferred.push({ stdout: { chunk, test, result } }); | |
| return; | |
| } | |
| this._reporter.onStdOut?.(chunk, test, result); | |
| } | |
| onStdErr(chunk, test, result) { | |
| if (this._deferred) { | |
| this._deferred.push({ stderr: { chunk, test, result } }); | |
| return; | |
| } | |
| this._reporter.onStdErr?.(chunk, test, result); | |
| } | |
| onTestEnd(test, result) { | |
| this._reporter.onTestEnd?.(test, result); | |
| } | |
| async onEnd(result) { | |
| return await this._reporter.onEnd?.(result); | |
| } | |
| async onExit() { | |
| await this._reporter.onExit?.(); | |
| } | |
| onError(error, workerInfo) { | |
| if (this._deferred) { | |
| this._deferred.push({ error: { error, workerInfo } }); | |
| return; | |
| } | |
| this._reporter.onError?.(error, workerInfo); | |
| } | |
| onStepBegin(test, result, step) { | |
| this._reporter.onStepBegin?.(test, result, step); | |
| } | |
| onStepEnd(test, result, step) { | |
| this._reporter.onStepEnd?.(test, result, step); | |
| } | |
| printsToStdio() { | |
| return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true; | |
| } | |
| }; | |
| // packages/playwright/src/reporters/internalReporter.ts | |
| var { monotonicTime: monotonicTime3 } = require("playwright-core/lib/coreBundle").iso; | |
| var InternalReporter = class { | |
| constructor(reporters) { | |
| this._didBegin = false; | |
| this._reporter = new Multiplexer(reporters.map(wrapReporterAsV2)); | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| hasReporterErrors() { | |
| return this._reporter.hasReporterErrors(); | |
| } | |
| onConfigure(config2) { | |
| this._config = config2; | |
| this._startTime = /* @__PURE__ */ new Date(); | |
| this._monotonicStartTime = monotonicTime3(); | |
| this._reporter.onConfigure?.(config2); | |
| } | |
| async preprocess(params) { | |
| await this._reporter.preprocess(params); | |
| } | |
| onBegin(suite) { | |
| this._didBegin = true; | |
| this._reporter.onBegin?.(suite); | |
| } | |
| onTestBegin(test, result) { | |
| this._reporter.onTestBegin?.(test, result); | |
| } | |
| onStdOut(chunk, test, result) { | |
| this._reporter.onStdOut?.(chunk, test, result); | |
| } | |
| onStdErr(chunk, test, result) { | |
| this._reporter.onStdErr?.(chunk, test, result); | |
| } | |
| async onTestPaused(test, result) { | |
| this._addSnippetToTestErrors(test, result); | |
| return await this._reporter.onTestPaused?.(test, result); | |
| } | |
| onTestEnd(test, result) { | |
| this._addSnippetToTestErrors(test, result); | |
| this._reporter.onTestEnd?.(test, result); | |
| } | |
| async onEnd(result) { | |
| if (!this._didBegin) { | |
| this.onBegin(new import_common.test.Suite("", "root")); | |
| } | |
| return await this._reporter.onEnd?.({ | |
| ...result, | |
| startTime: this._startTime, | |
| duration: monotonicTime3() - this._monotonicStartTime | |
| }); | |
| } | |
| async onExit() { | |
| await this._reporter.onExit?.(); | |
| } | |
| onError(error, workerInfo) { | |
| addLocationAndSnippetToError(this._config, error); | |
| this._reporter.onError?.(error, workerInfo); | |
| } | |
| onStepBegin(test, result, step) { | |
| this._reporter.onStepBegin?.(test, result, step); | |
| } | |
| onStepEnd(test, result, step) { | |
| this._addSnippetToStepError(test, step); | |
| this._reporter.onStepEnd?.(test, result, step); | |
| } | |
| printsToStdio() { | |
| return this._reporter.printsToStdio?.() ?? true; | |
| } | |
| _addSnippetToTestErrors(test, result) { | |
| for (const error of result.errors) | |
| addLocationAndSnippetToError(this._config, error, test.location.file); | |
| } | |
| _addSnippetToStepError(test, step) { | |
| if (step.error) | |
| addLocationAndSnippetToError(this._config, step.error, test.location.file); | |
| } | |
| }; | |
| function addLocationAndSnippetToError(config2, error, file) { | |
| if (error.stack && !error.location) | |
| error.location = prepareErrorStack(error.stack).location; | |
| const location = error.location; | |
| if (!location) | |
| return; | |
| if (!!error.snippet) | |
| return; | |
| try { | |
| const tokens = []; | |
| const source = import_fs.default.readFileSync(location.file, "utf8"); | |
| const codeFrame = babel.codeFrameColumns(source, { start: location }, { highlightCode: true }); | |
| if (!file || import_fs.default.realpathSync(file) !== location.file) { | |
| tokens.push(internalScreen.colors.gray(` at `) + `${relativeFilePath(internalScreen, config2, location.file)}:${location.line}`); | |
| tokens.push(""); | |
| } | |
| tokens.push(codeFrame); | |
| error.snippet = tokens.join("\n"); | |
| } catch (e) { | |
| } | |
| } | |
| // packages/playwright/src/runner/testRunner.ts | |
| var import_util16 = require("../util"); | |
| // packages/playwright/src/runner/reporters.ts | |
| var reporters_exports = {}; | |
| __export(reporters_exports, { | |
| createErrorCollectingReporter: () => createErrorCollectingReporter, | |
| createReporters: () => createReporters | |
| }); | |
| var import_fs9 = __toESM(require("fs")); | |
| // packages/playwright/src/runner/loadUtils.ts | |
| var import_path4 = __toESM(require("path")); | |
| var import_fs3 = __toESM(require("fs")); | |
| // packages/playwright/src/runner/processHost.ts | |
| var import_child_process = __toESM(require("child_process")); | |
| var import_events = require("events"); | |
| var debug3 = require("playwright-core/lib/utilsBundle").debug; | |
| var { assert } = require("playwright-core/lib/coreBundle").iso; | |
| var { monotonicTime: monotonicTime4, timeOrigin } = require("playwright-core/lib/coreBundle").iso; | |
| var { raceAgainstDeadline: raceAgainstDeadline2 } = require("playwright-core/lib/coreBundle").iso; | |
| var ProcessHost = class extends import_events.EventEmitter { | |
| constructor(entryScript, processName, env) { | |
| super(); | |
| this._didSendStop = false; | |
| this._processDidExit = false; | |
| this._didExitAndRanOnExit = false; | |
| this._lastMessageId = 0; | |
| this._callbacks = /* @__PURE__ */ new Map(); | |
| this._producedEnv = {}; | |
| this._requestHandlers = /* @__PURE__ */ new Map(); | |
| this._entryScript = entryScript; | |
| this._processName = processName; | |
| this._extraEnv = env; | |
| } | |
| async startRunner(runnerParams, options = {}) { | |
| assert(!this.process, "Internal error: starting the same process twice"); | |
| this.process = import_child_process.default.fork(this._entryScript, { | |
| // Note: we pass detached:false, so that workers are in the same process group. | |
| // This way Ctrl+C or a kill command can shutdown all workers in case they misbehave. | |
| // Otherwise user can end up with a bunch of workers stuck in a busy loop without self-destructing. | |
| detached: false, | |
| env: { | |
| ...process.env, | |
| ...this._extraEnv | |
| }, | |
| stdio: [ | |
| "ignore", | |
| options.onStdOut ? "pipe" : "inherit", | |
| options.onStdErr && !process.env.PW_RUNNER_DEBUG ? "pipe" : "inherit", | |
| "ipc" | |
| ] | |
| }); | |
| this.process.on("exit", async (code, signal) => { | |
| this._processDidExit = true; | |
| await this.onExit(); | |
| this._didExitAndRanOnExit = true; | |
| this.emit("exit", { unexpectedly: !this._didSendStop, code, signal }); | |
| }); | |
| this.process.on("error", (e) => { | |
| }); | |
| this.process.on("message", (message) => { | |
| if (debug3.enabled("pw:test:protocol")) | |
| debug3("pw:test:protocol")("\u25C0 RECV " + JSON.stringify(message)); | |
| if (message.method === "__env_produced__") { | |
| const producedEnv = message.params; | |
| this._producedEnv = Object.fromEntries(producedEnv.map((e) => [e[0], e[1] ?? void 0])); | |
| } else if (message.method === "__dispatch__") { | |
| const { id, error: error2, method, params, result } = message.params; | |
| if (id && this._callbacks.has(id)) { | |
| const { resolve, reject } = this._callbacks.get(id); | |
| this._callbacks.delete(id); | |
| if (error2) { | |
| const errorObject = new Error(error2.message); | |
| errorObject.stack = error2.stack; | |
| reject(errorObject); | |
| } else { | |
| resolve(result); | |
| } | |
| } else { | |
| this.emit(method, params); | |
| } | |
| } else if (message.method === "__request__") { | |
| const { id, method, params } = message.params; | |
| const handler = this._requestHandlers.get(method); | |
| if (!handler) { | |
| this.send({ method: "__response__", params: { id, error: { message: "Unknown method" } } }); | |
| } else { | |
| handler(params).then((result) => { | |
| this.send({ method: "__response__", params: { id, result } }); | |
| }).catch((error2) => { | |
| this.send({ method: "__response__", params: { id, error: { message: error2.message } } }); | |
| }); | |
| } | |
| } else { | |
| this.emit(message.method, message.params); | |
| } | |
| }); | |
| if (options.onStdOut) | |
| this.process.stdout?.on("data", options.onStdOut); | |
| if (options.onStdErr) | |
| this.process.stderr?.on("data", options.onStdErr); | |
| const error = await new Promise((resolve) => { | |
| this.process.once("exit", (code, signal) => resolve({ unexpectedly: true, code, signal })); | |
| this.once("ready", () => resolve(void 0)); | |
| }); | |
| if (error) | |
| return error; | |
| const processParams = { | |
| processName: this._processName, | |
| timeOrigin: timeOrigin() | |
| }; | |
| this.send({ | |
| method: "__init__", | |
| params: { | |
| processParams, | |
| runnerParams | |
| } | |
| }); | |
| } | |
| sendMessage(message) { | |
| const id = ++this._lastMessageId; | |
| this.send({ | |
| method: "__dispatch__", | |
| params: { id, ...message } | |
| }); | |
| return new Promise((resolve, reject) => { | |
| this._callbacks.set(id, { resolve, reject }); | |
| }); | |
| } | |
| sendMessageNoReply(message) { | |
| this.sendMessage(message).catch(() => { | |
| }); | |
| } | |
| async onExit() { | |
| } | |
| onRequest(method, handler) { | |
| this._requestHandlers.set(method, handler); | |
| } | |
| async stop() { | |
| if (!this._processDidExit && !this._didSendStop) { | |
| this.send({ method: "__stop__" }); | |
| this._didSendStop = true; | |
| } | |
| if (this._didExitAndRanOnExit) | |
| return; | |
| const exitPromise = new Promise((f) => this.once("exit", () => f())); | |
| const timeout = +(process.env.PWTEST_CHILD_PROCESS_TIMEOUT || 5 * 60 * 1e3); | |
| let lastHeartbeat = monotonicTime4(); | |
| const onHeartbeat = () => lastHeartbeat = monotonicTime4(); | |
| this.on("__heartbeat__", onHeartbeat); | |
| try { | |
| while (true) { | |
| const result = await raceAgainstDeadline2(() => exitPromise, lastHeartbeat + timeout); | |
| if (!result.timedOut) | |
| return; | |
| if (monotonicTime4() < lastHeartbeat + timeout) | |
| continue; | |
| this.emit("processError", { message: `Error: ${this._processName} process did not exit within ${timeout}ms after stop, force-killed it` }); | |
| this._forceKill(); | |
| await exitPromise; | |
| return; | |
| } | |
| } finally { | |
| this.off("__heartbeat__", onHeartbeat); | |
| } | |
| } | |
| _forceKill() { | |
| const pid = this.process?.pid; | |
| if (!pid) | |
| return; | |
| try { | |
| if (process.platform === "win32") | |
| import_child_process.default.spawnSync(`taskkill /pid ${pid} /T /F`, { shell: true }); | |
| else | |
| process.kill(pid, "SIGKILL"); | |
| } catch { | |
| } | |
| } | |
| didSendStop() { | |
| return this._didSendStop; | |
| } | |
| producedEnv() { | |
| return this._producedEnv; | |
| } | |
| send(message) { | |
| if (debug3.enabled("pw:test:protocol")) | |
| debug3("pw:test:protocol")("SEND \u25BA " + JSON.stringify(message)); | |
| this.process?.send(message); | |
| } | |
| }; | |
| // packages/playwright/src/runner/loaderHost.ts | |
| var import_common2 = require("../common"); | |
| var InProcessLoaderHost = class { | |
| constructor(config2) { | |
| this._config = config2; | |
| this._poolBuilder = import_common2.poolBuilder.PoolBuilder.createForLoader(); | |
| } | |
| async start(errors) { | |
| return true; | |
| } | |
| async loadTestFile(file, testErrors) { | |
| const result = await import_common2.testLoader.loadTestFile(file, this._config, testErrors); | |
| this._poolBuilder.buildPools(result, testErrors); | |
| return result; | |
| } | |
| async stop() { | |
| await import_common2.transform.incorporateCompilationCache(); | |
| } | |
| }; | |
| var OutOfProcessLoaderHost = class { | |
| constructor(config2) { | |
| this._config = config2; | |
| this._processHost = new ProcessHost(require.resolve("../loader/loaderProcessEntry.js"), "loader", {}); | |
| } | |
| async start(errors) { | |
| const startError = await this._processHost.startRunner(import_common2.ipc.serializeConfig(this._config, false)); | |
| if (startError) { | |
| errors.push({ | |
| message: `Test loader process failed to start with code "${startError.code}" and signal "${startError.signal}"` | |
| }); | |
| return false; | |
| } | |
| return true; | |
| } | |
| async loadTestFile(file, testErrors) { | |
| const result = await this._processHost.sendMessage({ method: "loadTestFile", params: { file } }); | |
| testErrors.push(...result.testErrors); | |
| return import_common2.test.Suite._deepParse(result.fileSuite); | |
| } | |
| async stop() { | |
| const result = await this._processHost.sendMessage({ method: "getCompilationCacheFromLoader" }); | |
| import_common2.cc.addToCompilationCache(result); | |
| await this._processHost.stop(); | |
| } | |
| }; | |
| // packages/playwright/src/runner/loadUtils.ts | |
| var import_util6 = require("../util"); | |
| // packages/playwright/src/runner/projectUtils.ts | |
| var projectUtils_exports = {}; | |
| __export(projectUtils_exports, { | |
| buildDependentProjects: () => buildDependentProjects, | |
| buildProjectsClosure: () => buildProjectsClosure, | |
| buildTeardownToSetupsMap: () => buildTeardownToSetupsMap, | |
| collectFilesForProject: () => collectFilesForProject, | |
| filterProjects: () => filterProjects, | |
| findTopLevelProjects: () => findTopLevelProjects | |
| }); | |
| var import_fs2 = __toESM(require("fs")); | |
| var import_path3 = __toESM(require("path")); | |
| var import_util4 = require("util"); | |
| var import_util5 = require("../util"); | |
| var { minimatch } = require("playwright-core/lib/utilsBundle"); | |
| var { escapeRegExp } = require("playwright-core/lib/coreBundle").iso; | |
| var readFileAsync = (0, import_util4.promisify)(import_fs2.default.readFile); | |
| var readDirAsync = (0, import_util4.promisify)(import_fs2.default.readdir); | |
| function wildcardPatternToRegExp(pattern) { | |
| return new RegExp("^" + pattern.split("*").map(escapeRegExp).join(".*") + "$", "ig"); | |
| } | |
| function filterProjects(projects, projectNames) { | |
| if (!projectNames) | |
| return [...projects]; | |
| const projectNamesToFind = /* @__PURE__ */ new Set(); | |
| const unmatchedProjectNames = /* @__PURE__ */ new Map(); | |
| const patterns = /* @__PURE__ */ new Set(); | |
| for (const name of projectNames) { | |
| const lowerCaseName = name.toLocaleLowerCase(); | |
| if (lowerCaseName.includes("*")) { | |
| patterns.add(wildcardPatternToRegExp(lowerCaseName)); | |
| } else { | |
| projectNamesToFind.add(lowerCaseName); | |
| unmatchedProjectNames.set(lowerCaseName, name); | |
| } | |
| } | |
| const result = projects.filter((project) => { | |
| const lowerCaseName = project.project.name.toLocaleLowerCase(); | |
| if (projectNamesToFind.has(lowerCaseName)) { | |
| unmatchedProjectNames.delete(lowerCaseName); | |
| return true; | |
| } | |
| for (const regex of patterns) { | |
| regex.lastIndex = 0; | |
| if (regex.test(lowerCaseName)) | |
| return true; | |
| } | |
| return false; | |
| }); | |
| if (unmatchedProjectNames.size) { | |
| const unknownProjectNames = Array.from(unmatchedProjectNames.values()).map((n) => `"${n}"`).join(", "); | |
| throw new Error(`Project(s) ${unknownProjectNames} not found. Available projects: ${projects.map((p) => `"${p.project.name}"`).join(", ")}`); | |
| } | |
| if (!result.length) { | |
| const allProjects = projects.map((p) => `"${p.project.name}"`).join(", "); | |
| throw new Error(`No projects matched. Available projects: ${allProjects}`); | |
| } | |
| return result; | |
| } | |
| function buildTeardownToSetupsMap(projects) { | |
| const result = /* @__PURE__ */ new Map(); | |
| for (const project of projects) { | |
| if (project.teardown) { | |
| const setups = result.get(project.teardown) || []; | |
| setups.push(project); | |
| result.set(project.teardown, setups); | |
| } | |
| } | |
| return result; | |
| } | |
| function buildProjectsClosure(projects, hasTests) { | |
| const result = /* @__PURE__ */ new Map(); | |
| const visit = (depth, project) => { | |
| if (depth > 100) { | |
| const error = new Error("Circular dependency detected between projects."); | |
| error.stack = ""; | |
| throw error; | |
| } | |
| if (depth === 0 && hasTests && !hasTests(project)) | |
| return; | |
| if (result.get(project) !== "dependency") | |
| result.set(project, depth ? "dependency" : "top-level"); | |
| for (const dep of project.deps) | |
| visit(depth + 1, dep); | |
| if (project.teardown) | |
| visit(depth + 1, project.teardown); | |
| }; | |
| for (const p of projects) | |
| visit(0, p); | |
| return result; | |
| } | |
| function findTopLevelProjects(config2) { | |
| const closure = buildProjectsClosure(config2.projects); | |
| return [...closure].filter((entry) => entry[1] === "top-level").map((entry) => entry[0]); | |
| } | |
| function buildDependentProjects(forProjects, projects) { | |
| const reverseDeps = new Map(projects.map((p) => [p, []])); | |
| for (const project of projects) { | |
| for (const dep of project.deps) | |
| reverseDeps.get(dep).push(project); | |
| } | |
| const result = /* @__PURE__ */ new Set(); | |
| const visit = (depth, project) => { | |
| if (depth > 100) { | |
| const error = new Error("Circular dependency detected between projects."); | |
| error.stack = ""; | |
| throw error; | |
| } | |
| result.add(project); | |
| for (const reverseDep of reverseDeps.get(project)) | |
| visit(depth + 1, reverseDep); | |
| if (project.teardown) | |
| visit(depth + 1, project.teardown); | |
| }; | |
| for (const forProject of forProjects) | |
| visit(0, forProject); | |
| return result; | |
| } | |
| async function collectFilesForProject(project, fsCache = /* @__PURE__ */ new Map()) { | |
| const extensions = /* @__PURE__ */ new Set([".js", ".ts", ".mjs", ".mts", ".cjs", ".cts", ".jsx", ".tsx", ".mjsx", ".mtsx", ".cjsx", ".ctsx"]); | |
| const testFileExtension = (file) => extensions.has(import_path3.default.extname(file)); | |
| const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache); | |
| const testMatch = (0, import_util5.createFileMatcher)(project.project.testMatch); | |
| const testIgnore = (0, import_util5.createFileMatcher)(project.project.testIgnore); | |
| const testFiles = allFiles.filter((file) => { | |
| if (!testFileExtension(file)) | |
| return false; | |
| const isTest = !testIgnore(file) && testMatch(file); | |
| if (!isTest) | |
| return false; | |
| return true; | |
| }); | |
| return testFiles; | |
| } | |
| async function cachedCollectFiles(testDir, respectGitIgnore, fsCache) { | |
| const key = testDir + ":" + respectGitIgnore; | |
| let result = fsCache.get(key); | |
| if (!result) { | |
| result = await collectFiles(testDir, respectGitIgnore); | |
| fsCache.set(key, result); | |
| } | |
| return result; | |
| } | |
| async function collectFiles(testDir, respectGitIgnore) { | |
| if (!import_fs2.default.existsSync(testDir)) | |
| return []; | |
| if (!import_fs2.default.statSync(testDir).isDirectory()) | |
| return []; | |
| const checkIgnores = (entryPath, rules, isDirectory, parentStatus) => { | |
| let status = parentStatus; | |
| for (const rule of rules) { | |
| const ruleIncludes = rule.negate; | |
| if (status === "included" === ruleIncludes) | |
| continue; | |
| const relative = import_path3.default.relative(rule.dir, entryPath); | |
| if (rule.match("/" + relative) || rule.match(relative)) { | |
| status = ruleIncludes ? "included" : "ignored"; | |
| } else if (isDirectory && (rule.match("/" + relative + "/") || rule.match(relative + "/"))) { | |
| status = ruleIncludes ? "included" : "ignored"; | |
| } else if (isDirectory && ruleIncludes && (rule.match("/" + relative, true) || rule.match(relative, true))) { | |
| status = "ignored-but-recurse"; | |
| } | |
| } | |
| return status; | |
| }; | |
| const files = []; | |
| const visit = async (dir, rules, status) => { | |
| const entries = await readDirAsync(dir, { withFileTypes: true }); | |
| entries.sort((a, b) => a.name.localeCompare(b.name)); | |
| if (respectGitIgnore) { | |
| const gitignore = entries.find((e) => e.isFile() && e.name === ".gitignore"); | |
| if (gitignore) { | |
| const content = await readFileAsync(import_path3.default.join(dir, gitignore.name), "utf8"); | |
| const newRules = content.split(/\r?\n/).map((s) => { | |
| s = s.trim(); | |
| if (!s) | |
| return; | |
| const rule = new minimatch.Minimatch(s, { matchBase: true, dot: true, flipNegate: true }); | |
| if (rule.comment) | |
| return; | |
| rule.dir = dir; | |
| return rule; | |
| }).filter((rule) => !!rule); | |
| rules = [...rules, ...newRules]; | |
| } | |
| } | |
| for (const entry of entries) { | |
| if (entry.name === "." || entry.name === "..") | |
| continue; | |
| if (entry.isFile() && entry.name === ".gitignore") | |
| continue; | |
| if (entry.isDirectory() && entry.name === "node_modules") | |
| continue; | |
| const entryPath = import_path3.default.join(dir, entry.name); | |
| const entryStatus = checkIgnores(entryPath, rules, entry.isDirectory(), status); | |
| if (entry.isDirectory() && entryStatus !== "ignored") | |
| await visit(entryPath, rules, entryStatus); | |
| else if (entry.isFile() && entryStatus === "included") | |
| files.push(entryPath); | |
| } | |
| }; | |
| await visit(testDir, [], "included"); | |
| return files; | |
| } | |
| // packages/playwright/src/runner/reporterTestRun.ts | |
| var import_common3 = require("../common"); | |
| var ReporterTestRunImpl = class { | |
| constructor(_rootSuite, _readonlyProjectSuites) { | |
| this._rootSuite = _rootSuite; | |
| this._readonlyProjectSuites = _readonlyProjectSuites; | |
| this._active = true; | |
| this._skipSharding = false; | |
| this.skip = import_common3.transform.wrapFunctionWithLocation((location, target, reason) => this._modifier("skip", location, target, reason)); | |
| this.fixme = import_common3.transform.wrapFunctionWithLocation((location, target, reason) => this._modifier("fixme", location, target, reason)); | |
| this.fail = import_common3.transform.wrapFunctionWithLocation((location, target, reason) => this._modifier("fail", location, target, reason)); | |
| } | |
| close() { | |
| this._active = false; | |
| } | |
| shouldSkipSharding() { | |
| return this._skipSharding; | |
| } | |
| skipSharding() { | |
| if (!this._active) | |
| throw new Error(`TestRun.skipSharding() can only be called from Reporter.preprocess().`); | |
| if (this._skipSharding) | |
| throw new Error(`Multiple reporters called 'skipSharding'. Only one reporter may handle sharding.`); | |
| this._skipSharding = true; | |
| } | |
| exclude(target) { | |
| if (!this._active) | |
| throw new Error(`TestRun.exclude() can only be called from Reporter.preprocess().`); | |
| const internalTarget = target; | |
| this._checkTarget("exclude", internalTarget); | |
| internalTarget.parent._detach(internalTarget); | |
| } | |
| _modifier(type, location, _target, reason) { | |
| if (!this._active) | |
| throw new Error(`TestRun.${type}() can only be called from Reporter.preprocess().`); | |
| const target = _target; | |
| this._checkTarget(type, target); | |
| const annotation = { type, description: reason, location }; | |
| if (target instanceof import_common3.test.Suite) { | |
| for (const test of target.allTests()) | |
| test._applyPlanAnnotation(annotation); | |
| } else { | |
| target._applyPlanAnnotation(annotation); | |
| } | |
| } | |
| _checkTarget(method, target) { | |
| if (target === this._rootSuite) | |
| throw new Error(`TestRun.${method}() cannot be called on the root suite.`); | |
| let suite = target instanceof import_common3.test.Suite ? target : target.parent; | |
| while (suite) { | |
| if (this._readonlyProjectSuites.has(suite)) { | |
| const targetType = target instanceof import_common3.test.TestCase ? " test" : ""; | |
| throw new Error(`TestRun.${method}() cannot be called on a setup or teardown project${targetType}; these always run in full.`); | |
| } | |
| suite = suite.parent; | |
| } | |
| } | |
| }; | |
| // packages/playwright/src/runner/testGroups.ts | |
| function createTestGroups(projectSuite, expectedParallelism) { | |
| const groups = /* @__PURE__ */ new Map(); | |
| const createGroup = (test) => { | |
| return { | |
| workerHash: test._workerHash, | |
| requireFile: test._requireFile, | |
| repeatEachIndex: test.repeatEachIndex, | |
| projectId: test._projectId, | |
| tests: [], | |
| locks: [] | |
| }; | |
| }; | |
| for (const test of projectSuite.allTests()) { | |
| let withWorkerHash = groups.get(test._workerHash); | |
| if (!withWorkerHash) { | |
| withWorkerHash = /* @__PURE__ */ new Map(); | |
| groups.set(test._workerHash, withWorkerHash); | |
| } | |
| let withRequireFile = withWorkerHash.get(test._requireFile); | |
| if (!withRequireFile) { | |
| withRequireFile = { | |
| general: createGroup(test), | |
| parallel: /* @__PURE__ */ new Map(), | |
| parallelWithHooks: createGroup(test) | |
| }; | |
| withWorkerHash.set(test._requireFile, withRequireFile); | |
| } | |
| let insideParallel = false; | |
| let outerMostSequentialSuite; | |
| let hasAllHooks = false; | |
| for (let parent = test.parent; parent; parent = parent.parent) { | |
| if (parent._parallelMode === "serial" || parent._parallelMode === "default") | |
| outerMostSequentialSuite = parent; | |
| insideParallel = insideParallel || parent._parallelMode === "parallel"; | |
| hasAllHooks = hasAllHooks || parent._hooks.some((hook) => hook.type === "beforeAll" || hook.type === "afterAll"); | |
| } | |
| if (insideParallel) { | |
| if (hasAllHooks && !outerMostSequentialSuite) { | |
| withRequireFile.parallelWithHooks.tests.push(test); | |
| } else { | |
| const key = outerMostSequentialSuite || test; | |
| let group = withRequireFile.parallel.get(key); | |
| if (!group) { | |
| group = createGroup(test); | |
| withRequireFile.parallel.set(key, group); | |
| } | |
| group.tests.push(test); | |
| } | |
| } else { | |
| withRequireFile.general.tests.push(test); | |
| } | |
| } | |
| const result = []; | |
| for (const withWorkerHash of groups.values()) { | |
| for (const withRequireFile of withWorkerHash.values()) { | |
| if (withRequireFile.general.tests.length) | |
| result.push(withRequireFile.general); | |
| result.push(...withRequireFile.parallel.values()); | |
| const parallelWithHooksGroupSize = Math.ceil(withRequireFile.parallelWithHooks.tests.length / expectedParallelism); | |
| let lastGroup; | |
| for (const test of withRequireFile.parallelWithHooks.tests) { | |
| if (!lastGroup || lastGroup.tests.length >= parallelWithHooksGroupSize) { | |
| lastGroup = createGroup(test); | |
| result.push(lastGroup); | |
| } | |
| lastGroup.tests.push(test); | |
| } | |
| } | |
| } | |
| for (const group of result) { | |
| const locks = /* @__PURE__ */ new Set(); | |
| for (const test of group.tests) { | |
| for (const lock of test._locks) | |
| locks.add(lock); | |
| } | |
| group.locks = [...locks]; | |
| } | |
| return result; | |
| } | |
| function filterForShard(shard, weights, testGroups) { | |
| weights ??= Array.from({ length: shard.total }, () => 1); | |
| if (weights.length !== shard.total) | |
| throw new Error(`PWTEST_SHARD_WEIGHTS number of weights must match the shard total of ${shard.total}`); | |
| const totalWeight = weights.reduce((a, b) => a + b, 0); | |
| let shardableTotal = 0; | |
| for (const group of testGroups) | |
| shardableTotal += group.tests.length; | |
| const shardSizes = weights.map((w) => Math.floor(w * shardableTotal / totalWeight)); | |
| const remainder = shardableTotal - shardSizes.reduce((a, b) => a + b, 0); | |
| for (let i = 0; i < remainder; i++) { | |
| shardSizes[i % shardSizes.length]++; | |
| } | |
| let from = 0; | |
| for (let i = 0; i < shard.current - 1; i++) | |
| from += shardSizes[i]; | |
| const to = from + shardSizes[shard.current - 1]; | |
| let current = 0; | |
| const result = /* @__PURE__ */ new Set(); | |
| for (const group of testGroups) { | |
| if (current >= from && current < to) | |
| result.add(group); | |
| current += group.tests.length; | |
| } | |
| return result; | |
| } | |
| // packages/playwright/src/runner/loadUtils.ts | |
| var import_common4 = require("../common"); | |
| var sourceMapSupport = require("playwright-core/lib/utilsBundle").sourceMapSupport; | |
| var { toPosixPath } = require("playwright-core/lib/coreBundle").utils; | |
| async function collectProjectsAndTestFiles(testRun, doNotRunTestsOutsideProjectFilter) { | |
| const fsCache = /* @__PURE__ */ new Map(); | |
| const sourceMapCache = /* @__PURE__ */ new Map(); | |
| const allFilesForProject = /* @__PURE__ */ new Map(); | |
| for (const project of testRun.filteredProjects) { | |
| const files = await collectFilesForProject(project, fsCache); | |
| allFilesForProject.set(project, files); | |
| } | |
| const filesToRunByProject = /* @__PURE__ */ new Map(); | |
| for (const [project, files] of allFilesForProject) { | |
| const matchedFiles = files.filter((file) => { | |
| if (!testRun.loadFileFilters.length) { | |
| return true; | |
| } | |
| const hasMatchingSources = sourceMapSources(file, sourceMapCache).some((source) => { | |
| const matchesAllFileFilters = testRun.loadFileFilters.every((filter) => filter(source)); | |
| return matchesAllFileFilters; | |
| }); | |
| return hasMatchingSources; | |
| }); | |
| const filteredFiles = matchedFiles.filter(Boolean); | |
| filesToRunByProject.set(project, filteredFiles); | |
| } | |
| const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]); | |
| for (const [project, type] of projectClosure) { | |
| if (type === "dependency") { | |
| const treatProjectAsEmpty = doNotRunTestsOutsideProjectFilter && !testRun.filteredProjects.includes(project); | |
| const files = treatProjectAsEmpty ? [] : allFilesForProject.get(project) || await collectFilesForProject(project, fsCache); | |
| filesToRunByProject.set(project, files); | |
| } | |
| } | |
| testRun.projectFiles = filesToRunByProject; | |
| testRun.projectSuites = /* @__PURE__ */ new Map(); | |
| } | |
| async function loadFileSuites(testRun, mode, errors) { | |
| const config2 = testRun.config; | |
| const allTestFiles = /* @__PURE__ */ new Set(); | |
| for (const files of testRun.projectFiles.values()) | |
| files.forEach((file) => allTestFiles.add(file)); | |
| const fileSuiteByFile = /* @__PURE__ */ new Map(); | |
| const loaderHost = mode === "out-of-process" ? new OutOfProcessLoaderHost(config2) : new InProcessLoaderHost(config2); | |
| if (await loaderHost.start(errors)) { | |
| for (const file of allTestFiles) { | |
| const fileSuite = await loaderHost.loadTestFile(file, errors); | |
| fileSuiteByFile.set(file, fileSuite); | |
| errors.push(...createDuplicateTitlesErrors(config2, fileSuite)); | |
| } | |
| await loaderHost.stop(); | |
| } | |
| for (const file of allTestFiles) { | |
| for (const dependency of import_common4.cc.dependenciesForTestFile(file)) { | |
| if (allTestFiles.has(dependency)) { | |
| const importer = import_path4.default.relative(config2.config.rootDir, file); | |
| const importee = import_path4.default.relative(config2.config.rootDir, dependency); | |
| errors.push({ | |
| message: `Error: test file "${importer}" should not import test file "${importee}"`, | |
| location: { file, line: 1, column: 1 } | |
| }); | |
| } | |
| } | |
| } | |
| for (const [project, files] of testRun.projectFiles) { | |
| const suites = files.map((file) => fileSuiteByFile.get(file)).filter(Boolean); | |
| testRun.projectSuites.set(project, suites); | |
| } | |
| } | |
| async function createRootSuite(testRun, errors, shouldFilterOnly) { | |
| const config2 = testRun.config; | |
| const rootSuite = new import_common4.test.Suite("", "root"); | |
| const projectSuites = /* @__PURE__ */ new Map(); | |
| const filteredProjectSuites = /* @__PURE__ */ new Map(); | |
| { | |
| for (const [project, fileSuites] of testRun.projectSuites) { | |
| const projectSuite = createProjectSuite(project, fileSuites); | |
| projectSuites.set(project, projectSuite); | |
| const filteredProjectSuite = filterProjectSuite(projectSuite, testRun.preOnlyTestFilters); | |
| filteredProjectSuites.set(project, filteredProjectSuite); | |
| } | |
| } | |
| if (shouldFilterOnly) { | |
| const filteredRoot = new import_common4.test.Suite("", "root"); | |
| for (const filteredProjectSuite of filteredProjectSuites.values()) | |
| filteredRoot._addSuite(filteredProjectSuite); | |
| import_common4.suiteUtils.filterOnly(filteredRoot); | |
| for (const [project, filteredProjectSuite] of filteredProjectSuites) { | |
| if (!filteredRoot.suites.includes(filteredProjectSuite)) | |
| filteredProjectSuites.delete(project); | |
| } | |
| } | |
| const projectClosure = buildProjectsClosure([...filteredProjectSuites.keys()], (project) => filteredProjectSuites.get(project)._hasTests()); | |
| for (const [project, type] of projectClosure) { | |
| if (type === "top-level") { | |
| project.project.repeatEach = project.fullConfig.configCLIOverrides.repeatEach ?? project.project.repeatEach; | |
| rootSuite._addSuite(buildProjectSuite(project, filteredProjectSuites.get(project))); | |
| } | |
| } | |
| if (config2.config.forbidOnly) { | |
| const onlyTestsAndSuites = rootSuite._getOnlyItems(); | |
| if (onlyTestsAndSuites.length > 0) { | |
| const configFilePath = config2.config.configFile ? import_path4.default.relative(config2.config.rootDir, config2.config.configFile) : void 0; | |
| errors.push(...createForbidOnlyErrors(onlyTestsAndSuites, config2.configCLIOverrides.forbidOnly, configFilePath)); | |
| } | |
| } | |
| const dependencySuites = /* @__PURE__ */ new Map(); | |
| for (const [project, type] of projectClosure) { | |
| if (type !== "dependency") | |
| continue; | |
| const dependencySuite = buildProjectSuite(project, projectSuites.get(project)); | |
| dependencySuites.set(project, dependencySuite); | |
| rootSuite._prependSuite(dependencySuite); | |
| } | |
| const reporterTestRun = new ReporterTestRunImpl(rootSuite, new Set(dependencySuites.values())); | |
| await testRun.reporter.preprocess({ | |
| config: config2.config, | |
| suite: rootSuite, | |
| testRun: reporterTestRun | |
| }); | |
| reporterTestRun.close(); | |
| for (const dependencySuite of dependencySuites.values()) | |
| rootSuite._detach(dependencySuite); | |
| if (config2.config.shard && !reporterTestRun.shouldSkipSharding()) { | |
| const testGroups = []; | |
| for (const projectSuite of rootSuite.suites) { | |
| for (const group of createTestGroups(projectSuite, config2.config.shard.total)) | |
| testGroups.push(group); | |
| } | |
| const testGroupsInThisShard = filterForShard(config2.config.shard, testRun.options.shardWeights, testGroups); | |
| const testsInThisShard = /* @__PURE__ */ new Set(); | |
| for (const group of testGroupsInThisShard) { | |
| for (const test of group.tests) | |
| testsInThisShard.add(test); | |
| } | |
| import_common4.suiteUtils.filterTestsRemoveEmptySuites(rootSuite, (test) => testsInThisShard.has(test)); | |
| } | |
| if (testRun.postShardTestFilters.length) | |
| import_common4.suiteUtils.filterTestsRemoveEmptySuites(rootSuite, (test) => testRun.postShardTestFilters.every((filter) => filter(test))); | |
| const topLevelProjects = []; | |
| { | |
| const finalProjectClosure = buildProjectsClosure(rootSuite.suites.map((suite) => suite._fullProject)); | |
| for (const [project, type] of finalProjectClosure) { | |
| if (type === "dependency") | |
| rootSuite._addSuite(dependencySuites.get(project)); | |
| else | |
| topLevelProjects.push(project); | |
| } | |
| } | |
| rootSuite._entries.sort((a, b) => config2.projects.indexOf(a._fullProject) - config2.projects.indexOf(b._fullProject)); | |
| testRun.rootSuite = rootSuite; | |
| testRun.topLevelProjects = topLevelProjects; | |
| } | |
| function createProjectSuite(project, fileSuites) { | |
| const projectSuite = new import_common4.test.Suite(project.project.name, "project"); | |
| for (const fileSuite of fileSuites) | |
| projectSuite._addSuite(import_common4.suiteUtils.bindFileSuiteToProject(project, fileSuite)); | |
| const grepMatcher = (0, import_util6.createTitleMatcher)(project.project.grep); | |
| const grepInvertMatcher = project.project.grepInvert ? (0, import_util6.createTitleMatcher)(project.project.grepInvert) : null; | |
| import_common4.suiteUtils.filterTestsRemoveEmptySuites(projectSuite, (test) => { | |
| const grepTitle = test._grepTitleWithTags(); | |
| if (grepInvertMatcher?.(grepTitle)) | |
| return false; | |
| return grepMatcher(grepTitle); | |
| }); | |
| return projectSuite; | |
| } | |
| function filterProjectSuite(projectSuite, testFilters) { | |
| if (!testFilters.length) | |
| return projectSuite; | |
| const result = projectSuite._deepClone(); | |
| import_common4.suiteUtils.filterTestsRemoveEmptySuites(result, (test) => testFilters.every((filter) => filter(test))); | |
| return result; | |
| } | |
| function buildProjectSuite(project, projectSuite) { | |
| const result = new import_common4.test.Suite(project.project.name, "project"); | |
| result._fullProject = project; | |
| if (project.fullyParallel) | |
| result._parallelMode = "parallel"; | |
| for (const fileSuite of projectSuite.suites) { | |
| result._addSuite(fileSuite); | |
| for (let repeatEachIndex = 1; repeatEachIndex < project.project.repeatEach; repeatEachIndex++) { | |
| const clone = fileSuite._deepClone(); | |
| import_common4.suiteUtils.applyRepeatEachIndex(project, clone, repeatEachIndex); | |
| result._addSuite(clone); | |
| } | |
| } | |
| return result; | |
| } | |
| function createForbidOnlyErrors(onlyTestsAndSuites, forbidOnlyCLIFlag, configFilePath) { | |
| const errors = []; | |
| for (const testOrSuite of onlyTestsAndSuites) { | |
| const title = testOrSuite.titlePath().slice(2).join(" "); | |
| const configFilePathName = configFilePath ? `'${configFilePath}'` : "the Playwright configuration file"; | |
| const forbidOnlySource = forbidOnlyCLIFlag ? `'--forbid-only' CLI flag` : `'forbidOnly' option in ${configFilePathName}`; | |
| const error = { | |
| message: `Error: item focused with '.only' is not allowed due to the ${forbidOnlySource}: "${title}"`, | |
| location: testOrSuite.location | |
| }; | |
| errors.push(error); | |
| } | |
| return errors; | |
| } | |
| function createDuplicateTitlesErrors(config2, fileSuite) { | |
| const errors = []; | |
| const testsByFullTitle = /* @__PURE__ */ new Map(); | |
| for (const test of fileSuite.allTests()) { | |
| const fullTitle = test.titlePath().slice(1).join(" \u203A "); | |
| const existingTest = testsByFullTitle.get(fullTitle); | |
| if (existingTest) { | |
| const error = { | |
| message: `Error: duplicate test title "${fullTitle}", first declared in ${buildItemLocation(config2.config.rootDir, existingTest)}`, | |
| location: test.location | |
| }; | |
| errors.push(error); | |
| } | |
| testsByFullTitle.set(fullTitle, test); | |
| } | |
| return errors; | |
| } | |
| function buildItemLocation(rootDir, testOrSuite) { | |
| if (!testOrSuite.location) | |
| return ""; | |
| return `${import_path4.default.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`; | |
| } | |
| async function requireOrImportDefaultFunction(file, expectConstructor) { | |
| let func = await import_common4.transform.requireOrImport(file); | |
| if (func && typeof func === "object" && "default" in func) | |
| func = func["default"]; | |
| if (typeof func !== "function") | |
| throw (0, import_util6.errorWithFile)(file, `file must export a single ${expectConstructor ? "class" : "function"}.`); | |
| return func; | |
| } | |
| function loadGlobalHook(config2, file) { | |
| return requireOrImportDefaultFunction(import_path4.default.resolve(config2.config.rootDir, file), false); | |
| } | |
| function loadReporter(config2, file) { | |
| return requireOrImportDefaultFunction(config2 ? import_path4.default.resolve(config2.config.rootDir, file) : file, true); | |
| } | |
| function sourceMapSources(file, cache) { | |
| let sources = [file]; | |
| if (!file.endsWith(".js")) | |
| return sources; | |
| if (cache.has(file)) | |
| return cache.get(file); | |
| try { | |
| const sourceMap = sourceMapSupport.retrieveSourceMap(file); | |
| const sourceMapData = typeof sourceMap?.map === "string" ? JSON.parse(sourceMap.map) : sourceMap?.map; | |
| if (sourceMapData?.sources) | |
| sources = sourceMapData.sources.map((source) => import_path4.default.resolve(import_path4.default.dirname(file), source)); | |
| } finally { | |
| cache.set(file, sources); | |
| return sources; | |
| } | |
| } | |
| async function loadTestList(config2, filePath) { | |
| try { | |
| const content = await import_fs3.default.promises.readFile(filePath, "utf-8"); | |
| const lines = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")); | |
| const descriptions = lines.map((line) => { | |
| const delimiter = line.includes("\u203A") ? "\u203A" : ">"; | |
| const tokens = line.split(delimiter).map((token) => token.trim()); | |
| let project; | |
| if (tokens[0].startsWith("[")) { | |
| if (!tokens[0].endsWith("]")) | |
| throw new Error(`Malformed test description: ${line}`); | |
| project = tokens[0].substring(1, tokens[0].length - 1); | |
| tokens.shift(); | |
| } | |
| return { project, file: toPosixPath((0, import_util6.parseLocationArg)(tokens[0]).file), titlePath: tokens.slice(1) }; | |
| }); | |
| const testFilter = (test) => descriptions.some((d) => { | |
| const [projectName, , ...titles] = test.titlePath(); | |
| if (d.project !== void 0 && d.project !== projectName) | |
| return false; | |
| const relativeFile = toPosixPath(import_path4.default.relative(config2.config.rootDir, test.location.file)); | |
| if (relativeFile !== d.file) | |
| return false; | |
| return d.titlePath.length <= titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]); | |
| }); | |
| const fileFilter = (file) => { | |
| const relativeFile = toPosixPath(import_path4.default.relative(config2.config.rootDir, file)); | |
| return descriptions.some((d) => d.file === relativeFile); | |
| }; | |
| return { testFilter, fileFilter }; | |
| } catch (e) { | |
| throw (0, import_util6.errorWithFile)(filePath, "Cannot read test list file: " + e.message); | |
| } | |
| } | |
| // packages/playwright/src/reporters/blob.ts | |
| var import_fs4 = __toESM(require("fs")); | |
| var import_path6 = __toESM(require("path")); | |
| var import_stream2 = require("stream"); | |
| var import_coreBundle = require("playwright-core/lib/coreBundle"); | |
| // packages/playwright/src/reporters/teleEmitter.ts | |
| var import_path5 = __toESM(require("path")); | |
| var { createGuid } = require("playwright-core/lib/coreBundle").utils; | |
| var TeleReporterEmitter = class { | |
| constructor(messageSink, options = {}) { | |
| this._resultKnownAttachmentCounts = /* @__PURE__ */ new Map(); | |
| this._resultKnownErrorCounts = /* @__PURE__ */ new Map(); | |
| // In case there is blob reporter and UI mode, make sure one doesn't override | |
| // the id assigned by the other. | |
| this._idSymbol = /* @__PURE__ */ Symbol("id"); | |
| this._messageSink = messageSink; | |
| this._emitterOptions = options; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| onConfigure(config2) { | |
| this._rootDir = config2.rootDir; | |
| this._messageSink({ method: "onConfigure", params: { config: this._serializeConfig(config2) } }); | |
| } | |
| onBegin(suite) { | |
| const projects = suite.suites.map((projectSuite) => this._serializeProject(projectSuite)); | |
| for (const project of projects) | |
| this._messageSink({ method: "onProject", params: { project } }); | |
| this._messageSink({ method: "onBegin", params: void 0 }); | |
| } | |
| onTestBegin(test, result) { | |
| result[this._idSymbol] = createGuid(); | |
| this._messageSink({ | |
| method: "onTestBegin", | |
| params: { | |
| testId: test.id, | |
| result: this._serializeResultStart(result) | |
| } | |
| }); | |
| } | |
| async onTestPaused(test, result) { | |
| const resultId = result[this._idSymbol]; | |
| this._resultKnownErrorCounts.set(resultId, result.errors.length); | |
| this._messageSink({ | |
| method: "onTestPaused", | |
| params: { | |
| testId: test.id, | |
| resultId, | |
| errors: result.errors | |
| } | |
| }); | |
| await new Promise(() => { | |
| }); | |
| } | |
| onTestEnd(test, result) { | |
| const testEnd = { | |
| testId: test.id, | |
| expectedStatus: test.expectedStatus, | |
| timeout: test.timeout, | |
| annotations: [] | |
| }; | |
| this._sendNewAttachments(result, test.id); | |
| this._messageSink({ | |
| method: "onTestEnd", | |
| params: { | |
| test: testEnd, | |
| result: this._serializeResultEnd(result) | |
| } | |
| }); | |
| const resultId = result[this._idSymbol]; | |
| this._resultKnownAttachmentCounts.delete(resultId); | |
| this._resultKnownErrorCounts.delete(resultId); | |
| } | |
| onStepBegin(test, result, step) { | |
| step[this._idSymbol] = createGuid(); | |
| this._messageSink({ | |
| method: "onStepBegin", | |
| params: { | |
| testId: test.id, | |
| resultId: result[this._idSymbol], | |
| step: this._serializeStepStart(step) | |
| } | |
| }); | |
| } | |
| onStepEnd(test, result, step) { | |
| const resultId = result[this._idSymbol]; | |
| this._sendNewAttachments(result, test.id); | |
| this._messageSink({ | |
| method: "onStepEnd", | |
| params: { | |
| testId: test.id, | |
| resultId, | |
| step: this._serializeStepEnd(step, result) | |
| } | |
| }); | |
| } | |
| onError(error, workerInfo) { | |
| this._messageSink({ | |
| method: "onError", | |
| params: { | |
| error, | |
| workerInfo: workerInfo ? { | |
| workerIndex: workerInfo.workerIndex, | |
| parallelIndex: workerInfo.parallelIndex, | |
| projectName: workerInfo.project.name | |
| } : void 0 | |
| } | |
| }); | |
| } | |
| onStdOut(chunk, test, result) { | |
| this._onStdIO("stdout", chunk, test, result); | |
| } | |
| onStdErr(chunk, test, result) { | |
| this._onStdIO("stderr", chunk, test, result); | |
| } | |
| _onStdIO(type, chunk, test, result) { | |
| if (this._emitterOptions.omitOutput) | |
| return; | |
| const isBase64 = typeof chunk !== "string"; | |
| const data = isBase64 ? chunk.toString("base64") : chunk; | |
| this._messageSink({ | |
| method: "onStdIO", | |
| params: { testId: test?.id, resultId: result ? result[this._idSymbol] : void 0, type, data, isBase64 } | |
| }); | |
| } | |
| async onEnd(result) { | |
| const resultPayload = { | |
| status: result.status, | |
| startTime: result.startTime.getTime(), | |
| duration: result.duration | |
| }; | |
| this._messageSink({ | |
| method: "onEnd", | |
| params: { | |
| result: resultPayload | |
| } | |
| }); | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| _serializeConfig(config2) { | |
| return { | |
| configFile: this._relativePath(config2.configFile), | |
| globalTimeout: config2.globalTimeout, | |
| maxFailures: config2.maxFailures, | |
| metadata: config2.metadata, | |
| rootDir: config2.rootDir, | |
| shard: config2.shard, | |
| version: config2.version, | |
| workers: config2.workers, | |
| globalSetup: config2.globalSetup, | |
| globalTeardown: config2.globalTeardown, | |
| tags: config2.tags, | |
| webServer: config2.webServer, | |
| failOnFlakyTests: config2.failOnFlakyTests | |
| }; | |
| } | |
| _serializeProject(suite) { | |
| const project = suite.project(); | |
| const report = { | |
| metadata: project.metadata, | |
| name: project.name, | |
| outputDir: this._relativePath(project.outputDir), | |
| repeatEach: project.repeatEach, | |
| retries: project.retries, | |
| testDir: this._relativePath(project.testDir), | |
| testIgnore: serializeRegexPatterns(project.testIgnore), | |
| testMatch: serializeRegexPatterns(project.testMatch), | |
| timeout: project.timeout, | |
| suites: suite.suites.map((fileSuite) => { | |
| return this._serializeSuite(fileSuite); | |
| }), | |
| grep: serializeRegexPatterns(project.grep), | |
| grepInvert: serializeRegexPatterns(project.grepInvert || []), | |
| dependencies: project.dependencies, | |
| snapshotDir: this._relativePath(project.snapshotDir), | |
| teardown: project.teardown, | |
| ignoreSnapshots: project.ignoreSnapshots ? true : void 0, | |
| use: this._serializeProjectUseOptions(project.use) | |
| }; | |
| return report; | |
| } | |
| _serializeProjectUseOptions(use) { | |
| return { | |
| testIdAttribute: use.testIdAttribute | |
| }; | |
| } | |
| _serializeSuite(suite) { | |
| const result = { | |
| title: suite.title, | |
| location: this._relativeLocation(suite.location), | |
| entries: suite.entries().map((e) => { | |
| if (e.type === "test") | |
| return this._serializeTest(e); | |
| return this._serializeSuite(e); | |
| }) | |
| }; | |
| return result; | |
| } | |
| _serializeTest(test) { | |
| return { | |
| testId: test.id, | |
| title: test.title, | |
| location: this._relativeLocation(test.location), | |
| retries: test.retries, | |
| tags: test.tags, | |
| repeatEachIndex: test.repeatEachIndex, | |
| annotations: this._relativeAnnotationLocations(test.annotations) | |
| }; | |
| } | |
| _serializeResultStart(result) { | |
| return { | |
| id: result[this._idSymbol], | |
| retry: result.retry, | |
| workerIndex: result.workerIndex, | |
| parallelIndex: result.parallelIndex, | |
| startTime: +result.startTime | |
| }; | |
| } | |
| _serializeResultEnd(result) { | |
| const id = result[this._idSymbol]; | |
| return { | |
| id, | |
| duration: result.duration, | |
| status: result.status, | |
| errors: this._resultKnownErrorCounts.has(id) ? result.errors.slice(this._resultKnownAttachmentCounts.get(id)) : result.errors, | |
| annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0 | |
| }; | |
| } | |
| _sendNewAttachments(result, testId) { | |
| const resultId = result[this._idSymbol]; | |
| const knownAttachmentCount = this._resultKnownAttachmentCounts.get(resultId) ?? 0; | |
| if (result.attachments.length > knownAttachmentCount) { | |
| this._messageSink({ | |
| method: "onAttach", | |
| params: { | |
| testId, | |
| resultId, | |
| attachments: this._serializeAttachments(result.attachments.slice(knownAttachmentCount)) | |
| } | |
| }); | |
| } | |
| this._resultKnownAttachmentCounts.set(resultId, result.attachments.length); | |
| } | |
| _serializeAttachments(attachments) { | |
| return attachments.map((a) => { | |
| const { body, ...rest } = a; | |
| return { | |
| ...rest, | |
| // There is no Buffer in the browser, so there is no point in sending the data there. | |
| base64: body && !this._emitterOptions.omitBuffers ? body.toString("base64") : void 0 | |
| }; | |
| }); | |
| } | |
| _serializeStepStart(step) { | |
| return { | |
| id: step[this._idSymbol], | |
| parentStepId: step.parent?.[this._idSymbol], | |
| title: step.title, | |
| subtitle: step.subtitle, | |
| category: step.category, | |
| params: step.params, | |
| startTime: +step.startTime, | |
| location: this._relativeLocation(step.location) | |
| }; | |
| } | |
| _serializeStepEnd(step, result) { | |
| return { | |
| id: step[this._idSymbol], | |
| duration: step.duration, | |
| error: step.error, | |
| attachments: step.attachments.length ? step.attachments.map((a) => result.attachments.indexOf(a)) : void 0, | |
| annotations: step.annotations.length ? this._relativeAnnotationLocations(step.annotations) : void 0 | |
| }; | |
| } | |
| _relativeAnnotationLocations(annotations) { | |
| return annotations.map((annotation) => ({ | |
| ...annotation, | |
| location: annotation.location ? this._relativeLocation(annotation.location) : void 0 | |
| })); | |
| } | |
| _relativeLocation(location) { | |
| if (!location) | |
| return location; | |
| return { | |
| ...location, | |
| file: this._relativePath(location.file) | |
| }; | |
| } | |
| _relativePath(absolutePath) { | |
| if (!absolutePath) | |
| return absolutePath; | |
| return import_path5.default.relative(this._rootDir, absolutePath); | |
| } | |
| }; | |
| // packages/playwright/src/reporters/blob.ts | |
| var mime = require("playwright-core/lib/utilsBundle").mime; | |
| var yazl = require("playwright-core/lib/utilsBundle").yazl; | |
| var { ManualPromise: ManualPromise2 } = require("playwright-core/lib/coreBundle").iso; | |
| var { calculateSha1, createGuid: createGuid2 } = require("playwright-core/lib/coreBundle").utils; | |
| var { removeFolders, sanitizeForFilePath } = require("playwright-core/lib/coreBundle").utils; | |
| var currentBlobReportVersion = 2; | |
| var BlobReporter = class extends TeleReporterEmitter { | |
| constructor(options) { | |
| super((message) => this._messages.push(message)); | |
| this._messages = []; | |
| this._attachments = []; | |
| this._options = options; | |
| if (this._options.fileName && !this._options.fileName.endsWith(".zip")) | |
| throw new Error(`Blob report file name must end with .zip extension: ${this._options.fileName}`); | |
| this._salt = createGuid2(); | |
| } | |
| onConfigure(config2) { | |
| const metadata = { | |
| version: currentBlobReportVersion, | |
| userAgent: (0, import_coreBundle.getUserAgent)(), | |
| // TODO: remove after some time, recommend config.tag instead. | |
| name: process.env.PWTEST_BOT_NAME, | |
| shard: config2.shard ?? void 0, | |
| pathSeparator: import_path6.default.sep | |
| }; | |
| this._messages.push({ | |
| method: "onBlobReportMetadata", | |
| params: metadata | |
| }); | |
| this._config = config2; | |
| super.onConfigure(config2); | |
| } | |
| async onTestPaused(test, result) { | |
| } | |
| async onEnd(result) { | |
| await super.onEnd(result); | |
| const zipFileName = await this._prepareOutputFile(); | |
| const zipFile = new yazl.ZipFile(); | |
| const zipFinishPromise = new ManualPromise2(); | |
| const finishPromise = zipFinishPromise.catch((e) => { | |
| throw new Error(`Failed to write report ${zipFileName}: ` + e.message); | |
| }); | |
| zipFile.on("error", (error) => zipFinishPromise.reject(error)); | |
| zipFile.outputStream.pipe(import_fs4.default.createWriteStream(zipFileName)).on("close", () => { | |
| zipFinishPromise.resolve(void 0); | |
| }).on("error", (error) => zipFinishPromise.reject(error)); | |
| for (const { originalPath, zipEntryPath } of this._attachments) { | |
| if (!import_fs4.default.statSync(originalPath, { throwIfNoEntry: false })?.isFile()) | |
| continue; | |
| zipFile.addFile(originalPath, zipEntryPath); | |
| } | |
| const lines = this._messages.map((m) => JSON.stringify(m) + "\n"); | |
| const content = import_stream2.Readable.from(lines); | |
| zipFile.addReadStream(content, "report.jsonl"); | |
| zipFile.end(); | |
| await finishPromise; | |
| } | |
| async _prepareOutputFile() { | |
| const { outputFile, outputDir } = resolveOutputFile("BLOB", { | |
| ...this._options, | |
| default: { | |
| fileName: this._defaultReportName(this._config), | |
| outputDir: "blob-report" | |
| } | |
| }); | |
| if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE) | |
| await removeFolders([outputDir]); | |
| await import_fs4.default.promises.mkdir(import_path6.default.dirname(outputFile), { recursive: true }); | |
| return outputFile; | |
| } | |
| _defaultReportName(config2) { | |
| let reportName = "report"; | |
| if (this._options._commandHash) | |
| reportName += "-" + sanitizeForFilePath(this._options._commandHash); | |
| if (config2.shard) { | |
| const paddedNumber = `${config2.shard.current}`.padStart(`${config2.shard.total}`.length, "0"); | |
| reportName = `${reportName}-${paddedNumber}`; | |
| } | |
| return `${reportName}.zip`; | |
| } | |
| _serializeAttachments(attachments) { | |
| return super._serializeAttachments(attachments).map((attachment) => { | |
| if (!attachment.path) | |
| return attachment; | |
| const sha1 = calculateSha1(attachment.path + this._salt); | |
| const extension = mime.getExtension(attachment.contentType) || "dat"; | |
| const newPath = `resources/${sha1}.${extension}`; | |
| this._attachments.push({ originalPath: attachment.path, zipEntryPath: newPath }); | |
| return { | |
| ...attachment, | |
| path: newPath | |
| }; | |
| }); | |
| } | |
| }; | |
| // packages/playwright/src/reporters/dot.ts | |
| var { getAsBooleanFromENV } = require("playwright-core/lib/coreBundle").utils; | |
| var DotReporter = class extends TerminalReporter { | |
| constructor(options) { | |
| super({ ...options, omitTags: getAsBooleanFromENV("PLAYWRIGHT_DOT_OMIT_TAGS", options?.omitTags) }); | |
| this._counter = 0; | |
| } | |
| onBegin(suite) { | |
| super.onBegin(suite); | |
| this.writeLine(this.generateStartingMessage()); | |
| } | |
| onStdOut(chunk, test, result) { | |
| super.onStdOut(chunk, test, result); | |
| if (!this.config.quiet) | |
| this.screen.stdout.write(chunk); | |
| } | |
| onStdErr(chunk, test, result) { | |
| super.onStdErr(chunk, test, result); | |
| if (!this.config.quiet) | |
| this.screen.stderr.write(chunk); | |
| } | |
| onTestEnd(test, result) { | |
| super.onTestEnd(test, result); | |
| if (this._counter === 80) { | |
| this.screen.stdout.write("\n"); | |
| this._counter = 0; | |
| } | |
| ++this._counter; | |
| if (result.status === "skipped") { | |
| this.screen.stdout.write(this.screen.colors.yellow("\xB0")); | |
| return; | |
| } | |
| if (this.willRetry(test)) { | |
| this.screen.stdout.write(this.screen.colors.gray("\xD7")); | |
| return; | |
| } | |
| switch (test.outcome()) { | |
| case "expected": | |
| this.screen.stdout.write(this.screen.colors.green("\xB7")); | |
| break; | |
| case "unexpected": | |
| this.screen.stdout.write(this.screen.colors.red(result.status === "timedOut" ? "T" : "F")); | |
| break; | |
| case "flaky": | |
| this.screen.stdout.write(this.screen.colors.yellow("\xB1")); | |
| break; | |
| } | |
| } | |
| onError(error) { | |
| super.onError(error); | |
| this.writeLine("\n" + this.formatError(error).message); | |
| this._counter = 0; | |
| } | |
| async onTestPaused(test, result) { | |
| if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS) | |
| return; | |
| this.screen.stdout.write("\n"); | |
| if (test.outcome() === "unexpected") { | |
| this.writeLine(this.screen.colors.red(this.formatTestHeader(test, { indent: " " }))); | |
| this.writeLine(this.formatResultErrors(test, result)); | |
| markErrorsAsReported(result); | |
| this.writeLine(this.screen.colors.yellow(" Paused on error. Press Ctrl+C to end.") + "\n"); | |
| } else { | |
| this.writeLine(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " }))); | |
| this.writeLine(this.screen.colors.yellow(" Paused at test end. Press Ctrl+C to end.") + "\n"); | |
| } | |
| this._counter = 0; | |
| await new Promise(() => { | |
| }); | |
| } | |
| async onEnd(result) { | |
| await super.onEnd(result); | |
| this.screen.stdout.write("\n"); | |
| this.epilogue(true); | |
| } | |
| }; | |
| var dot_default = DotReporter; | |
| // packages/playwright/src/reporters/empty.ts | |
| var EmptyReporter = class { | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| }; | |
| var empty_default = EmptyReporter; | |
| // packages/playwright/src/reporters/github.ts | |
| var import_path7 = __toESM(require("path")); | |
| var import_util7 = require("../util"); | |
| var { noColors: noColors2 } = require("playwright-core/lib/coreBundle").iso; | |
| var { msToString: msToString2 } = require("playwright-core/lib/coreBundle").iso; | |
| var { getAsBooleanFromENV: getAsBooleanFromENV2 } = require("playwright-core/lib/coreBundle").utils; | |
| var GitHubLogger = class { | |
| newLine() { | |
| process.stdout.write("\n"); | |
| } | |
| _log(message, type = "notice", options = {}) { | |
| message = message.replace(/\n/g, "%0A"); | |
| const configs = Object.entries(options).map(([key, option]) => `${key}=${option}`).join(","); | |
| process.stdout.write((0, import_util7.stripAnsiEscapes)(`::${type} ${configs}::${message} | |
| `)); | |
| } | |
| debug(message, options) { | |
| this._log(message, "debug", options); | |
| } | |
| error(message, options) { | |
| this._log(message, "error", options); | |
| } | |
| notice(message, options) { | |
| this._log(message, "notice", options); | |
| } | |
| warning(message, options) { | |
| this._log(message, "warning", options); | |
| } | |
| }; | |
| var GitHubReporter = class extends TerminalReporter { | |
| constructor(options = {}) { | |
| super({ ...options, omitTags: getAsBooleanFromENV2("PLAYWRIGHT_GITHUB_OMIT_TAGS", options.omitTags) }); | |
| this.githubLogger = new GitHubLogger(); | |
| this._failedTestCount = 0; | |
| this.screen = { ...this.screen, colors: noColors2 }; | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| onTestEnd(test, result) { | |
| super.onTestEnd(test, result); | |
| if (this.willRetry(test)) | |
| return; | |
| if (!this._shouldPrintFailureAnnotations(test)) | |
| return; | |
| this._failedTestCount++; | |
| this.githubLogger.newLine(); | |
| for (const r of test.results) | |
| this._printFailureAnnotation(test, r, this._failedTestCount); | |
| } | |
| _shouldPrintFailureAnnotations(test) { | |
| switch (test.outcome()) { | |
| case "unexpected": | |
| case "flaky": | |
| return true; | |
| case "skipped": | |
| return test.results.some((r) => r.status === "interrupted") && test.results.some((r) => !!r.error); | |
| default: | |
| return false; | |
| } | |
| } | |
| async onEnd(result) { | |
| await super.onEnd(result); | |
| this._printAnnotations(); | |
| } | |
| onError(error) { | |
| const errorMessage = this.formatError(error).message; | |
| this.githubLogger.error(errorMessage); | |
| } | |
| _printAnnotations() { | |
| const summary = this.generateSummary(); | |
| const summaryMessage = this.generateSummaryMessage(summary); | |
| this._printSlowTestAnnotations(); | |
| this._printSummaryAnnotation(summaryMessage); | |
| } | |
| _printSlowTestAnnotations() { | |
| this.getSlowTests().forEach(([file, duration]) => { | |
| const filePath = workspaceRelativePath(import_path7.default.join(process.cwd(), file)); | |
| this.githubLogger.warning(`${filePath} took ${msToString2(duration)}`, { | |
| title: "Slow Test", | |
| file: filePath | |
| }); | |
| }); | |
| } | |
| _printSummaryAnnotation(summary) { | |
| this.githubLogger.notice(summary, { | |
| title: "\u{1F3AD} Playwright Run Summary" | |
| }); | |
| } | |
| _printFailureAnnotation(test, result, index) { | |
| const title = this.formatTestTitle(test); | |
| const header = this.formatTestHeader(test, { indent: " ", index, mode: "error" }); | |
| const errors = formatResultFailure(this.screen, test, result, " "); | |
| for (const error of errors) { | |
| const options = { | |
| file: workspaceRelativePath(error.location?.file || test.location.file), | |
| title | |
| }; | |
| if (error.location) { | |
| options.line = error.location.line; | |
| options.col = error.location.column; | |
| } | |
| const message = [header, ...formatRetry(this.screen, result), error.message].join("\n"); | |
| this.githubLogger.error(message, options); | |
| } | |
| } | |
| }; | |
| function workspaceRelativePath(filePath) { | |
| return import_path7.default.relative(process.env["GITHUB_WORKSPACE"] ?? "", filePath); | |
| } | |
| var github_default = GitHubReporter; | |
| // packages/playwright/src/reporters/html.ts | |
| var html_exports = {}; | |
| __export(html_exports, { | |
| default: () => html_default, | |
| showHTMLReport: () => showHTMLReport | |
| }); | |
| var import_fs5 = __toESM(require("fs")); | |
| var import_os = __toESM(require("os")); | |
| var import_path8 = __toESM(require("path")); | |
| var import_stream3 = require("stream"); | |
| var babel2 = __toESM(require("../transform/babelBundle")); | |
| var import_util8 = require("../util"); | |
| var colors2 = require("playwright-core/lib/utilsBundle").colors; | |
| var mime2 = require("playwright-core/lib/utilsBundle").mime; | |
| var open = require("playwright-core/lib/utilsBundle").open; | |
| var yazl2 = require("playwright-core/lib/utilsBundle").yazl; | |
| var { MultiMap } = require("playwright-core/lib/coreBundle").iso; | |
| var { calculateSha1: calculateSha12 } = require("playwright-core/lib/coreBundle").utils; | |
| var { copyFileAndMakeWritable, removeFolders: removeFolders2, sanitizeForFilePath: sanitizeForFilePath2, toPosixPath: toPosixPath2 } = require("playwright-core/lib/coreBundle").utils; | |
| var { getPackageManagerExecCommand: getPackageManagerExecCommand2, isCodingAgent } = require("playwright-core/lib/coreBundle").utils; | |
| var { HttpServer, serveFolder } = require("playwright-core/lib/coreBundle").utils; | |
| var { gracefullyProcessExitDoNotHang } = require("playwright-core/lib/coreBundle").utils; | |
| var { extractZip } = require("playwright-core/lib/coreBundle").utils; | |
| var htmlReportOptions = ["always", "never", "on-failure"]; | |
| var isHtmlReportOption = (type) => { | |
| return htmlReportOptions.includes(type); | |
| }; | |
| var HtmlReporter = class { | |
| constructor(options) { | |
| this._topLevelErrors = []; | |
| this._reportConfigs = /* @__PURE__ */ new Map(); | |
| this._machines = []; | |
| this._options = options; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| onConfigure(config2) { | |
| this.config = config2; | |
| } | |
| onBegin(suite) { | |
| const { outputFolder, open: open3, attachmentsBaseURL, host, port } = this._resolveOptions(); | |
| this._outputFolder = outputFolder; | |
| this._open = open3; | |
| this._host = host; | |
| this._port = port; | |
| this._attachmentsBaseURL = attachmentsBaseURL; | |
| const reportedWarnings = /* @__PURE__ */ new Set(); | |
| for (const project of this.config.projects) { | |
| if (this._isSubdirectory(outputFolder, project.outputDir) || this._isSubdirectory(project.outputDir, outputFolder)) { | |
| const key = outputFolder + "|" + project.outputDir; | |
| if (reportedWarnings.has(key)) | |
| continue; | |
| reportedWarnings.add(key); | |
| writeLine(colors2.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`)); | |
| writeLine(` | |
| html reporter folder: ${colors2.bold(outputFolder)} | |
| test results folder: ${colors2.bold(project.outputDir)}`); | |
| writeLine(""); | |
| writeLine(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss. | |
| `); | |
| } | |
| } | |
| this.suite = suite; | |
| } | |
| _resolveOptions() { | |
| const outputFolder = reportFolderFromEnv() ?? (0, import_util8.resolveReporterOutputPath)("playwright-report", this._options.configDir, this._options.outputFolder); | |
| return { | |
| outputFolder, | |
| open: getHtmlReportOptionProcessEnv() || this._options.open || "on-failure", | |
| attachmentsBaseURL: process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || this._options.attachmentsBaseURL || "data/", | |
| host: process.env.PLAYWRIGHT_HTML_HOST || this._options.host, | |
| port: process.env.PLAYWRIGHT_HTML_PORT ? +process.env.PLAYWRIGHT_HTML_PORT : this._options.port | |
| }; | |
| } | |
| _isSubdirectory(parentDir, dir) { | |
| const relativePath = import_path8.default.relative(parentDir, dir); | |
| return !!relativePath && !relativePath.startsWith("..") && !import_path8.default.isAbsolute(relativePath); | |
| } | |
| onError(error) { | |
| this._topLevelErrors.push(error); | |
| } | |
| onReportConfigure(params) { | |
| this._reportConfigs.set(params.reportPath, params.config); | |
| } | |
| onReportEnd(params) { | |
| const config2 = this._reportConfigs.get(params.reportPath); | |
| if (config2) | |
| this._machines.push({ config: config2, result: params.result, reportPath: params.reportPath }); | |
| } | |
| async onEnd(result) { | |
| const projectSuites = this.suite.suites; | |
| await removeFolders2([this._outputFolder]); | |
| const noSnippets = parseBooleanEnvVar("PLAYWRIGHT_HTML_NO_SNIPPETS") ?? this._options.noSnippets; | |
| const noCopyPrompt = parseBooleanEnvVar("PLAYWRIGHT_HTML_NO_COPY_PROMPT") ?? this._options.noCopyPrompt; | |
| const doNotInlineAssets = parseBooleanEnvVar("PLAYWRIGHT_HTML_DO_NOT_INLINE_ASSETS") ?? this._options.doNotInlineAssets ?? false; | |
| const mergeFiles = parseBooleanEnvVar("PLAYWRIGHT_HTML_MERGE_FILES") ?? this._options.mergeFiles; | |
| const builder = new HtmlBuilder(yazl2, this.config, this._outputFolder, this._attachmentsBaseURL, doNotInlineAssets, { | |
| title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, | |
| noSnippets, | |
| noCopyPrompt, | |
| mergeFiles | |
| }); | |
| this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors, this._machines); | |
| } | |
| async onExit() { | |
| if (process.env.CI || !this._buildResult) | |
| return; | |
| const { ok, singleTestId } = this._buildResult; | |
| const shouldOpen = !isCodingAgent() && !!process.stdin.isTTY && (this._open === "always" || !ok && this._open === "on-failure"); | |
| if (shouldOpen) { | |
| await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); | |
| } else if (this._options._mode === "test" && !!process.stdin.isTTY) { | |
| const packageManagerCommand = getPackageManagerExecCommand2(); | |
| const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? "" : " " + import_path8.default.relative(process.cwd(), this._outputFolder); | |
| const hostArg = this._host ? ` --host ${this._host}` : ""; | |
| const portArg = this._port ? ` --port ${this._port}` : ""; | |
| writeLine(""); | |
| writeLine("To open last HTML report run:"); | |
| writeLine(colors2.cyan(` | |
| ${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg} | |
| `)); | |
| } | |
| } | |
| }; | |
| function reportFolderFromEnv() { | |
| const envValue = process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || process.env.PLAYWRIGHT_HTML_REPORT; | |
| return envValue ? import_path8.default.resolve(envValue) : void 0; | |
| } | |
| function getHtmlReportOptionProcessEnv() { | |
| const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN; | |
| if (!htmlOpenEnv) | |
| return void 0; | |
| if (!isHtmlReportOption(htmlOpenEnv)) { | |
| writeLine(colors2.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(", ")}`)); | |
| return void 0; | |
| } | |
| return htmlOpenEnv; | |
| } | |
| function parseBooleanEnvVar(name) { | |
| const value = process.env[name]; | |
| if (value === "false" || value === "0") | |
| return false; | |
| if (value) | |
| return true; | |
| return void 0; | |
| } | |
| function standaloneDefaultFolder() { | |
| return reportFolderFromEnv() ?? (0, import_util8.resolveReporterOutputPath)("playwright-report", process.cwd(), void 0); | |
| } | |
| async function resolveReportFolder(reportPath) { | |
| const stat = await import_fs5.default.promises.stat(reportPath).catch(() => null); | |
| if (!stat) | |
| throw new Error(`No report found at "${reportPath}"`); | |
| if (stat.isDirectory()) | |
| return reportPath; | |
| if (stat.isFile() && reportPath.toLowerCase().endsWith(".zip")) | |
| return await extractReportZip(reportPath); | |
| throw new Error(`No report found at "${reportPath}"`); | |
| } | |
| async function extractReportZip(zipPath) { | |
| const tempDir = await import_fs5.default.promises.mkdtemp(import_path8.default.join(import_os.default.tmpdir(), "playwright-show-report-")); | |
| const cleanup = () => { | |
| try { | |
| import_fs5.default.rmSync(tempDir, { recursive: true, force: true }); | |
| } catch { | |
| } | |
| }; | |
| process.on("exit", cleanup); | |
| try { | |
| await extractZip(zipPath, { dir: tempDir }); | |
| } catch (e) { | |
| cleanup(); | |
| throw new Error(`Failed to extract report from "${zipPath}": ${e.message}`); | |
| } | |
| const hasIndex = await import_fs5.default.promises.access(import_path8.default.join(tempDir, "index.html")).then(() => true, () => false); | |
| if (!hasIndex) { | |
| cleanup(); | |
| throw new Error(`No "index.html" found at the top level of "${zipPath}"`); | |
| } | |
| return tempDir; | |
| } | |
| async function showHTMLReport(reportFolder, host = "localhost", port, testId) { | |
| const requestedPath = reportFolder ?? standaloneDefaultFolder(); | |
| let folder; | |
| try { | |
| folder = await resolveReportFolder(requestedPath); | |
| } catch (e) { | |
| writeLine(colors2.red(e.message)); | |
| gracefullyProcessExitDoNotHang(1); | |
| return; | |
| } | |
| const server = false ? await serveHtmlReportWithHMR(folder) : serveFolder(folder); | |
| await server.start({ port, host, preferredPort: port ? void 0 : 9323 }); | |
| let url = server.urlPrefix("human-readable"); | |
| writeLine(""); | |
| writeLine(colors2.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`)); | |
| if (testId) | |
| url += `#?testId=${testId}`; | |
| url = url.replace("0.0.0.0", "localhost"); | |
| if (!isCodingAgent()) | |
| await open(url, { wait: true }).catch(() => { | |
| }); | |
| await new Promise(() => { | |
| }); | |
| } | |
| var HtmlBuilder = class { | |
| constructor(yazl3, config2, outputDir, attachmentsBaseURL, doNotInlineAssets, options) { | |
| this._stepsInFile = new MultiMap(); | |
| this._hasTraces = false; | |
| this._dataZipFile = new yazl3.ZipFile(); | |
| this._config = config2; | |
| this._reportFolder = outputDir; | |
| this._options = options; | |
| this._doNotInlineAssets = doNotInlineAssets; | |
| import_fs5.default.mkdirSync(this._reportFolder, { recursive: true }); | |
| this._attachmentsBaseURL = attachmentsBaseURL; | |
| } | |
| async build(metadata, projectSuites, result, topLevelErrors, machines) { | |
| const data = /* @__PURE__ */ new Map(); | |
| for (const projectSuite of projectSuites) { | |
| const projectName = projectSuite.project().name; | |
| for (const fileSuite of projectSuite.suites) { | |
| const fileName = this._relativeLocation(fileSuite.location).file; | |
| this._createEntryForSuite(data, projectName, fileSuite, fileName, true); | |
| } | |
| } | |
| if (!this._options.noSnippets) | |
| createSnippets(this._stepsInFile); | |
| let ok = true; | |
| for (const [fileId, { testFile, testFileSummary }] of data) { | |
| const stats = testFileSummary.stats; | |
| for (const test of testFileSummary.tests) { | |
| if (test.outcome === "expected") | |
| ++stats.expected; | |
| if (test.outcome === "skipped") | |
| ++stats.skipped; | |
| if (test.outcome === "unexpected") | |
| ++stats.unexpected; | |
| if (test.outcome === "flaky") | |
| ++stats.flaky; | |
| ++stats.total; | |
| } | |
| stats.ok = stats.unexpected + stats.flaky === 0; | |
| if (!stats.ok) | |
| ok = false; | |
| const testCaseSummaryComparator = (t1, t2) => { | |
| const w1 = (t1.outcome === "unexpected" ? 1e3 : 0) + (t1.outcome === "flaky" ? 1 : 0); | |
| const w2 = (t2.outcome === "unexpected" ? 1e3 : 0) + (t2.outcome === "flaky" ? 1 : 0); | |
| return w2 - w1; | |
| }; | |
| testFileSummary.tests.sort(testCaseSummaryComparator); | |
| this._addDataFile(fileId + ".json", testFile); | |
| } | |
| const htmlReport = { | |
| metadata, | |
| startTime: result.startTime.getTime(), | |
| duration: result.duration, | |
| files: [...data.values()].map((e) => e.testFileSummary), | |
| projectNames: projectSuites.map((r) => r.project().name), | |
| stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) }, | |
| errors: topLevelErrors.map((error) => formatError(internalScreen, error).message), | |
| options: this._options, | |
| machines: machines.map((machine) => ({ | |
| duration: machine.result.duration, | |
| startTime: machine.result.startTime.getTime(), | |
| tag: machine.config.tags, | |
| shardIndex: machine.config.shard?.current | |
| })) | |
| }; | |
| htmlReport.files.sort((f1, f2) => { | |
| const w1 = f1.stats.unexpected * 1e3 + f1.stats.flaky; | |
| const w2 = f2.stats.unexpected * 1e3 + f2.stats.flaky; | |
| return w2 - w1; | |
| }); | |
| this._addDataFile("report.json", htmlReport); | |
| let singleTestId; | |
| if (htmlReport.stats.total === 1) { | |
| const testFile = data.values().next().value.testFile; | |
| singleTestId = testFile.tests[0].testId; | |
| } | |
| const reportIndexFile = await this._writeStaticAssets(); | |
| if (this._hasTraces) { | |
| const traceViewerFolder = import_path8.default.join(require.resolve("playwright-core"), "..", "lib", "vite", "traceViewer"); | |
| const traceViewerTargetFolder = import_path8.default.join(this._reportFolder, "trace"); | |
| const traceViewerAssetsTargetFolder = import_path8.default.join(traceViewerTargetFolder, "assets"); | |
| import_fs5.default.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true }); | |
| for (const file of import_fs5.default.readdirSync(traceViewerFolder)) { | |
| if (file.endsWith(".map") || file.includes("watch") || file.includes("assets")) | |
| continue; | |
| await copyFileAndMakeWritable(import_path8.default.join(traceViewerFolder, file), import_path8.default.join(traceViewerTargetFolder, file)); | |
| } | |
| for (const file of import_fs5.default.readdirSync(import_path8.default.join(traceViewerFolder, "assets"))) { | |
| if (file.endsWith(".map") || file.includes("xtermModule")) | |
| continue; | |
| await copyFileAndMakeWritable(import_path8.default.join(traceViewerFolder, "assets", file), import_path8.default.join(traceViewerAssetsTargetFolder, file)); | |
| } | |
| } | |
| await this._writeReportData(reportIndexFile); | |
| return { ok, singleTestId }; | |
| } | |
| async _writeStaticAssets() { | |
| const appFolder = import_path8.default.join(require.resolve("playwright-core"), "..", "lib", "vite", "htmlReport"); | |
| const reportIndexFile = import_path8.default.join(this._reportFolder, "index.html"); | |
| if (this._doNotInlineAssets) { | |
| const html = await import_fs5.default.promises.readFile(import_path8.default.join(appFolder, "index.html"), "utf-8"); | |
| await Promise.all([ | |
| import_fs5.default.promises.writeFile(reportIndexFile, html), | |
| import_fs5.default.promises.copyFile(import_path8.default.join(appFolder, "report.js"), import_path8.default.join(this._reportFolder, "report.js")), | |
| import_fs5.default.promises.copyFile(import_path8.default.join(appFolder, "report.css"), import_path8.default.join(this._reportFolder, "report.css")) | |
| ]); | |
| } else { | |
| let html = await import_fs5.default.promises.readFile(import_path8.default.join(appFolder, "index.html"), "utf-8"); | |
| const [js, css] = await Promise.all([ | |
| import_fs5.default.promises.readFile(import_path8.default.join(appFolder, "report.js"), "utf-8"), | |
| import_fs5.default.promises.readFile(import_path8.default.join(appFolder, "report.css"), "utf-8") | |
| ]); | |
| html = html.replace(/<script type="module"[^>]*><\/script>/, () => `<script type="module">${js}</script>`); | |
| html = html.replace(/<link rel="stylesheet"[^>]*>/, () => `<style type='text/css'>${css}</style>`); | |
| await import_fs5.default.promises.writeFile(reportIndexFile, html); | |
| } | |
| return reportIndexFile; | |
| } | |
| async _writeReportData(filePath) { | |
| import_fs5.default.appendFileSync(filePath, '<template id="playwrightReportBase64">data:application/zip;base64,'); | |
| await new Promise((resolve, reject) => { | |
| this._dataZipFile.end(void 0, () => { | |
| this._dataZipFile.outputStream.pipe(new Base64Encoder()).pipe(import_fs5.default.createWriteStream(filePath, { flags: "a" })).on("close", resolve).on("error", reject); | |
| }); | |
| }); | |
| import_fs5.default.appendFileSync(filePath, "</template>"); | |
| } | |
| _addDataFile(fileName, data) { | |
| this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName); | |
| } | |
| _createEntryForSuite(data, projectName, suite, fileName, deep) { | |
| const fileId = calculateSha12(fileName).slice(0, 20); | |
| let fileEntry = data.get(fileId); | |
| if (!fileEntry) { | |
| fileEntry = { | |
| testFile: { fileId, fileName, tests: [] }, | |
| testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() } | |
| }; | |
| data.set(fileId, fileEntry); | |
| } | |
| const { testFile, testFileSummary } = fileEntry; | |
| const testEntries = []; | |
| this._processSuite(suite, projectName, [], deep, testEntries); | |
| for (const test of testEntries) { | |
| testFile.tests.push(test.testCase); | |
| testFileSummary.tests.push(test.testCaseSummary); | |
| } | |
| } | |
| _processSuite(suite, projectName, path20, deep, outTests) { | |
| const newPath = [...path20, suite.title]; | |
| suite.entries().forEach((e) => { | |
| if (e.type === "test") | |
| outTests.push(this._createTestEntry(e, projectName, newPath)); | |
| else if (deep) | |
| this._processSuite(e, projectName, newPath, deep, outTests); | |
| }); | |
| } | |
| _createTestEntry(test, projectName, path20) { | |
| const duration = test.results.reduce((a, r) => a + r.duration, 0); | |
| const location = this._relativeLocation(test.location); | |
| path20 = path20.slice(1).filter((path21) => path21.length > 0); | |
| const results = test.results.map((r) => this._createTestResult(test, r)); | |
| return { | |
| testCase: { | |
| testId: test.id, | |
| title: test.title, | |
| projectName, | |
| location, | |
| duration, | |
| annotations: this._serializeAnnotations(test.annotations), | |
| tags: [...new Set(test.tags)], | |
| outcome: test.outcome(), | |
| path: path20, | |
| results, | |
| repeatEachIndex: test.repeatEachIndex || void 0, | |
| // Do not include zero. | |
| ok: test.outcome() === "expected" || test.outcome() === "flaky" | |
| }, | |
| testCaseSummary: { | |
| testId: test.id, | |
| title: test.title, | |
| projectName, | |
| location, | |
| duration, | |
| annotations: this._serializeAnnotations(test.annotations), | |
| tags: [...new Set(test.tags)], | |
| outcome: test.outcome(), | |
| path: path20, | |
| ok: test.outcome() === "expected" || test.outcome() === "flaky", | |
| repeatEachIndex: test.repeatEachIndex || void 0, | |
| // Do not include zero. | |
| results: results.map((result) => { | |
| return { | |
| attachments: result.attachments.map((a) => ({ name: a.name, contentType: a.contentType, path: a.path })), | |
| startTime: result.startTime, | |
| workerIndex: result.workerIndex | |
| }; | |
| }) | |
| } | |
| }; | |
| } | |
| _serializeAttachments(attachments) { | |
| let lastAttachment; | |
| return attachments.map((a) => { | |
| if (a.name === "trace") | |
| this._hasTraces = true; | |
| if ((a.name === "stdout" || a.name === "stderr") && a.contentType === "text/plain") { | |
| if (lastAttachment && lastAttachment.name === a.name && lastAttachment.contentType === a.contentType) { | |
| lastAttachment.body += (0, import_util8.stripAnsiEscapes)(a.body); | |
| return null; | |
| } | |
| a.body = (0, import_util8.stripAnsiEscapes)(a.body); | |
| lastAttachment = a; | |
| return a; | |
| } | |
| if (a.path) { | |
| let fileName = a.path; | |
| try { | |
| const buffer = import_fs5.default.readFileSync(a.path); | |
| const sha1 = calculateSha12(buffer) + import_path8.default.extname(a.path); | |
| fileName = this._attachmentsBaseURL + sha1; | |
| import_fs5.default.mkdirSync(import_path8.default.join(this._reportFolder, "data"), { recursive: true }); | |
| import_fs5.default.writeFileSync(import_path8.default.join(this._reportFolder, "data", sha1), buffer); | |
| } catch (e) { | |
| } | |
| return { | |
| name: a.name, | |
| contentType: a.contentType, | |
| path: fileName, | |
| body: a.body | |
| }; | |
| } | |
| if (a.body instanceof Buffer) { | |
| if (isTextContentType(a.contentType)) { | |
| const charset = a.contentType.match(/charset=(.*)/)?.[1]; | |
| try { | |
| const body = a.body.toString(charset || "utf-8"); | |
| return { | |
| name: a.name, | |
| contentType: a.contentType, | |
| body | |
| }; | |
| } catch (e) { | |
| } | |
| } | |
| import_fs5.default.mkdirSync(import_path8.default.join(this._reportFolder, "data"), { recursive: true }); | |
| const extension = sanitizeForFilePath2(import_path8.default.extname(a.name).replace(/^\./, "")) || mime2.getExtension(a.contentType) || "dat"; | |
| const sha1 = calculateSha12(a.body) + "." + extension; | |
| import_fs5.default.writeFileSync(import_path8.default.join(this._reportFolder, "data", sha1), a.body); | |
| return { | |
| name: a.name, | |
| contentType: a.contentType, | |
| path: this._attachmentsBaseURL + sha1 | |
| }; | |
| } | |
| return { | |
| name: a.name, | |
| contentType: a.contentType, | |
| body: a.body | |
| }; | |
| }).filter(Boolean); | |
| } | |
| _serializeAnnotations(annotations) { | |
| return annotations.map((a) => ({ | |
| type: a.type, | |
| description: a.description === void 0 ? void 0 : String(a.description), | |
| location: a.location ? { | |
| file: a.location.file, | |
| line: a.location.line, | |
| column: a.location.column | |
| } : void 0 | |
| })); | |
| } | |
| _createTestResult(test, result) { | |
| return { | |
| duration: result.duration, | |
| startTime: result.startTime.toISOString(), | |
| retry: result.retry, | |
| steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)), | |
| errors: formatResultFailure(internalScreen, test, result, "").map((error) => { | |
| return { | |
| message: error.message, | |
| codeframe: error.location ? createErrorCodeframe(error.message, error.location) : void 0 | |
| }; | |
| }), | |
| status: result.status, | |
| annotations: this._serializeAnnotations(result.annotations), | |
| attachments: this._serializeAttachments([ | |
| ...result.attachments, | |
| ...result.stdout.map((m) => stdioAttachment(m, "stdout")), | |
| ...result.stderr.map((m) => stdioAttachment(m, "stderr")) | |
| ]), | |
| workerIndex: result.workerIndex | |
| }; | |
| } | |
| _createTestStep(dedupedStep, result) { | |
| const { step, duration, count } = dedupedStep; | |
| const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip"); | |
| let title = step.title; | |
| if (skipped) | |
| title = `${title} (skipped${skipped.description ? ": " + skipped.description : ""})`; | |
| const testStep = { | |
| title, | |
| subtitle: step.subtitle, | |
| startTime: step.startTime.toISOString(), | |
| duration, | |
| steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)), | |
| attachments: step.attachments.map((s) => { | |
| const index = result.attachments.indexOf(s); | |
| if (index === -1) | |
| throw new Error("Unexpected, attachment not found"); | |
| return index; | |
| }), | |
| location: this._relativeLocation(step.location), | |
| error: step.error?.message, | |
| count, | |
| skipped: !!skipped | |
| }; | |
| if (step.location) | |
| this._stepsInFile.set(step.location.file, testStep); | |
| return testStep; | |
| } | |
| _relativeLocation(location) { | |
| if (!location) | |
| return void 0; | |
| const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file)); | |
| return { | |
| file, | |
| line: location.line, | |
| column: location.column | |
| }; | |
| } | |
| }; | |
| var emptyStats = () => { | |
| return { | |
| total: 0, | |
| expected: 0, | |
| unexpected: 0, | |
| flaky: 0, | |
| skipped: 0, | |
| ok: true | |
| }; | |
| }; | |
| var addStats = (stats, delta) => { | |
| stats.total += delta.total; | |
| stats.skipped += delta.skipped; | |
| stats.expected += delta.expected; | |
| stats.unexpected += delta.unexpected; | |
| stats.flaky += delta.flaky; | |
| stats.ok = stats.ok && delta.ok; | |
| return stats; | |
| }; | |
| var Base64Encoder = class extends import_stream3.Transform { | |
| _transform(chunk, encoding, callback) { | |
| if (this._remainder) { | |
| chunk = Buffer.concat([this._remainder, chunk]); | |
| this._remainder = void 0; | |
| } | |
| const remaining = chunk.length % 3; | |
| if (remaining) { | |
| this._remainder = chunk.slice(chunk.length - remaining); | |
| chunk = chunk.slice(0, chunk.length - remaining); | |
| } | |
| chunk = chunk.toString("base64"); | |
| this.push(Buffer.from(chunk)); | |
| callback(); | |
| } | |
| _flush(callback) { | |
| if (this._remainder) | |
| this.push(Buffer.from(this._remainder.toString("base64"))); | |
| callback(); | |
| } | |
| }; | |
| function isTextContentType(contentType) { | |
| return contentType.startsWith("text/") || contentType.startsWith("application/json"); | |
| } | |
| function stdioAttachment(chunk, type) { | |
| return { | |
| name: type, | |
| contentType: "text/plain", | |
| body: typeof chunk === "string" ? chunk : chunk.toString("utf-8") | |
| }; | |
| } | |
| function dedupeSteps(steps) { | |
| const result = []; | |
| let lastResult = void 0; | |
| for (const step of steps) { | |
| const canDedupe = !step.error && step.duration >= 0 && step.location?.file && !step.steps.length; | |
| const lastStep = lastResult?.step; | |
| if (canDedupe && lastResult && lastStep && step.category === lastStep.category && step.title === lastStep.title && step.subtitle === lastStep.subtitle && step.location?.file === lastStep.location?.file && step.location?.line === lastStep.location?.line && step.location?.column === lastStep.location?.column) { | |
| ++lastResult.count; | |
| lastResult.duration += step.duration; | |
| continue; | |
| } | |
| lastResult = { step, count: 1, duration: step.duration }; | |
| result.push(lastResult); | |
| if (!canDedupe) | |
| lastResult = void 0; | |
| } | |
| return result; | |
| } | |
| function createSnippets(stepsInFile) { | |
| for (const file of stepsInFile.keys()) { | |
| let source; | |
| try { | |
| source = import_fs5.default.readFileSync(file, "utf-8") + "\n//"; | |
| } catch (e) { | |
| continue; | |
| } | |
| const lines = source.split("\n").length; | |
| const highlighted = babel2.codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 }); | |
| const highlightedLines = highlighted.split("\n"); | |
| const lineWithArrow = highlightedLines[highlightedLines.length - 1]; | |
| for (const step of stepsInFile.get(file)) { | |
| if (step.location.line < 2 || step.location.line >= lines) | |
| continue; | |
| const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1); | |
| const index = lineWithArrow.indexOf("^"); | |
| const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index); | |
| snippetLines.splice(2, 0, shiftedArrow); | |
| step.snippet = snippetLines.join("\n"); | |
| } | |
| } | |
| } | |
| function createErrorCodeframe(message, location) { | |
| let source; | |
| try { | |
| source = import_fs5.default.readFileSync(location.file, "utf-8") + "\n//"; | |
| } catch (e) { | |
| return; | |
| } | |
| return babel2.codeFrameColumns( | |
| source, | |
| { | |
| start: { | |
| line: location.line, | |
| column: location.column | |
| } | |
| }, | |
| { | |
| highlightCode: false, | |
| linesAbove: 100, | |
| linesBelow: 100, | |
| message: (0, import_util8.stripAnsiEscapes)(message).split("\n")[0] || void 0 | |
| } | |
| ); | |
| } | |
| function writeLine(line) { | |
| process.stdout.write(line + "\n"); | |
| } | |
| var html_default = HtmlReporter; | |
| // packages/playwright/src/reporters/json.ts | |
| var import_fs6 = __toESM(require("fs")); | |
| var import_path9 = __toESM(require("path")); | |
| var import_common5 = require("../common"); | |
| var { MultiMap: MultiMap2 } = require("playwright-core/lib/coreBundle").iso; | |
| var { toPosixPath: toPosixPath3 } = require("playwright-core/lib/coreBundle").utils; | |
| var JSONReporter = class { | |
| constructor(options) { | |
| this._errors = []; | |
| this._resolvedOutputFile = resolveOutputFile("JSON", options)?.outputFile; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return !this._resolvedOutputFile; | |
| } | |
| onConfigure(config2) { | |
| this.config = config2; | |
| } | |
| onBegin(suite) { | |
| this.suite = suite; | |
| } | |
| onError(error) { | |
| this._errors.push(error); | |
| } | |
| async onEnd(result) { | |
| await outputReport(this._serializeReport(result), this._resolvedOutputFile); | |
| } | |
| _serializeReport(result) { | |
| const report = { | |
| config: { | |
| ...removePrivateFields(this.config), | |
| rootDir: toPosixPath3(this.config.rootDir), | |
| projects: this.config.projects.map((project) => { | |
| return { | |
| outputDir: toPosixPath3(project.outputDir), | |
| repeatEach: project.repeatEach, | |
| retries: project.retries, | |
| metadata: project.metadata, | |
| id: import_common5.config.getProjectId(project), | |
| name: project.name, | |
| testDir: toPosixPath3(project.testDir), | |
| testIgnore: serializePatterns(project.testIgnore), | |
| testMatch: serializePatterns(project.testMatch), | |
| timeout: project.timeout | |
| }; | |
| }) | |
| }, | |
| suites: this._mergeSuites(this.suite.suites), | |
| errors: this._errors, | |
| stats: { | |
| startTime: result.startTime.toISOString(), | |
| duration: result.duration, | |
| expected: 0, | |
| skipped: 0, | |
| unexpected: 0, | |
| flaky: 0 | |
| } | |
| }; | |
| for (const test of this.suite.allTests()) | |
| ++report.stats[test.outcome()]; | |
| return report; | |
| } | |
| _mergeSuites(suites) { | |
| const fileSuites = new MultiMap2(); | |
| for (const projectSuite of suites) { | |
| const projectId = import_common5.config.getProjectId(projectSuite.project()); | |
| const projectName = projectSuite.project().name; | |
| for (const fileSuite of projectSuite.suites) { | |
| const file = fileSuite.location.file; | |
| const serialized = this._serializeSuite(projectId, projectName, fileSuite); | |
| if (serialized) | |
| fileSuites.set(file, serialized); | |
| } | |
| } | |
| const results = []; | |
| for (const [, suites2] of fileSuites) { | |
| const result = { | |
| title: suites2[0].title, | |
| file: suites2[0].file, | |
| column: 0, | |
| line: 0, | |
| specs: [] | |
| }; | |
| for (const suite of suites2) | |
| this._mergeTestsFromSuite(result, suite); | |
| results.push(result); | |
| } | |
| return results; | |
| } | |
| _relativeLocation(location) { | |
| if (!location) | |
| return { file: "", line: 0, column: 0 }; | |
| return { | |
| file: toPosixPath3(import_path9.default.relative(this.config.rootDir, location.file)), | |
| line: location.line, | |
| column: location.column | |
| }; | |
| } | |
| _locationMatches(s1, s2) { | |
| return s1.file === s2.file && s1.line === s2.line && s1.column === s2.column; | |
| } | |
| _mergeTestsFromSuite(to, from) { | |
| for (const fromSuite of from.suites || []) { | |
| const toSuite = (to.suites || []).find((s) => s.title === fromSuite.title && this._locationMatches(s, fromSuite)); | |
| if (toSuite) { | |
| this._mergeTestsFromSuite(toSuite, fromSuite); | |
| } else { | |
| if (!to.suites) | |
| to.suites = []; | |
| to.suites.push(fromSuite); | |
| } | |
| } | |
| for (const spec of from.specs || []) { | |
| const toSpec = to.specs.find((s) => s.title === spec.title && s.file === toPosixPath3(import_path9.default.relative(this.config.rootDir, spec.file)) && s.line === spec.line && s.column === spec.column); | |
| if (toSpec) | |
| toSpec.tests.push(...spec.tests); | |
| else | |
| to.specs.push(spec); | |
| } | |
| } | |
| _serializeSuite(projectId, projectName, suite) { | |
| if (!suite.allTests().length) | |
| return null; | |
| const suites = suite.suites.map((suite2) => this._serializeSuite(projectId, projectName, suite2)).filter((s) => s); | |
| return { | |
| title: suite.title, | |
| ...this._relativeLocation(suite.location), | |
| specs: suite.tests.map((test) => this._serializeTestSpec(projectId, projectName, test)), | |
| suites: suites.length ? suites : void 0 | |
| }; | |
| } | |
| _serializeTestSpec(projectId, projectName, test) { | |
| return { | |
| title: test.title, | |
| ok: test.ok(), | |
| tags: test.tags.map((tag) => tag.substring(1)), | |
| // Strip '@'. | |
| tests: [this._serializeTest(projectId, projectName, test)], | |
| id: test.id, | |
| ...this._relativeLocation(test.location) | |
| }; | |
| } | |
| _serializeTest(projectId, projectName, test) { | |
| return { | |
| timeout: test.timeout, | |
| annotations: test.annotations, | |
| expectedStatus: test.expectedStatus, | |
| projectId, | |
| projectName, | |
| results: test.results.map((r) => this._serializeTestResult(r, test)), | |
| status: test.outcome() | |
| }; | |
| } | |
| _serializeTestResult(result, test) { | |
| const steps = result.steps.filter((s) => s.category === "test.step"); | |
| const jsonResult = { | |
| workerIndex: result.workerIndex, | |
| parallelIndex: result.parallelIndex, | |
| status: result.status, | |
| duration: result.duration, | |
| error: result.error, | |
| errors: result.errors.map((e) => this._serializeError(e)), | |
| stdout: result.stdout.map((s) => stdioEntry(s)), | |
| stderr: result.stderr.map((s) => stdioEntry(s)), | |
| retry: result.retry, | |
| steps: steps.length ? steps.map((s) => this._serializeTestStep(s)) : void 0, | |
| startTime: result.startTime.toISOString(), | |
| annotations: result.annotations, | |
| attachments: result.attachments.map((a) => ({ | |
| name: a.name, | |
| contentType: a.contentType, | |
| path: a.path, | |
| body: a.body?.toString("base64") | |
| })) | |
| }; | |
| if (result.error?.stack) | |
| jsonResult.errorLocation = prepareErrorStack(result.error.stack).location; | |
| return jsonResult; | |
| } | |
| _serializeError(error) { | |
| return formatError(nonTerminalScreen, error); | |
| } | |
| _serializeTestStep(step) { | |
| const steps = step.steps.filter((s) => s.category === "test.step"); | |
| return { | |
| title: step.title, | |
| subtitle: step.subtitle, | |
| duration: step.duration, | |
| error: step.error, | |
| steps: steps.length ? steps.map((s) => this._serializeTestStep(s)) : void 0 | |
| }; | |
| } | |
| }; | |
| async function outputReport(report, resolvedOutputFile) { | |
| const reportString = JSON.stringify(report, void 0, 2); | |
| if (resolvedOutputFile) { | |
| await import_fs6.default.promises.mkdir(import_path9.default.dirname(resolvedOutputFile), { recursive: true }); | |
| await import_fs6.default.promises.writeFile(resolvedOutputFile, reportString); | |
| } else { | |
| console.log(reportString); | |
| } | |
| } | |
| function stdioEntry(s) { | |
| if (typeof s === "string") | |
| return { text: s }; | |
| return { buffer: s.toString("base64") }; | |
| } | |
| function removePrivateFields(config2) { | |
| return Object.fromEntries(Object.entries(config2).filter(([name, value]) => !name.startsWith("_"))); | |
| } | |
| function serializePatterns(patterns) { | |
| if (!Array.isArray(patterns)) | |
| patterns = [patterns]; | |
| return patterns.map((s) => s.toString()); | |
| } | |
| var json_default = JSONReporter; | |
| // packages/playwright/src/reporters/junit.ts | |
| var import_fs7 = __toESM(require("fs")); | |
| var import_path10 = __toESM(require("path")); | |
| var import_util9 = require("../util"); | |
| var { getAsBooleanFromENV: getAsBooleanFromENV3 } = require("playwright-core/lib/coreBundle").utils; | |
| var JUnitReporter = class { | |
| constructor(options) { | |
| this.totalTests = 0; | |
| this.totalFailures = 0; | |
| this.totalErrors = 0; | |
| this.totalSkipped = 0; | |
| this.stripANSIControlSequences = false; | |
| this.includeProjectInTestName = false; | |
| this.includeRetries = false; | |
| this.omitTags = false; | |
| this.stripANSIControlSequences = getAsBooleanFromENV3("PLAYWRIGHT_JUNIT_STRIP_ANSI", !!options.stripANSIControlSequences); | |
| this.includeProjectInTestName = getAsBooleanFromENV3("PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME", !!options.includeProjectInTestName); | |
| this.includeRetries = getAsBooleanFromENV3("PLAYWRIGHT_JUNIT_INCLUDE_RETRIES", !!options.includeRetries); | |
| this.omitTags = getAsBooleanFromENV3("PLAYWRIGHT_JUNIT_OMIT_TAGS", !!options.omitTags); | |
| this.configDir = options.configDir; | |
| this.resolvedOutputFile = resolveOutputFile("JUNIT", options)?.outputFile; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return !this.resolvedOutputFile; | |
| } | |
| onConfigure(config2) { | |
| this.config = config2; | |
| } | |
| onBegin(suite) { | |
| this.suite = suite; | |
| this.timestamp = /* @__PURE__ */ new Date(); | |
| } | |
| async onEnd(result) { | |
| const children = []; | |
| for (const projectSuite of this.suite.suites) { | |
| for (const fileSuite of projectSuite.suites) | |
| children.push(await this._buildTestSuite(projectSuite.title, fileSuite)); | |
| } | |
| const tokens = []; | |
| const self = this; | |
| const root = { | |
| name: "testsuites", | |
| attributes: { | |
| id: process.env[`PLAYWRIGHT_JUNIT_SUITE_ID`] || "", | |
| name: process.env[`PLAYWRIGHT_JUNIT_SUITE_NAME`] || "", | |
| tests: self.totalTests, | |
| failures: self.totalFailures, | |
| skipped: self.totalSkipped, | |
| errors: self.totalErrors, | |
| time: result.duration / 1e3 | |
| }, | |
| children | |
| }; | |
| serializeXML(root, tokens, this.stripANSIControlSequences); | |
| const reportString = tokens.join("\n"); | |
| if (this.resolvedOutputFile) { | |
| await import_fs7.default.promises.mkdir(import_path10.default.dirname(this.resolvedOutputFile), { recursive: true }); | |
| await import_fs7.default.promises.writeFile(this.resolvedOutputFile, reportString); | |
| } else { | |
| console.log(reportString); | |
| } | |
| } | |
| async _buildTestSuite(projectName, suite) { | |
| let tests = 0; | |
| let skipped = 0; | |
| let failures = 0; | |
| let errors = 0; | |
| let duration = 0; | |
| const children = []; | |
| const testCaseNamePrefix = projectName && this.includeProjectInTestName ? `[${projectName}] ` : ""; | |
| for (const test of suite.allTests()) { | |
| ++tests; | |
| if (test.outcome() === "skipped") | |
| ++skipped; | |
| for (const result of test.results) | |
| duration += result.duration; | |
| const classification = await this._addTestCase(suite.title, testCaseNamePrefix, test, children); | |
| if (classification === "error") | |
| ++errors; | |
| else if (classification === "failure") | |
| ++failures; | |
| } | |
| this.totalTests += tests; | |
| this.totalSkipped += skipped; | |
| this.totalFailures += failures; | |
| this.totalErrors += errors; | |
| const entry = { | |
| name: "testsuite", | |
| attributes: { | |
| name: suite.title, | |
| timestamp: this.timestamp.toISOString(), | |
| hostname: projectName, | |
| tests, | |
| failures, | |
| skipped, | |
| time: duration / 1e3, | |
| errors | |
| }, | |
| children | |
| }; | |
| return entry; | |
| } | |
| async _addTestCase(suiteName, namePrefix, test, entries) { | |
| const entry = { | |
| name: "testcase", | |
| attributes: { | |
| // Skip root, project, file | |
| name: namePrefix + test.titlePath().slice(3).join(" \u203A "), | |
| // filename | |
| classname: suiteName | |
| }, | |
| children: [] | |
| }; | |
| entries.push(entry); | |
| const properties = { | |
| name: "properties", | |
| children: [] | |
| }; | |
| for (const annotation of test.annotations) { | |
| const property = { | |
| name: "property", | |
| attributes: { | |
| name: annotation.type, | |
| value: annotation?.description ? annotation.description : "" | |
| } | |
| }; | |
| properties.children?.push(property); | |
| } | |
| if (properties.children?.length) | |
| entry.children.push(properties); | |
| if (test.outcome() === "skipped") { | |
| entry.children.push({ name: "skipped" }); | |
| return null; | |
| } | |
| if (this.includeRetries && test.ok()) { | |
| const passResult = test.results[test.results.length - 1]; | |
| entry.attributes.time = passResult.duration / 1e3; | |
| await this._appendStdIO(entry, [passResult]); | |
| for (let i = 0; i < test.results.length - 1; i++) { | |
| const result = test.results[i]; | |
| if (result.status === "passed" || result.status === "skipped") | |
| continue; | |
| entry.children.push(await this._buildRetryEntry(result, "flaky")); | |
| } | |
| return null; | |
| } | |
| if (this.includeRetries) { | |
| entry.attributes.time = test.results[0].duration / 1e3; | |
| await this._appendStdIO(entry, [test.results[0]]); | |
| for (let i = 1; i < test.results.length; i++) { | |
| const result = test.results[i]; | |
| if (result.status === "passed" || result.status === "skipped") | |
| continue; | |
| entry.children.push(await this._buildRetryEntry(result, "rerun")); | |
| } | |
| return this._addFailureEntry(test, classifyResultError(test.results[0]), entry); | |
| } | |
| entry.attributes.time = test.results.reduce((acc, value) => acc + value.duration, 0) / 1e3; | |
| await this._appendStdIO(entry, test.results); | |
| if (test.ok()) | |
| return null; | |
| return this._addFailureEntry(test, classifyTestError(test), entry); | |
| } | |
| _addFailureEntry(test, errorInfo, entry) { | |
| if (errorInfo) { | |
| entry.children.push({ | |
| name: errorInfo.elementName, | |
| attributes: { message: errorInfo.message, type: errorInfo.type }, | |
| text: (0, import_util9.stripAnsiEscapes)(formatFailure(nonTerminalScreen, this.config, test, void 0, { omitTags: this.omitTags })) | |
| }); | |
| return errorInfo.elementName; | |
| } | |
| entry.children.push({ | |
| name: "failure", | |
| attributes: { | |
| message: `${import_path10.default.basename(test.location.file)}:${test.location.line}:${test.location.column} ${test.title}`, | |
| type: "FAILURE" | |
| }, | |
| text: (0, import_util9.stripAnsiEscapes)(formatFailure(nonTerminalScreen, this.config, test, void 0, { omitTags: this.omitTags })) | |
| }); | |
| return "failure"; | |
| } | |
| async _appendStdIO(entry, results) { | |
| const systemOut = []; | |
| const systemErr = []; | |
| for (const result of results) { | |
| for (const item of result.stdout) | |
| systemOut.push(item.toString()); | |
| for (const item of result.stderr) | |
| systemErr.push(item.toString()); | |
| for (const attachment of result.attachments) { | |
| if (!attachment.path) | |
| continue; | |
| let attachmentPath = import_path10.default.relative(this.configDir, attachment.path); | |
| try { | |
| if (this.resolvedOutputFile) | |
| attachmentPath = import_path10.default.relative(import_path10.default.dirname(this.resolvedOutputFile), attachment.path); | |
| } catch { | |
| systemOut.push(` | |
| Warning: Unable to make attachment path ${attachment.path} relative to report output file ${this.resolvedOutputFile}`); | |
| } | |
| try { | |
| await import_fs7.default.promises.access(attachment.path); | |
| systemOut.push(` | |
| [[ATTACHMENT|${attachmentPath}]] | |
| `); | |
| } catch { | |
| systemErr.push(` | |
| Warning: attachment ${attachmentPath} is missing`); | |
| } | |
| } | |
| } | |
| if (systemOut.length) | |
| entry.children.push({ name: "system-out", text: systemOut.join("") }); | |
| if (systemErr.length) | |
| entry.children.push({ name: "system-err", text: systemErr.join("") }); | |
| } | |
| async _buildRetryEntry(result, prefix) { | |
| const errorInfo = classifyResultError(result); | |
| const entry = { | |
| name: `${prefix}${errorInfo?.elementName === "error" ? "Error" : "Failure"}`, | |
| attributes: { message: errorInfo?.message || "", type: errorInfo?.type || "FAILURE", time: result.duration / 1e3 }, | |
| children: [] | |
| }; | |
| const stackTrace = result.error?.stack || result.error?.message || result.error?.value || ""; | |
| entry.children.push({ name: "stackTrace", text: (0, import_util9.stripAnsiEscapes)(stackTrace) }); | |
| await this._appendStdIO(entry, [result]); | |
| return entry; | |
| } | |
| }; | |
| function classifyResultError(result) { | |
| const error = result.error; | |
| if (!error) | |
| return null; | |
| const rawMessage = (0, import_util9.stripAnsiEscapes)(error.message || error.value || ""); | |
| const nameMatch = rawMessage.match(/^(\w+): /); | |
| const errorName = nameMatch ? nameMatch[1] : ""; | |
| const messageBody = nameMatch ? rawMessage.slice(nameMatch[0].length) : rawMessage; | |
| const firstLine = messageBody.split("\n")[0].trim(); | |
| const matcherMatch = rawMessage.match(/expect\(.*?\)\.(not\.)?(\w+)/); | |
| if (matcherMatch) { | |
| const matcherName = `expect.${matcherMatch[1] || ""}${matcherMatch[2]}`; | |
| return { | |
| elementName: "failure", | |
| type: matcherName, | |
| message: firstLine | |
| }; | |
| } | |
| return { | |
| elementName: "error", | |
| type: errorName || "Error", | |
| message: firstLine | |
| }; | |
| } | |
| function classifyTestError(test) { | |
| for (const result of test.results) { | |
| const info = classifyResultError(result); | |
| if (info) | |
| return info; | |
| } | |
| return null; | |
| } | |
| function serializeXML(entry, tokens, stripANSIControlSequences) { | |
| const attrs = []; | |
| for (const [name, value] of Object.entries(entry.attributes || {})) | |
| attrs.push(`${name}="${escape(String(value), stripANSIControlSequences, false)}"`); | |
| tokens.push(`<${entry.name}${attrs.length ? " " : ""}${attrs.join(" ")}>`); | |
| for (const child of entry.children || []) | |
| serializeXML(child, tokens, stripANSIControlSequences); | |
| if (entry.text) | |
| tokens.push(escape(entry.text, stripANSIControlSequences, true)); | |
| tokens.push(`</${entry.name}>`); | |
| } | |
| var discouragedXMLCharacters = /[\u0000-\u0008\u000b-\u000c\u000e-\u001f\u007f-\u0084\u0086-\u009f]/g; | |
| function escape(text, stripANSIControlSequences, isCharacterData) { | |
| if (stripANSIControlSequences) | |
| text = (0, import_util9.stripAnsiEscapes)(text); | |
| if (isCharacterData) { | |
| text = "<![CDATA[" + text.replace(/]]>/g, "]]>") + "]]>"; | |
| } else { | |
| const escapeRe = /[&"'<>]/g; | |
| text = text.replace(escapeRe, (c) => ({ "&": "&", '"': """, "'": "'", "<": "<", ">": ">" })[c]); | |
| } | |
| text = text.replace(discouragedXMLCharacters, ""); | |
| return text; | |
| } | |
| var junit_default = JUnitReporter; | |
| // packages/playwright/src/reporters/line.ts | |
| var { getAsBooleanFromENV: getAsBooleanFromENV4 } = require("playwright-core/lib/coreBundle").utils; | |
| var LineReporter = class extends TerminalReporter { | |
| constructor(options) { | |
| super({ ...options, omitTags: getAsBooleanFromENV4("PLAYWRIGHT_LINE_OMIT_TAGS", options?.omitTags) }); | |
| this._current = 0; | |
| this._failures = 0; | |
| this._didBegin = false; | |
| } | |
| onBegin(suite) { | |
| super.onBegin(suite); | |
| const startingMessage = this.generateStartingMessage(); | |
| if (startingMessage) { | |
| this.writeLine(startingMessage); | |
| this.writeLine(); | |
| } | |
| this._didBegin = true; | |
| } | |
| onStdOut(chunk, test, result) { | |
| super.onStdOut(chunk, test, result); | |
| this._dumpToStdio(test, chunk, this.screen.stdout); | |
| } | |
| onStdErr(chunk, test, result) { | |
| super.onStdErr(chunk, test, result); | |
| this._dumpToStdio(test, chunk, this.screen.stderr); | |
| } | |
| _dumpToStdio(test, chunk, stream) { | |
| if (this.config.quiet) | |
| return; | |
| if (!process.env.PW_TEST_DEBUG_REPORTERS) | |
| stream.write(`\x1B[1A\x1B[2K`); | |
| if (test && this._lastTest !== test) { | |
| const title = this.screen.colors.dim(this.formatTestTitle(test)); | |
| stream.write(this.fitToScreen(title) + ` | |
| `); | |
| this._lastTest = test; | |
| } | |
| stream.write(chunk); | |
| if (chunk[chunk.length - 1] !== "\n") | |
| this.writeLine(); | |
| this.writeLine(); | |
| } | |
| onTestBegin(test, result) { | |
| ++this._current; | |
| this._updateLine(test, result, void 0); | |
| } | |
| onStepBegin(test, result, step) { | |
| if (this.screen.isTTY && step.category === "test.step") | |
| this._updateLine(test, result, step); | |
| } | |
| onStepEnd(test, result, step) { | |
| if (this.screen.isTTY && step.category === "test.step") | |
| this._updateLine(test, result, step.parent); | |
| } | |
| async onTestPaused(test, result) { | |
| if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS) | |
| return; | |
| if (!process.env.PW_TEST_DEBUG_REPORTERS) | |
| this.screen.stdout.write(`\x1B[1A\x1B[2K`); | |
| if (test.outcome() === "unexpected") { | |
| this.writeLine(this.screen.colors.red(this.formatTestHeader(test, { indent: " ", index: ++this._failures }))); | |
| this.writeLine(this.formatResultErrors(test, result)); | |
| markErrorsAsReported(result); | |
| this.writeLine(this.screen.colors.yellow(` Paused on error. Press Ctrl+C to end.`) + "\n\n"); | |
| } else { | |
| this.writeLine(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " }))); | |
| this.writeLine(this.screen.colors.yellow(` Paused at test end. Press Ctrl+C to end.`) + "\n\n"); | |
| } | |
| this._updateLine(test, result, void 0); | |
| await new Promise(() => { | |
| }); | |
| } | |
| onTestEnd(test, result) { | |
| super.onTestEnd(test, result); | |
| if (!this.willRetry(test) && (test.outcome() === "flaky" || test.outcome() === "unexpected" || result.status === "interrupted")) { | |
| if (!process.env.PW_TEST_DEBUG_REPORTERS) | |
| this.screen.stdout.write(`\x1B[1A\x1B[2K`); | |
| this.writeLine(this.formatFailure(test, ++this._failures)); | |
| this.writeLine(); | |
| } | |
| } | |
| _updateLine(test, result, step) { | |
| const retriesPrefix = result.retry ? ` (retries)` : ``; | |
| const prefix = `[${this._current}/${this.totalTestCount}]${retriesPrefix} `; | |
| const currentRetrySuffix = result.retry ? this.screen.colors.yellow(` (retry #${result.retry})`) : ""; | |
| const title = this.formatTestTitle(test, step) + currentRetrySuffix; | |
| if (process.env.PW_TEST_DEBUG_REPORTERS) | |
| this.screen.stdout.write(`${prefix + title} | |
| `); | |
| else | |
| this.screen.stdout.write(`\x1B[1A\x1B[2K${prefix + this.fitToScreen(title, prefix)} | |
| `); | |
| } | |
| onError(error) { | |
| super.onError(error); | |
| const message = this.formatError(error).message + "\n"; | |
| if (!process.env.PW_TEST_DEBUG_REPORTERS && this._didBegin) | |
| this.screen.stdout.write(`\x1B[1A\x1B[2K`); | |
| this.screen.stdout.write(message); | |
| this.writeLine(); | |
| } | |
| async onEnd(result) { | |
| if (!process.env.PW_TEST_DEBUG_REPORTERS && this._didBegin) | |
| this.screen.stdout.write(`\x1B[1A\x1B[2K`); | |
| await super.onEnd(result); | |
| this.epilogue(false); | |
| } | |
| }; | |
| var line_default = LineReporter; | |
| // packages/playwright/src/reporters/list.ts | |
| var import_util10 = require("../util"); | |
| var { msToString: msToString3 } = require("playwright-core/lib/coreBundle").iso; | |
| var { getAsBooleanFromENV: getAsBooleanFromENV5 } = require("playwright-core/lib/coreBundle").utils; | |
| var DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === "win32" && process.env.TERM_PROGRAM !== "vscode" && !process.env.WT_SESSION; | |
| var POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? "ok" : "\u2713"; | |
| var NEGATIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? "x" : "\u2718"; | |
| var ListReporter = class extends TerminalReporter { | |
| constructor(options) { | |
| super({ ...options, omitTags: getAsBooleanFromENV5("PLAYWRIGHT_LIST_OMIT_TAGS", options?.omitTags) }); | |
| this._lastRow = 0; | |
| this._lastColumn = 0; | |
| this._testRows = /* @__PURE__ */ new Map(); | |
| this._stepRows = /* @__PURE__ */ new Map(); | |
| this._resultIndex = /* @__PURE__ */ new Map(); | |
| this._stepIndex = /* @__PURE__ */ new Map(); | |
| this._needNewLine = false; | |
| this._failureIndex = 0; | |
| this._paused = /* @__PURE__ */ new Set(); | |
| this._printSteps = getAsBooleanFromENV5("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps); | |
| this._printFailuresInline = getAsBooleanFromENV5("PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE", options?.printFailuresInline); | |
| } | |
| onBegin(suite) { | |
| super.onBegin(suite); | |
| const startingMessage = this.generateStartingMessage(); | |
| if (startingMessage) { | |
| this.writeLine(startingMessage); | |
| this.writeLine(""); | |
| } | |
| } | |
| onTestBegin(test, result) { | |
| const index = String(this._resultIndex.size + 1); | |
| this._resultIndex.set(result, index); | |
| if (!this.screen.isTTY) | |
| return; | |
| this._maybeWriteNewLine(); | |
| this._testRows.set(test, this._lastRow); | |
| const prefix = this._testPrefix(index, ""); | |
| const line = this.screen.colors.dim(this.formatTestTitle(test)) + this._retrySuffix(result); | |
| this._appendLine(line, prefix); | |
| } | |
| onStdOut(chunk, test, result) { | |
| super.onStdOut(chunk, test, result); | |
| this._dumpToStdio(test, chunk, this.screen.stdout, "out"); | |
| } | |
| onStdErr(chunk, test, result) { | |
| super.onStdErr(chunk, test, result); | |
| this._dumpToStdio(test, chunk, this.screen.stderr, "err"); | |
| } | |
| getStepIndex(testIndex, result, step) { | |
| if (this._stepIndex.has(step)) | |
| return this._stepIndex.get(step); | |
| const ordinal = (result[lastStepOrdinalSymbol] || 0) + 1; | |
| result[lastStepOrdinalSymbol] = ordinal; | |
| const stepIndex = `${testIndex}.${ordinal}`; | |
| this._stepIndex.set(step, stepIndex); | |
| return stepIndex; | |
| } | |
| onStepBegin(test, result, step) { | |
| if (step.category !== "test.step") | |
| return; | |
| const testIndex = this._resultIndex.get(result) || ""; | |
| if (!this.screen.isTTY) | |
| return; | |
| if (this._printSteps) { | |
| this._maybeWriteNewLine(); | |
| this._stepRows.set(step, this._lastRow); | |
| const prefix = this._testPrefix(this.getStepIndex(testIndex, result, step), ""); | |
| const line = test.title + this.screen.colors.dim(stepSuffix(step)); | |
| this._appendLine(line, prefix); | |
| } else { | |
| this._updateOrAppendLine(this._testRows, test, this.screen.colors.dim(this.formatTestTitle(test, step)) + this._retrySuffix(result), this._testPrefix(testIndex, "")); | |
| } | |
| } | |
| onStepEnd(test, result, step) { | |
| if (step.category !== "test.step") | |
| return; | |
| const testIndex = this._resultIndex.get(result) || ""; | |
| if (!this._printSteps) { | |
| if (this.screen.isTTY) | |
| this._updateOrAppendLine(this._testRows, test, this.screen.colors.dim(this.formatTestTitle(test, step.parent)) + this._retrySuffix(result), this._testPrefix(testIndex, "")); | |
| return; | |
| } | |
| const index = this.getStepIndex(testIndex, result, step); | |
| const title = this.screen.isTTY ? test.title + this.screen.colors.dim(stepSuffix(step)) : this.formatTestTitle(test, step); | |
| const prefix = this._testPrefix(index, ""); | |
| let text = ""; | |
| if (step.error) | |
| text = this.screen.colors.red(title); | |
| else | |
| text = title; | |
| text += this.screen.colors.dim(` (${msToString3(step.duration)})`); | |
| this._updateOrAppendLine(this._stepRows, step, text, prefix); | |
| } | |
| _maybeWriteNewLine() { | |
| if (this._needNewLine) { | |
| this._needNewLine = false; | |
| this.screen.stdout.write("\n"); | |
| ++this._lastRow; | |
| this._lastColumn = 0; | |
| } | |
| } | |
| _updateLineCountAndNewLineFlagForOutput(text) { | |
| this._needNewLine = text[text.length - 1] !== "\n"; | |
| if (!this.screen.ttyWidth) | |
| return; | |
| for (const ch of text) { | |
| if (ch === "\n") { | |
| this._lastColumn = 0; | |
| ++this._lastRow; | |
| continue; | |
| } | |
| ++this._lastColumn; | |
| if (this._lastColumn > this.screen.ttyWidth) { | |
| this._lastColumn = 0; | |
| ++this._lastRow; | |
| } | |
| } | |
| } | |
| _dumpToStdio(test, chunk, stream, stdio) { | |
| if (this.config.quiet) | |
| return; | |
| const text = chunk.toString("utf-8"); | |
| this._updateLineCountAndNewLineFlagForOutput(text); | |
| stream.write(chunk); | |
| } | |
| async onTestPaused(test, result) { | |
| if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS) | |
| return; | |
| this._paused.add(result); | |
| this._updateTestLine(test, result); | |
| this._maybeWriteNewLine(); | |
| if (test.outcome() === "unexpected") { | |
| const errors = this.formatResultErrors(test, result); | |
| this.writeLine(errors); | |
| this._updateLineCountAndNewLineFlagForOutput(errors); | |
| markErrorsAsReported(result); | |
| } | |
| this._appendLine(this.screen.colors.yellow(`Paused ${test.outcome() === "unexpected" ? "on error" : "at test end"}. Press Ctrl+C to end.`), this._testPrefix("", "")); | |
| await new Promise(() => { | |
| }); | |
| } | |
| onTestEnd(test, result) { | |
| super.onTestEnd(test, result); | |
| const wasPaused = this._paused.delete(result); | |
| if (!wasPaused) | |
| this._updateTestLine(test, result); | |
| const isFailure = result.status !== "skipped" && result.status !== test.expectedStatus; | |
| if (!wasPaused && this._printFailuresInline && isFailure) | |
| this._printFailure(test); | |
| } | |
| _printFailure(test) { | |
| this._maybeWriteNewLine(); | |
| const message = "\n" + this.formatFailure(test, ++this._failureIndex) + "\n"; | |
| this._updateLineCountAndNewLineFlagForOutput(message); | |
| this.screen.stdout.write(message); | |
| } | |
| _updateTestLine(test, result) { | |
| const title = this.formatTestTitle(test); | |
| let prefix = ""; | |
| let text = ""; | |
| let index = this._resultIndex.get(result); | |
| if (!index) { | |
| index = String(this._resultIndex.size + 1); | |
| this._resultIndex.set(result, index); | |
| } | |
| if (result.status === "skipped") { | |
| prefix = this._testPrefix(index, this.screen.colors.green("-")); | |
| text = this.screen.colors.cyan(title) + this._retrySuffix(result); | |
| } else { | |
| const statusMark = result.status === "passed" ? POSITIVE_STATUS_MARK : NEGATIVE_STATUS_MARK; | |
| if (result.status === test.expectedStatus) { | |
| prefix = this._testPrefix(index, this.screen.colors.green(statusMark)); | |
| text = title; | |
| } else { | |
| prefix = this._testPrefix(index, this.screen.colors.red(statusMark)); | |
| text = this.screen.colors.red(title); | |
| } | |
| text += this._retrySuffix(result) + this.screen.colors.dim(` (${msToString3(result.duration)})`); | |
| } | |
| this._updateOrAppendLine(this._testRows, test, text, prefix); | |
| } | |
| _updateOrAppendLine(entityRowNumbers, entity, text, prefix) { | |
| const row = entityRowNumbers.get(entity); | |
| if (row !== void 0 && this.screen.isTTY && this._lastRow - row < this.screen.ttyHeight) { | |
| this._updateLine(row, text, prefix); | |
| } else { | |
| this._maybeWriteNewLine(); | |
| entityRowNumbers.set(entity, this._lastRow); | |
| this._appendLine(text, prefix); | |
| } | |
| } | |
| _appendLine(text, prefix) { | |
| const line = prefix + this.fitToScreen(text, prefix); | |
| if (process.env.PW_TEST_DEBUG_REPORTERS) { | |
| this.screen.stdout.write("#" + this._lastRow + " : " + line + "\n"); | |
| } else { | |
| this.screen.stdout.write(line); | |
| this.screen.stdout.write("\n"); | |
| } | |
| ++this._lastRow; | |
| this._lastColumn = 0; | |
| } | |
| _updateLine(row, text, prefix) { | |
| const line = prefix + this.fitToScreen(text, prefix); | |
| if (process.env.PW_TEST_DEBUG_REPORTERS) | |
| this.screen.stdout.write("#" + row + " : " + line + "\n"); | |
| else | |
| this._updateLineForTTY(row, line); | |
| } | |
| _updateLineForTTY(row, line) { | |
| if (row !== this._lastRow) | |
| this.screen.stdout.write(`\x1B[${this._lastRow - row}A`); | |
| this.screen.stdout.write("\x1B[2K\x1B[0G"); | |
| this.screen.stdout.write(line); | |
| if (row !== this._lastRow) | |
| this.screen.stdout.write(`\x1B[${this._lastRow - row}E`); | |
| } | |
| _testPrefix(index, statusMark) { | |
| const statusMarkLength = (0, import_util10.stripAnsiEscapes)(statusMark).length; | |
| const indexLength = Math.ceil(Math.log10(this.totalTestCount + 1)); | |
| return " " + statusMark + " ".repeat(3 - statusMarkLength) + this.screen.colors.dim(index.padStart(indexLength) + " "); | |
| } | |
| _retrySuffix(result) { | |
| return result.retry ? this.screen.colors.yellow(` (retry #${result.retry})`) : ""; | |
| } | |
| onError(error) { | |
| super.onError(error); | |
| this._maybeWriteNewLine(); | |
| const message = this.formatError(error).message + "\n"; | |
| this._updateLineCountAndNewLineFlagForOutput(message); | |
| this.screen.stdout.write(message); | |
| } | |
| async onEnd(result) { | |
| await super.onEnd(result); | |
| this.screen.stdout.write("\n"); | |
| this.epilogue(!this._printFailuresInline); | |
| } | |
| }; | |
| var lastStepOrdinalSymbol = /* @__PURE__ */ Symbol("lastStepOrdinal"); | |
| var list_default = ListReporter; | |
| // packages/playwright/src/reporters/listModeReporter.ts | |
| var import_path11 = __toESM(require("path")); | |
| var ListModeReporter = class { | |
| constructor(options = {}) { | |
| this._options = options; | |
| this.screen = options?.screen ?? terminalScreen; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| onConfigure(config2) { | |
| this.config = config2; | |
| } | |
| onBegin(suite) { | |
| this._writeLine(`Listing tests:`); | |
| const tests = suite.allTests(); | |
| const files = /* @__PURE__ */ new Set(); | |
| for (const test of tests) { | |
| const [, projectName, , ...titles] = test.titlePath(); | |
| const location = `${import_path11.default.relative(this.config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`; | |
| const testId = this._options.includeTestId ? `[id=${test.id}] ` : ""; | |
| const projectLabel = this._options.includeTestId ? `project=` : ""; | |
| const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : ""; | |
| this._writeLine(` ${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`); | |
| files.add(test.location.file); | |
| } | |
| this._writeLine(`Total: ${tests.length} ${tests.length === 1 ? "test" : "tests"} in ${files.size} ${files.size === 1 ? "file" : "files"}`); | |
| } | |
| onError(error) { | |
| this.screen.stderr.write("\n" + formatError(terminalScreen, error).message + "\n"); | |
| } | |
| _writeLine(line) { | |
| this.screen.stdout.write(line + "\n"); | |
| } | |
| }; | |
| var listModeReporter_default = ListModeReporter; | |
| // packages/playwright/src/reporters/perfetto.ts | |
| var import_fs8 = __toESM(require("fs")); | |
| var import_path12 = __toESM(require("path")); | |
| var import_zlib = __toESM(require("zlib")); | |
| var import_coreBundle2 = require("playwright-core/lib/coreBundle"); | |
| var import_util11 = require("../util"); | |
| var { toPosixPath: toPosixPath4 } = require("playwright-core/lib/coreBundle").utils; | |
| var kStatusColors = { | |
| passed: "good", | |
| failed: "bad", | |
| timedOut: "terrible", | |
| interrupted: "yellow", | |
| skipped: "grey" | |
| }; | |
| var kProcessId = 1; | |
| var kRunThreadId = 0; | |
| var PerfettoReporter = class { | |
| constructor(options) { | |
| this._events = []; | |
| this._laneEndTime = /* @__PURE__ */ new Map(); | |
| this._globalErrors = []; | |
| this._resolvedOutputFile = resolveOutputFile("PERFETTO", { | |
| ...options, | |
| default: { | |
| fileName: "perfetto.json", | |
| outputDir: "test-results" | |
| } | |
| }).outputFile; | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| onConfigure(config2) { | |
| this._config = config2; | |
| } | |
| onBegin(suite) { | |
| this._suite = suite; | |
| } | |
| onError(error) { | |
| this._globalErrors.push({ error, timestamp: Date.now() }); | |
| } | |
| async onEnd(result) { | |
| const entries = []; | |
| for (const test of this._suite?.allTests() ?? []) { | |
| for (const testResult of test.results) | |
| entries.push({ test, result: testResult }); | |
| } | |
| entries.sort((a, b) => a.result.startTime.getTime() - b.result.startTime.getTime()); | |
| for (const entry of entries) | |
| this._appendTestResult(entry.test, entry.result); | |
| for (const { error, timestamp } of this._globalErrors) { | |
| this._events.push({ | |
| name: "error", | |
| cat: "error", | |
| ph: "i", | |
| s: "g", | |
| ts: timestamp, | |
| pid: kProcessId, | |
| tid: kRunThreadId, | |
| cname: "bad", | |
| args: { error: this._formatError(error) } | |
| }); | |
| } | |
| await this._writeReport(result); | |
| } | |
| _appendTestResult(test, result) { | |
| const startMs = result.startTime.getTime(); | |
| let endMs = startMs + Math.max(0, result.duration); | |
| for (const step of result.steps) | |
| endMs = Math.max(endMs, stepEndTime(step)); | |
| const lane = this._allocateLane(result.parallelIndex, startMs); | |
| this._laneEndTime.set(lane, endMs); | |
| const tid = lane + 1; | |
| this._events.push({ | |
| name: test.title, | |
| cat: "test", | |
| ph: "X", | |
| ts: startMs, | |
| dur: endMs - startMs, | |
| pid: kProcessId, | |
| tid, | |
| cname: kStatusColors[result.status], | |
| args: this._testArgs(test, result) | |
| }); | |
| for (const step of result.steps) | |
| this._appendStep(step, tid, startMs, endMs); | |
| } | |
| _allocateLane(preferredLane, startMs) { | |
| const preferred = Math.max(0, preferredLane); | |
| if (!(this._laneEndTime.get(preferred) > startMs)) | |
| return preferred; | |
| for (let lane = 0; ; ++lane) { | |
| if (!(this._laneEndTime.get(lane) > startMs)) | |
| return lane; | |
| } | |
| } | |
| _appendStep(step, tid, parentStart, parentEnd) { | |
| const stepStart = step.startTime.getTime(); | |
| const startMs = clamp(stepStart, parentStart, parentEnd); | |
| const endMs = clamp(step.duration >= 0 ? stepStart + step.duration : parentEnd, startMs, parentEnd); | |
| this._events.push({ | |
| name: step.subtitle ? `${step.title} ${step.subtitle}` : step.title, | |
| cat: step.category, | |
| ph: "X", | |
| ts: startMs, | |
| dur: endMs - startMs, | |
| pid: kProcessId, | |
| tid, | |
| cname: step.error ? "bad" : void 0, | |
| args: this._stepArgs(step) | |
| }); | |
| for (const child of step.steps) | |
| this._appendStep(child, tid, startMs, endMs); | |
| } | |
| _testArgs(test, result) { | |
| const [, projectName, , ...titles] = test.titlePath(); | |
| const args = { | |
| status: result.status, | |
| expectedStatus: test.expectedStatus, | |
| testId: test.id, | |
| workerIndex: result.workerIndex, | |
| parallelIndex: result.parallelIndex, | |
| timeout: test.timeout, | |
| location: this._formatLocation(test.location) | |
| }; | |
| if (titles.length > 1) | |
| args.title = titles.join(" \u203A "); | |
| if (projectName) | |
| args.project = projectName; | |
| if (result.retry) | |
| args.retry = result.retry; | |
| if (test.tags.length) | |
| args.tags = test.tags.join(" "); | |
| if (result.annotations.length) | |
| args.annotations = result.annotations.map((annotation) => formatAnnotation(annotation)); | |
| if (result.attachments.length) | |
| args.attachments = result.attachments.map((attachment) => this._formatAttachment(attachment)); | |
| if (result.errors.length) | |
| args.errors = result.errors.map((error) => this._formatError(error)); | |
| const stdout = concatChunks(result.stdout); | |
| if (stdout) | |
| args.stdout = stdout; | |
| const stderr = concatChunks(result.stderr); | |
| if (stderr) | |
| args.stderr = stderr; | |
| return args; | |
| } | |
| _stepArgs(step) { | |
| const args = {}; | |
| if (step.params) | |
| args.params = step.params; | |
| if (step.location) | |
| args.location = this._formatLocation(step.location); | |
| if (step.annotations.length) | |
| args.annotations = step.annotations.map((annotation) => formatAnnotation(annotation)); | |
| if (step.attachments.length) | |
| args.attachments = step.attachments.map((attachment) => this._formatAttachment(attachment)); | |
| if (step.error) | |
| args.error = this._formatError(step.error); | |
| return Object.keys(args).length ? args : void 0; | |
| } | |
| _formatAttachment(attachment) { | |
| return attachment.path ? this._relativePath(attachment.path) : attachment.name; | |
| } | |
| _formatLocation(location) { | |
| if (!location) | |
| return void 0; | |
| return `${this._relativePath(location.file)}:${location.line}:${location.column}`; | |
| } | |
| _formatError(error) { | |
| return (0, import_util11.stripAnsiEscapes)(formatError(nonTerminalScreen, error).message); | |
| } | |
| _relativePath(file) { | |
| return toPosixPath4(import_path12.default.relative(this._config.rootDir, file)); | |
| } | |
| async _writeReport(result) { | |
| let timeOrigin2 = result.startTime.getTime(); | |
| for (const event of this._events) | |
| timeOrigin2 = Math.min(timeOrigin2, event.ts); | |
| const metadataEvents = [ | |
| { name: "process_name", cat: "__metadata", ph: "M", ts: 0, pid: kProcessId, tid: kRunThreadId, args: { name: "Playwright Test" } }, | |
| { name: "process_sort_index", cat: "__metadata", ph: "M", ts: 0, pid: kProcessId, tid: kRunThreadId, args: { sort_index: 0 } } | |
| ]; | |
| for (const lane of [...this._laneEndTime.keys()].sort((a, b) => a - b)) { | |
| metadataEvents.push({ name: "thread_name", cat: "__metadata", ph: "M", ts: 0, pid: kProcessId, tid: lane + 1, args: { name: `Worker ${lane}` } }); | |
| metadataEvents.push({ name: "thread_sort_index", cat: "__metadata", ph: "M", ts: 0, pid: kProcessId, tid: lane + 1, args: { sort_index: lane + 1 } }); | |
| } | |
| this._events.sort((a, b) => a.ts - b.ts); | |
| for (const event of this._events) { | |
| event.ts = Math.round((event.ts - timeOrigin2) * 1e3); | |
| if (event.dur !== void 0) | |
| event.dur = Math.round(event.dur * 1e3); | |
| } | |
| const metadata = { | |
| "playwright-version": (0, import_coreBundle2.getPlaywrightVersion)(), | |
| "start-time": result.startTime.toISOString(), | |
| "duration": result.duration, | |
| "status": result.status | |
| }; | |
| await import_fs8.default.promises.mkdir(import_path12.default.dirname(this._resolvedOutputFile), { recursive: true }); | |
| const writer = new ChunkWriter(this._resolvedOutputFile); | |
| await writer.write('{"traceEvents":['); | |
| let separator2 = ""; | |
| for (const events of [metadataEvents, this._events]) { | |
| for (const event of events) { | |
| await writer.write(separator2 + JSON.stringify(event)); | |
| separator2 = ","; | |
| } | |
| } | |
| await writer.write(`],"displayTimeUnit":"ms","metadata":${JSON.stringify(metadata)}}`); | |
| await writer.close(); | |
| } | |
| }; | |
| var ChunkWriter = class { | |
| constructor(file) { | |
| const fileStream = import_fs8.default.createWriteStream(file); | |
| const gzip = file.endsWith(".gz") ? import_zlib.default.createGzip() : void 0; | |
| gzip?.pipe(fileStream); | |
| this._stream = gzip ?? fileStream; | |
| this._closed = new Promise((resolve) => fileStream.on("close", resolve)); | |
| for (const stream of /* @__PURE__ */ new Set([this._stream, fileStream])) | |
| stream.on("error", (error) => this._error ??= error); | |
| } | |
| async write(chunk) { | |
| if (this._error) | |
| throw this._error; | |
| if (!this._stream.write(chunk)) | |
| await new Promise((resolve) => this._stream.once("drain", () => resolve())); | |
| } | |
| async close() { | |
| this._stream.end(); | |
| await this._closed; | |
| if (this._error) | |
| throw this._error; | |
| } | |
| }; | |
| function stepEndTime(step) { | |
| let end = step.startTime.getTime() + Math.max(0, step.duration); | |
| for (const child of step.steps) | |
| end = Math.max(end, stepEndTime(child)); | |
| return end; | |
| } | |
| function formatAnnotation(annotation) { | |
| return annotation.description ? `${annotation.type}: ${annotation.description}` : annotation.type; | |
| } | |
| function clamp(value, min, max) { | |
| return Math.min(Math.max(value, min), max); | |
| } | |
| function concatChunks(chunks) { | |
| return chunks.map((chunk) => typeof chunk === "string" ? chunk : chunk.toString("utf8")).join(""); | |
| } | |
| var perfetto_default = PerfettoReporter; | |
| // packages/playwright/src/runner/reporters.ts | |
| var { calculateSha1: calculateSha13 } = require("playwright-core/lib/coreBundle").utils; | |
| async function createReporters(config2, mode, descriptions, runOptions) { | |
| const defaultReporters = { | |
| "blob": BlobReporter, | |
| "perfetto": perfetto_default, | |
| "dot": mode === "list" ? listModeReporter_default : dot_default, | |
| "line": mode === "list" ? listModeReporter_default : line_default, | |
| "list": mode === "list" ? listModeReporter_default : list_default, | |
| "github": github_default, | |
| "json": json_default, | |
| "junit": junit_default, | |
| "null": empty_default, | |
| "html": html_default | |
| }; | |
| const reporters = []; | |
| descriptions ??= config2.config.reporter; | |
| const reportOptions = reporterCommandOptions(config2, mode, runOptions); | |
| for (const r of descriptions) { | |
| const [name, arg] = r; | |
| const options = { ...reportOptions, ...arg }; | |
| if (name in defaultReporters) { | |
| reporters.push(new defaultReporters[name](options)); | |
| } else { | |
| const reporterConstructor = await loadReporter(config2, name); | |
| reporters.push(wrapReporterAsV2(new reporterConstructor(options))); | |
| } | |
| } | |
| if (process.env.PW_TEST_REPORTER) { | |
| const name = process.env.PW_TEST_REPORTER; | |
| if (name in defaultReporters) { | |
| reporters.push(new defaultReporters[name](reportOptions)); | |
| } else { | |
| const reporterConstructor = await loadReporter(config2, name); | |
| reporters.push(wrapReporterAsV2(new reporterConstructor(reportOptions))); | |
| } | |
| } | |
| const someReporterPrintsToStdio = reporters.some((r) => r.printsToStdio ? r.printsToStdio() : true); | |
| if (reporters.length && !someReporterPrintsToStdio) { | |
| if (mode === "list") | |
| reporters.unshift(new listModeReporter_default()); | |
| else if (mode !== "merge") | |
| reporters.unshift(!process.env.CI ? new line_default() : new dot_default()); | |
| } | |
| return reporters; | |
| } | |
| function createErrorCollectingReporter(screen) { | |
| const errors = []; | |
| return { | |
| version: () => "v2", | |
| onError(error) { | |
| errors.push(error); | |
| screen.stderr?.write(formatError(screen, error).message + "\n"); | |
| }, | |
| errors: () => errors | |
| }; | |
| } | |
| function reporterCommandOptions(config2, mode, runOptions) { | |
| return { | |
| configDir: config2.configDir, | |
| _mode: mode, | |
| _commandHash: computeCommandHash(config2, runOptions) | |
| }; | |
| } | |
| function computeCommandHash(config2, runOptions) { | |
| const parts = []; | |
| if (runOptions?.projectFilter) | |
| parts.push(...runOptions.projectFilter); | |
| const command = {}; | |
| if (runOptions?.locations?.length) | |
| command.locations = runOptions.locations; | |
| if (runOptions?.grep) | |
| command.grep = runOptions.grep; | |
| if (runOptions?.grepInvert) | |
| command.grepInvert = runOptions.grepInvert; | |
| if (runOptions?.onlyChanged) | |
| command.onlyChanged = runOptions.onlyChanged; | |
| if (config2.config.tags.length) | |
| command.tags = config2.config.tags.join(" "); | |
| if (runOptions?.testList) | |
| command.testList = calculateSha13(import_fs9.default.readFileSync(runOptions.testList)); | |
| if (runOptions?.testListInvert) | |
| command.testListInvert = calculateSha13(import_fs9.default.readFileSync(runOptions.testListInvert)); | |
| if (Object.keys(command).length) | |
| parts.push(calculateSha13(JSON.stringify(command)).substring(0, 7)); | |
| return parts.join("-"); | |
| } | |
| // packages/playwright/src/runner/tasks.ts | |
| var import_fs12 = __toESM(require("fs")); | |
| var import_path16 = __toESM(require("path")); | |
| var import_util14 = require("util"); | |
| // packages/playwright/src/runner/rebase.ts | |
| var import_fs10 = __toESM(require("fs")); | |
| var import_path13 = __toESM(require("path")); | |
| var babel3 = __toESM(require("../transform/babelBundle")); | |
| var colors3 = require("playwright-core/lib/utilsBundle").colors; | |
| var diff = require("playwright-core/lib/utilsBundle").diff; | |
| var { MultiMap: MultiMap3 } = require("playwright-core/lib/coreBundle").iso; | |
| var t = babel3.types; | |
| var suggestedRebaselines = new MultiMap3(); | |
| function addSuggestedRebaseline(location, suggestedRebaseline) { | |
| suggestedRebaselines.set(location.file, { location, code: suggestedRebaseline }); | |
| } | |
| function clearSuggestedRebaselines() { | |
| suggestedRebaselines.clear(); | |
| } | |
| async function applySuggestedRebaselines(config2, reporter, filteredProjects) { | |
| if (config2.config.updateSnapshots === "none") | |
| return; | |
| if (!suggestedRebaselines.size) | |
| return; | |
| const [project] = filteredProjects; | |
| if (!project) | |
| return; | |
| const patches = []; | |
| const files = []; | |
| const gitCache = /* @__PURE__ */ new Map(); | |
| const patchFile = import_path13.default.join(project.project.outputDir, "rebaselines.patch"); | |
| for (const fileName of [...suggestedRebaselines.keys()].sort()) { | |
| const source = await import_fs10.default.promises.readFile(fileName, "utf8"); | |
| const lines = source.split("\n"); | |
| const replacements = suggestedRebaselines.get(fileName); | |
| const fileNode = babel3.babelParse(source, fileName, true); | |
| const ranges = []; | |
| babel3.traverse(fileNode, { | |
| CallExpression: (path20) => { | |
| const node = path20.node; | |
| if (node.arguments.length < 1) | |
| return; | |
| if (!t.isMemberExpression(node.callee)) | |
| return; | |
| const argument = node.arguments[0]; | |
| if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument)) | |
| return; | |
| const prop = node.callee.property; | |
| if (!prop.loc || !argument.start || !argument.end) | |
| return; | |
| for (const replacement of replacements) { | |
| if (prop.loc.start.line !== replacement.location.line) | |
| continue; | |
| if (prop.loc.start.column + 1 !== replacement.location.column) | |
| continue; | |
| const indent2 = lines[prop.loc.start.line - 1].match(/^\s*/)[0]; | |
| const newText = replacement.code.replace(/\{indent\}/g, indent2); | |
| ranges.push({ start: argument.start, end: argument.end, oldText: source.substring(argument.start, argument.end), newText }); | |
| break; | |
| } | |
| } | |
| }); | |
| ranges.sort((a, b) => b.start - a.start); | |
| let result = source; | |
| for (const range of ranges) | |
| result = result.substring(0, range.start) + range.newText + result.substring(range.end); | |
| const relativeName = import_path13.default.relative(process.cwd(), fileName); | |
| files.push(relativeName); | |
| if (config2.config.updateSourceMethod === "overwrite") { | |
| await import_fs10.default.promises.writeFile(fileName, result); | |
| } else if (config2.config.updateSourceMethod === "3way") { | |
| await import_fs10.default.promises.writeFile(fileName, applyPatchWithConflictMarkers(source, result)); | |
| } else { | |
| const gitFolder = findGitRoot(import_path13.default.dirname(fileName), gitCache); | |
| const relativeToGit = import_path13.default.relative(gitFolder || process.cwd(), fileName); | |
| patches.push(createPatch(relativeToGit, source, result)); | |
| } | |
| } | |
| const fileList = files.map((file) => " " + colors3.dim(file)).join("\n"); | |
| reporter.onStdErr(` | |
| New baselines created for: | |
| ${fileList} | |
| `); | |
| if (config2.config.updateSourceMethod === "patch") { | |
| await import_fs10.default.promises.mkdir(import_path13.default.dirname(patchFile), { recursive: true }); | |
| await import_fs10.default.promises.writeFile(patchFile, patches.join("\n")); | |
| reporter.onStdErr(` | |
| ` + colors3.cyan("git apply " + import_path13.default.relative(process.cwd(), patchFile)) + "\n"); | |
| } | |
| } | |
| function createPatch(fileName, before, after) { | |
| const file = fileName.replace(/\\/g, "/"); | |
| const text = diff.createPatch(file, before, after, void 0, void 0, { context: 3 }); | |
| return [ | |
| "diff --git a/" + file + " b/" + file, | |
| "--- a/" + file, | |
| "+++ b/" + file, | |
| ...text.split("\n").slice(4) | |
| ].join("\n"); | |
| } | |
| function findGitRoot(dir, cache) { | |
| const result = cache.get(dir); | |
| if (result !== void 0) | |
| return result; | |
| const gitPath = import_path13.default.join(dir, ".git"); | |
| if (import_fs10.default.existsSync(gitPath) && import_fs10.default.lstatSync(gitPath).isDirectory()) { | |
| cache.set(dir, dir); | |
| return dir; | |
| } | |
| const parentDir = import_path13.default.dirname(dir); | |
| if (dir === parentDir) { | |
| cache.set(dir, null); | |
| return null; | |
| } | |
| const parentResult = findGitRoot(parentDir, cache); | |
| cache.set(dir, parentResult); | |
| return parentResult; | |
| } | |
| function applyPatchWithConflictMarkers(oldText, newText) { | |
| const diffResult = diff.diffLines(oldText, newText); | |
| let result = ""; | |
| let conflict = false; | |
| diffResult.forEach((part) => { | |
| if (part.added) { | |
| if (conflict) { | |
| result += part.value; | |
| result += ">>>>>>> SNAPSHOT\n"; | |
| conflict = false; | |
| } else { | |
| result += "<<<<<<< HEAD\n"; | |
| result += part.value; | |
| result += "=======\n"; | |
| conflict = true; | |
| } | |
| } else if (part.removed) { | |
| result += "<<<<<<< HEAD\n"; | |
| result += part.value; | |
| result += "=======\n"; | |
| conflict = true; | |
| } else { | |
| if (conflict) { | |
| result += ">>>>>>> SNAPSHOT\n"; | |
| conflict = false; | |
| } | |
| result += part.value; | |
| } | |
| }); | |
| if (conflict) | |
| result += ">>>>>>> SNAPSHOT\n"; | |
| return result; | |
| } | |
| // packages/playwright/src/runner/workerHost.ts | |
| var import_fs11 = __toESM(require("fs")); | |
| var import_path14 = __toESM(require("path")); | |
| var import_common6 = require("../common"); | |
| // packages/playwright/src/isomorphic/folders.ts | |
| function artifactsFolderName(workerIndex) { | |
| return `.playwright-artifacts-${workerIndex}`; | |
| } | |
| // packages/playwright/src/runner/workerHost.ts | |
| var { removeFolders: removeFolders3 } = require("playwright-core/lib/coreBundle").utils; | |
| var lastWorkerIndex = 0; | |
| var WorkerHost = class extends ProcessHost { | |
| constructor(testGroup, options) { | |
| const workerIndex = lastWorkerIndex++; | |
| super(require.resolve("../worker/workerProcessEntry.js"), `worker-${workerIndex}`, { | |
| ...options.extraEnv, | |
| FORCE_COLOR: "1", | |
| DEBUG_COLORS: process.env.DEBUG_COLORS === void 0 ? "1" : process.env.DEBUG_COLORS | |
| }); | |
| this._didFail = false; | |
| this.workerIndex = workerIndex; | |
| this.parallelIndex = options.parallelIndex; | |
| this._hash = testGroup.workerHash; | |
| this._params = { | |
| workerIndex: this.workerIndex, | |
| parallelIndex: options.parallelIndex, | |
| repeatEachIndex: testGroup.repeatEachIndex, | |
| projectId: testGroup.projectId, | |
| config: options.config, | |
| artifactsDir: import_path14.default.join(options.outputDir, artifactsFolderName(workerIndex)), | |
| pauseOnError: options.pauseOnError, | |
| pauseAtEnd: options.pauseAtEnd | |
| }; | |
| } | |
| async start() { | |
| await import_fs11.default.promises.mkdir(this._params.artifactsDir, { recursive: true }); | |
| return await this.startRunner(this._params, { | |
| onStdOut: (chunk) => this.emit("stdOut", import_common6.ipc.stdioChunkToParams(chunk)), | |
| onStdErr: (chunk) => this.emit("stdErr", import_common6.ipc.stdioChunkToParams(chunk)) | |
| }); | |
| } | |
| async onExit() { | |
| await removeFolders3([this._params.artifactsDir]); | |
| } | |
| async stop(didFail) { | |
| if (didFail) | |
| this._didFail = true; | |
| await super.stop(); | |
| } | |
| runTestGroup(runPayload) { | |
| this.sendMessageNoReply({ method: "runTestGroup", params: runPayload }); | |
| } | |
| async sendCustomMessage(payload) { | |
| return await this.sendMessage({ method: "customMessage", params: payload }); | |
| } | |
| sendResume(payload) { | |
| this.sendMessageNoReply({ method: "resume", params: payload }); | |
| } | |
| hash() { | |
| return this._hash; | |
| } | |
| projectId() { | |
| return this._params.projectId; | |
| } | |
| didFail() { | |
| return this._didFail; | |
| } | |
| }; | |
| // packages/playwright/src/runner/dispatcher.ts | |
| var import_common7 = require("../common"); | |
| var import_util12 = require("../util"); | |
| var colors4 = require("playwright-core/lib/utilsBundle").colors; | |
| var { ManualPromise: ManualPromise3 } = require("playwright-core/lib/coreBundle").iso; | |
| var { eventsHelper } = require("playwright-core/lib/coreBundle").utils; | |
| var Dispatcher = class { | |
| constructor(testRun, options = {}) { | |
| // Worker slot is claimed when it has jobDispatcher assigned. | |
| this._workerSlots = []; | |
| this._queue = []; | |
| this._isolatedJobs = /* @__PURE__ */ new Set(); | |
| this._workerLimitPerProjectId = /* @__PURE__ */ new Map(); | |
| this._queuedOrRunningHashCount = /* @__PURE__ */ new Map(); | |
| this._finished = new ManualPromise3(); | |
| this._isStopped = true; | |
| this._extraEnvByProjectId = /* @__PURE__ */ new Map(); | |
| this._producedEnvByProjectId = /* @__PURE__ */ new Map(); | |
| this._testRun = testRun; | |
| this._ignoreMaxFailures = !!options.ignoreMaxFailures; | |
| for (const project of testRun.config.projects) { | |
| if (project.workers) | |
| this._workerLimitPerProjectId.set(project.id, project.workers); | |
| } | |
| } | |
| _heldLocks() { | |
| const heldLocks = /* @__PURE__ */ new Set(); | |
| for (const slot of this._workerSlots) { | |
| for (const lock of slot.jobDispatcher?.job.locks || []) | |
| heldLocks.add(lock); | |
| } | |
| return heldLocks; | |
| } | |
| _findFirstJobToRun() { | |
| const heldLocks = this._heldLocks(); | |
| for (let index = 0; index < this._queue.length; index++) { | |
| const job = this._queue[index]; | |
| if (this._isolatedJobs.has(job) && this._workerSlots.some((w) => !!w.jobDispatcher)) | |
| continue; | |
| if (job.locks.some((lock) => heldLocks.has(lock))) | |
| continue; | |
| const projectIdWorkerLimit = this._workerLimitPerProjectId.get(job.projectId); | |
| if (!projectIdWorkerLimit) | |
| return index; | |
| const runningWorkersWithSameProjectId = this._workerSlots.filter((w) => w.jobDispatcher?.job.projectId === job.projectId).length; | |
| if (runningWorkersWithSameProjectId < projectIdWorkerLimit) | |
| return index; | |
| } | |
| return -1; | |
| } | |
| _scheduleJobs() { | |
| while (this._scheduleJob()) { | |
| } | |
| } | |
| _scheduleJob() { | |
| if (this._isStopped || !this._workerSlots.some((w) => !w.jobDispatcher)) | |
| return false; | |
| const jobIndex = this._findFirstJobToRun(); | |
| if (jobIndex === -1) | |
| return false; | |
| const job = this._queue[jobIndex]; | |
| let workerIndex = this._workerSlots.findIndex((w) => !w.jobDispatcher && w.worker && w.worker.hash() === job.workerHash && !w.worker.didSendStop()); | |
| if (workerIndex === -1) | |
| workerIndex = this._workerSlots.findIndex((w) => !w.jobDispatcher); | |
| if (workerIndex === -1) { | |
| return false; | |
| } | |
| this._queue.splice(jobIndex, 1); | |
| const jobDispatcher = new JobDispatcher(job, this._testRun, this._ignoreMaxFailures ? void 0 : () => this.stop().catch(() => { | |
| })); | |
| this._workerSlots[workerIndex].jobDispatcher = jobDispatcher; | |
| void this._runJobInWorker(workerIndex, jobDispatcher).then(() => { | |
| this._workerSlots[workerIndex].jobDispatcher = void 0; | |
| this._checkFinished(); | |
| this._scheduleJobs(); | |
| }); | |
| return true; | |
| } | |
| async _runJobInWorker(index, jobDispatcher) { | |
| const job = jobDispatcher.job; | |
| if (jobDispatcher.skipWholeJob()) | |
| return; | |
| let worker = this._workerSlots[index].worker; | |
| if (worker && (worker.hash() !== job.workerHash || worker.didSendStop())) { | |
| await worker.stop(); | |
| worker = void 0; | |
| if (this._isStopped) | |
| return; | |
| } | |
| let startError; | |
| if (!worker) { | |
| worker = this._createWorker(job, index, import_common7.ipc.serializeConfig(this._testRun.config, true)); | |
| this._workerSlots[index].worker = worker; | |
| worker.on("exit", () => this._workerSlots[index].worker = void 0); | |
| startError = await worker.start(); | |
| if (this._isStopped) | |
| return; | |
| } | |
| if (startError) | |
| jobDispatcher.onExit(startError); | |
| else | |
| jobDispatcher.runInWorker(worker); | |
| const result = await jobDispatcher.jobResult; | |
| this._updateCounterForWorkerHash(job.workerHash, -1); | |
| if (result.didFail) | |
| void worker.stop( | |
| true | |
| /* didFail */ | |
| ); | |
| else if (this._isWorkerRedundant(worker)) | |
| void worker.stop(); | |
| if (!this._isStopped) { | |
| if (result.remainingJob) { | |
| this._queue.unshift(result.remainingJob); | |
| this._updateCounterForWorkerHash(result.remainingJob.workerHash, 1); | |
| } | |
| if (result.isolatedRetriesJob) { | |
| this._isolatedJobs.add(result.isolatedRetriesJob); | |
| this._queue.push(result.isolatedRetriesJob); | |
| this._updateCounterForWorkerHash(result.isolatedRetriesJob.workerHash, 1); | |
| } | |
| } | |
| } | |
| _checkFinished() { | |
| if (this._finished.isDone()) | |
| return; | |
| if (this._queue.length && !this._isStopped) | |
| return; | |
| if (this._workerSlots.some((w) => !!w.jobDispatcher)) | |
| return; | |
| this._finished.resolve(); | |
| } | |
| _isWorkerRedundant(worker) { | |
| let workersWithSameHash = 0; | |
| for (const slot of this._workerSlots) { | |
| if (slot.worker && !slot.worker.didSendStop() && slot.worker.hash() === worker.hash()) | |
| workersWithSameHash++; | |
| } | |
| return workersWithSameHash > this._queuedOrRunningHashCount.get(worker.hash()); | |
| } | |
| _updateCounterForWorkerHash(hash, delta) { | |
| this._queuedOrRunningHashCount.set(hash, delta + (this._queuedOrRunningHashCount.get(hash) || 0)); | |
| } | |
| async run(testGroups, extraEnvByProjectId) { | |
| this._extraEnvByProjectId = extraEnvByProjectId; | |
| this._queue = testGroups; | |
| for (const group of testGroups) | |
| this._updateCounterForWorkerHash(group.workerHash, 1); | |
| this._isStopped = false; | |
| this._workerSlots = []; | |
| if (!this._ignoreMaxFailures && this._testRun.hasReachedMaxFailures()) | |
| void this.stop(); | |
| for (let i = 0; i < this._testRun.config.config.workers; i++) | |
| this._workerSlots.push({}); | |
| this._scheduleJobs(); | |
| this._checkFinished(); | |
| await this._finished; | |
| } | |
| _createWorker(testGroup, parallelIndex, loaderData) { | |
| const project = this._testRun.config.projects.find((p) => p.id === testGroup.projectId); | |
| const pauseAtEnd = this._testRun.topLevelProjects.includes(project) && !!this._testRun.options.pauseAtEnd; | |
| const worker = new WorkerHost(testGroup, { | |
| parallelIndex, | |
| config: loaderData, | |
| extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {}, | |
| outputDir: project.project.outputDir, | |
| pauseOnError: !!this._testRun.options.pauseOnError, | |
| pauseAtEnd | |
| }); | |
| const handleOutput = (params) => { | |
| const chunk = chunkFromParams(params); | |
| if (worker.didFail()) { | |
| return { chunk }; | |
| } | |
| const currentlyRunning = this._workerSlots[parallelIndex].jobDispatcher?.currentlyRunning(); | |
| if (!currentlyRunning) | |
| return { chunk }; | |
| return { chunk, test: currentlyRunning.test, result: currentlyRunning.result }; | |
| }; | |
| worker.on("stdOut", (params) => { | |
| const { chunk, test, result } = handleOutput(params); | |
| result?.stdout.push(chunk); | |
| this._testRun.reporter.onStdOut?.(chunk, test, result); | |
| }); | |
| worker.on("stdErr", (params) => { | |
| const { chunk, test, result } = handleOutput(params); | |
| result?.stderr.push(chunk); | |
| this._testRun.reporter.onStdErr?.(chunk, test, result); | |
| }); | |
| worker.on("teardownErrors", (params) => { | |
| this._testRun.hasWorkerErrors = true; | |
| const workerInfo = { | |
| config: this._testRun.config.config, | |
| project: project.project, | |
| workerIndex: worker.workerIndex, | |
| parallelIndex: worker.parallelIndex | |
| }; | |
| for (const error of params.fatalErrors) | |
| this._testRun.reporter.onError?.(error, workerInfo); | |
| }); | |
| worker.on("processError", (error) => { | |
| this._testRun.hasWorkerErrors = true; | |
| this._testRun.reporter.onError?.(error); | |
| }); | |
| worker.on("exit", () => { | |
| const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {}; | |
| this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() }); | |
| }); | |
| return worker; | |
| } | |
| producedEnvByProjectId() { | |
| return this._producedEnvByProjectId; | |
| } | |
| async stop() { | |
| if (this._isStopped) | |
| return; | |
| this._isStopped = true; | |
| await Promise.all(this._workerSlots.map(({ worker }) => worker?.stop())); | |
| this._checkFinished(); | |
| } | |
| }; | |
| var JobDispatcher = class { | |
| constructor(job, testRun, onMaxFailuresReached) { | |
| this.jobResult = new ManualPromise3(); | |
| this._listeners = []; | |
| this._failedTests = /* @__PURE__ */ new Set(); | |
| this._failedWithNonRetriableError = /* @__PURE__ */ new Set(); | |
| this._remainingByTestId = /* @__PURE__ */ new Map(); | |
| this._dataByTestId = /* @__PURE__ */ new Map(); | |
| this._parallelIndex = 0; | |
| this._workerIndex = 0; | |
| this.job = job; | |
| this._testRun = testRun; | |
| this._onMaxFailuresReached = onMaxFailuresReached; | |
| this._remainingByTestId = new Map(this.job.tests.map((e) => [e.id, e])); | |
| } | |
| _isStoppedByMaxFailures() { | |
| return !!this._onMaxFailuresReached && this._testRun.hasReachedMaxFailures(); | |
| } | |
| _onTestBegin(params) { | |
| const test = this._remainingByTestId.get(params.testId); | |
| if (!test) { | |
| return; | |
| } | |
| const result = test._appendTestResult(); | |
| this._dataByTestId.set(test.id, { test, result, steps: /* @__PURE__ */ new Map() }); | |
| result.parallelIndex = this._parallelIndex; | |
| result.workerIndex = this._workerIndex; | |
| result.startTime = new Date(params.startWallTime); | |
| this._testRun.reporter.onTestBegin?.(test, result); | |
| this._currentlyRunning = { test, result }; | |
| } | |
| _onTestEnd(params) { | |
| if (this._isStoppedByMaxFailures()) { | |
| params.status = "interrupted"; | |
| params.errors = []; | |
| } | |
| const data = this._dataByTestId.get(params.testId); | |
| if (!data) { | |
| return; | |
| } | |
| this._dataByTestId.delete(params.testId); | |
| this._remainingByTestId.delete(params.testId); | |
| const { result, test } = data; | |
| result.duration = params.duration; | |
| result.errors = params.errors; | |
| result.error = result.errors[0]; | |
| result.status = params.status; | |
| result.annotations = params.annotations; | |
| test.annotations = [...params.annotations]; | |
| test.expectedStatus = params.expectedStatus; | |
| test.timeout = params.timeout; | |
| const isFailure = result.status !== "skipped" && result.status !== test.expectedStatus; | |
| if (isFailure) | |
| this._failedTests.add(test); | |
| if (params.hasNonRetriableError) | |
| this._addNonretriableTestAndSerialModeParents(test); | |
| this._reportTestEnd(test, result); | |
| this._currentlyRunning = void 0; | |
| } | |
| _addNonretriableTestAndSerialModeParents(test) { | |
| this._failedWithNonRetriableError.add(test); | |
| for (let parent = test.parent; parent; parent = parent.parent) { | |
| if (parent._parallelMode === "serial") | |
| this._failedWithNonRetriableError.add(parent); | |
| } | |
| } | |
| _onStepBegin(params) { | |
| const data = this._dataByTestId.get(params.testId); | |
| if (!data) { | |
| return; | |
| } | |
| const { result, steps, test } = data; | |
| const parentStep = params.parentStepId ? steps.get(params.parentStepId) : void 0; | |
| const step = { | |
| title: params.title, | |
| subtitle: params.subtitle, | |
| titlePath: () => { | |
| const parentPath = parentStep?.titlePath() || []; | |
| return [...parentPath, params.title]; | |
| }, | |
| parent: parentStep, | |
| category: params.category, | |
| params: params.params, | |
| startTime: new Date(params.wallTime), | |
| duration: -1, | |
| steps: [], | |
| attachments: [], | |
| annotations: [], | |
| location: params.location | |
| }; | |
| steps.set(params.stepId, step); | |
| (parentStep || result).steps.push(step); | |
| this._testRun.reporter.onStepBegin?.(test, result, step); | |
| } | |
| _onStepEnd(params) { | |
| const data = this._dataByTestId.get(params.testId); | |
| if (!data) { | |
| return; | |
| } | |
| const { result, steps, test } = data; | |
| const step = steps.get(params.stepId); | |
| if (!step) { | |
| this._testRun.reporter.onStdErr?.("Internal error: step end without step begin: " + params.stepId, test, result); | |
| return; | |
| } | |
| step.duration = params.wallTime - step.startTime.getTime(); | |
| if (params.error) | |
| step.error = params.error; | |
| if (params.suggestedRebaseline) | |
| addSuggestedRebaseline(step.location, params.suggestedRebaseline); | |
| step.annotations = params.annotations; | |
| steps.delete(params.stepId); | |
| this._testRun.reporter.onStepEnd?.(test, result, step); | |
| } | |
| _onAttach(params) { | |
| const data = this._dataByTestId.get(params.testId); | |
| if (!data) { | |
| return; | |
| } | |
| const attachment = { | |
| name: params.name, | |
| path: params.path, | |
| contentType: params.contentType, | |
| body: params.body !== void 0 ? Buffer.from(params.body, "base64") : void 0 | |
| }; | |
| data.result.attachments.push(attachment); | |
| if (params.stepId) { | |
| const step = data.steps.get(params.stepId); | |
| if (step) | |
| step.attachments.push(attachment); | |
| else | |
| this._testRun.reporter.onStdErr?.("Internal error: step id not found: " + params.stepId); | |
| } | |
| } | |
| _failTestWithErrors(test, errors) { | |
| const runData = this._dataByTestId.get(test.id); | |
| let result; | |
| if (runData) { | |
| result = runData.result; | |
| } else { | |
| result = test._appendTestResult(); | |
| this._testRun.reporter.onTestBegin?.(test, result); | |
| } | |
| result.errors = [...errors]; | |
| result.error = result.errors[0]; | |
| result.status = errors.length ? "failed" : "skipped"; | |
| this._reportTestEnd(test, result); | |
| this._failedTests.add(test); | |
| } | |
| _massSkipTestsFromRemaining(testIds, errors) { | |
| for (const test of this._remainingByTestId.values()) { | |
| if (!testIds.has(test.id)) | |
| continue; | |
| if (!this._isStoppedByMaxFailures()) { | |
| this._failTestWithErrors(test, errors); | |
| errors = []; | |
| } | |
| this._remainingByTestId.delete(test.id); | |
| } | |
| if (errors.length) { | |
| this._testRun.hasWorkerErrors = true; | |
| for (const error of errors) | |
| this._testRun.reporter.onError?.(error); | |
| } | |
| } | |
| _onDone(params) { | |
| if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError && !params.stoppedDueToUnhandledErrorInTestFail) { | |
| this._finished({ didFail: false }); | |
| return; | |
| } | |
| for (const testId of params.fatalUnknownTestIds || []) { | |
| const test = this._remainingByTestId.get(testId); | |
| if (test) { | |
| this._remainingByTestId.delete(testId); | |
| this._failTestWithErrors(test, [{ message: `Test not found in the worker process. Make sure test title does not change.` }]); | |
| } | |
| } | |
| if (params.fatalErrors.length) { | |
| this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), params.fatalErrors); | |
| } | |
| this._massSkipTestsFromRemaining(new Set(params.skipTestsDueToSetupFailure), []); | |
| if (params.unexpectedExitError) { | |
| if (this._currentlyRunning) | |
| this._massSkipTestsFromRemaining(/* @__PURE__ */ new Set([this._currentlyRunning.test.id]), [params.unexpectedExitError]); | |
| else | |
| this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError]); | |
| } | |
| const retryCandidates = /* @__PURE__ */ new Set(); | |
| const serialSuitesWithFailures = /* @__PURE__ */ new Set(); | |
| for (const failedTest of this._failedTests) { | |
| if (this._failedWithNonRetriableError.has(failedTest)) | |
| continue; | |
| retryCandidates.add(failedTest); | |
| let outermostSerialSuite; | |
| for (let parent = failedTest.parent; parent; parent = parent.parent) { | |
| if (parent._parallelMode === "serial") | |
| outermostSerialSuite = parent; | |
| } | |
| if (outermostSerialSuite && !this._failedWithNonRetriableError.has(outermostSerialSuite)) | |
| serialSuitesWithFailures.add(outermostSerialSuite); | |
| } | |
| const testsBelongingToSomeSerialSuiteWithFailures = [...this._remainingByTestId.values()].filter((test) => { | |
| let parent = test.parent; | |
| while (parent && !serialSuitesWithFailures.has(parent)) | |
| parent = parent.parent; | |
| return !!parent; | |
| }); | |
| this._massSkipTestsFromRemaining(new Set(testsBelongingToSomeSerialSuiteWithFailures.map((test) => test.id)), []); | |
| for (const serialSuite of serialSuitesWithFailures) { | |
| serialSuite.allTests().forEach((test) => retryCandidates.add(test)); | |
| } | |
| const remaining = [...this._remainingByTestId.values()]; | |
| const isolatedRetries = []; | |
| for (const test of retryCandidates) { | |
| if (test.results.length < test.retries + 1) { | |
| if (this._testRun.config.retryStrategy === "immediate") | |
| remaining.push(test); | |
| else | |
| isolatedRetries.push(test); | |
| } | |
| } | |
| const remainingJob = remaining.length ? { ...this.job, tests: remaining } : void 0; | |
| const isolatedRetriesJob = isolatedRetries.length ? { ...this.job, tests: isolatedRetries } : void 0; | |
| this._finished({ didFail: true, remainingJob, isolatedRetriesJob }); | |
| } | |
| onExit(data) { | |
| const unexpectedExitError = data.unexpectedly ? { | |
| message: `Error: worker process exited unexpectedly (code=${data.code}, signal=${data.signal})` | |
| } : void 0; | |
| this._onDone({ skipTestsDueToSetupFailure: [], fatalErrors: [], unexpectedExitError }); | |
| } | |
| _finished(result) { | |
| eventsHelper.removeEventListeners(this._listeners); | |
| this.jobResult.resolve(result); | |
| } | |
| runInWorker(worker) { | |
| this._parallelIndex = worker.parallelIndex; | |
| this._workerIndex = worker.workerIndex; | |
| const runPayload = { | |
| file: this.job.requireFile, | |
| entries: this.job.tests.map((test) => { | |
| return { testId: test.id, retry: test.results.length, planAnnotations: test._planAnnotations }; | |
| }) | |
| }; | |
| worker.runTestGroup(runPayload); | |
| this._listeners = [ | |
| eventsHelper.addEventListener(worker, "testBegin", this._onTestBegin.bind(this)), | |
| eventsHelper.addEventListener(worker, "testEnd", this._onTestEnd.bind(this)), | |
| eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)), | |
| eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)), | |
| eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)), | |
| eventsHelper.addEventListener(worker, "testPaused", this._onTestPaused.bind(this, worker)), | |
| eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)), | |
| eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this)) | |
| ]; | |
| } | |
| _onTestPaused(worker, params) { | |
| const data = this._dataByTestId.get(params.testId); | |
| if (!data) | |
| return; | |
| const { result, test } = data; | |
| const sendMessage = async (message) => { | |
| try { | |
| if (this.jobResult.isDone()) | |
| throw new Error("Test has already stopped"); | |
| const response = await worker.sendCustomMessage({ testId: test.id, request: message.request }); | |
| if (response.error) | |
| addLocationAndSnippetToError(this._testRun.config.config, response.error); | |
| return response; | |
| } catch (e) { | |
| const error = (0, import_util12.serializeError)(e); | |
| addLocationAndSnippetToError(this._testRun.config.config, error); | |
| return { response: void 0, error }; | |
| } | |
| }; | |
| result.status = params.status; | |
| result.errors = params.errors; | |
| result.error = result.errors[0]; | |
| void this._testRun.reporter.onTestPaused?.(test, result).then(() => { | |
| worker.sendResume({}); | |
| }); | |
| this._testRun.onTestPaused({ ...params, sendMessage }); | |
| } | |
| skipWholeJob() { | |
| const allTestsSkipped = this.job.tests.every((test) => test.expectedStatus === "skipped"); | |
| if (allTestsSkipped && !this._isStoppedByMaxFailures()) { | |
| for (const test of this.job.tests) { | |
| const result = test._appendTestResult(); | |
| this._testRun.reporter.onTestBegin?.(test, result); | |
| result.status = "skipped"; | |
| result.annotations = [...test.annotations]; | |
| this._reportTestEnd(test, result); | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| currentlyRunning() { | |
| return this._currentlyRunning; | |
| } | |
| _reportTestEnd(test, result) { | |
| this._testRun.reporter.onTestEnd?.(test, result); | |
| const hadMaxFailures = this._isStoppedByMaxFailures(); | |
| if (test.outcome() === "unexpected" && test.results.length > test.retries) | |
| ++this._testRun.failedTestCount; | |
| if (!hadMaxFailures && this._isStoppedByMaxFailures()) { | |
| this._onMaxFailuresReached?.(); | |
| this._testRun.reporter.onError?.({ message: colors4.red(`Testing stopped early after ${this._testRun.config.config.maxFailures} maximum allowed failures.`) }); | |
| } | |
| } | |
| }; | |
| function chunkFromParams(params) { | |
| if (typeof params.text === "string") | |
| return params.text; | |
| return Buffer.from(params.buffer, "base64"); | |
| } | |
| // packages/playwright/src/runner/sigIntWatcher.ts | |
| var SigIntWatcher = class { | |
| constructor() { | |
| this._hadSignal = false; | |
| let sigintCallback; | |
| this._sigintPromise = new Promise((f) => sigintCallback = f); | |
| this._sigintHandler = () => { | |
| FixedNodeSIGINTHandler.off(this._sigintHandler); | |
| this._hadSignal = true; | |
| sigintCallback(); | |
| }; | |
| FixedNodeSIGINTHandler.on(this._sigintHandler); | |
| } | |
| promise() { | |
| return this._sigintPromise; | |
| } | |
| hadSignal() { | |
| return this._hadSignal; | |
| } | |
| disarm() { | |
| FixedNodeSIGINTHandler.off(this._sigintHandler); | |
| } | |
| }; | |
| var FixedNodeSIGINTHandler = class { | |
| static { | |
| this._handlers = []; | |
| } | |
| static { | |
| this._ignoreNextSIGINTs = false; | |
| } | |
| static { | |
| this._handlerInstalled = false; | |
| } | |
| static { | |
| this._dispatch = () => { | |
| if (this._ignoreNextSIGINTs) | |
| return; | |
| this._ignoreNextSIGINTs = true; | |
| setTimeout(() => { | |
| this._ignoreNextSIGINTs = false; | |
| if (!this._handlers.length) | |
| this._uninstall(); | |
| }, 1e3); | |
| for (const handler of this._handlers) | |
| handler(); | |
| }; | |
| } | |
| static _install() { | |
| if (!this._handlerInstalled) { | |
| this._handlerInstalled = true; | |
| process.on("SIGINT", this._dispatch); | |
| } | |
| } | |
| static _uninstall() { | |
| if (this._handlerInstalled) { | |
| this._handlerInstalled = false; | |
| process.off("SIGINT", this._dispatch); | |
| } | |
| } | |
| static on(handler) { | |
| this._handlers.push(handler); | |
| if (this._handlers.length === 1) | |
| this._install(); | |
| } | |
| static off(handler) { | |
| this._handlers = this._handlers.filter((h) => h !== handler); | |
| if (!this._ignoreNextSIGINTs && !this._handlers.length) | |
| this._uninstall(); | |
| } | |
| }; | |
| // packages/playwright/src/runner/taskRunner.ts | |
| var import_util13 = require("../util"); | |
| var colors5 = require("playwright-core/lib/utilsBundle").colors; | |
| var debug4 = require("playwright-core/lib/utilsBundle").debug; | |
| var { ManualPromise: ManualPromise4 } = require("playwright-core/lib/coreBundle").iso; | |
| var { monotonicTime: monotonicTime5 } = require("playwright-core/lib/coreBundle").iso; | |
| var TaskRunner = class _TaskRunner { | |
| constructor(reporter, globalTimeoutForError) { | |
| this._tasks = []; | |
| this._hasErrors = false; | |
| this._interrupted = false; | |
| this._isTearDown = false; | |
| this._reporter = reporter; | |
| this._globalTimeoutForError = globalTimeoutForError; | |
| } | |
| addTask(task) { | |
| this._tasks.push(task); | |
| } | |
| async run(context, deadline, cancelPromise) { | |
| const { status, cleanup } = await this.runDeferCleanup(context, deadline, cancelPromise); | |
| const teardownStatus = await cleanup(); | |
| return status === "passed" ? teardownStatus : status; | |
| } | |
| async runDeferCleanup(context, deadline, cancelPromise = new ManualPromise4()) { | |
| const sigintWatcher = new SigIntWatcher(); | |
| const timeoutWatcher = new TimeoutWatcher(deadline); | |
| const teardownRunner = new _TaskRunner(this._reporter, this._globalTimeoutForError); | |
| teardownRunner._isTearDown = true; | |
| let currentTaskName; | |
| const taskLoop = async () => { | |
| for (const task of this._tasks) { | |
| currentTaskName = task.title; | |
| if (this._interrupted) | |
| break; | |
| debug4("pw:test:task")(`"${task.title}" started`); | |
| const errors = []; | |
| const softErrors = []; | |
| try { | |
| teardownRunner._tasks.unshift({ title: `teardown for ${task.title}`, setup: task.teardown }); | |
| await task.setup?.(context, errors, softErrors); | |
| } catch (e) { | |
| debug4("pw:test:task")(`error in "${task.title}": `, e); | |
| errors.push((0, import_util13.serializeError)(e)); | |
| } finally { | |
| for (const error of [...softErrors, ...errors]) | |
| this._reporter.onError?.(error); | |
| if (errors.length) { | |
| if (!this._isTearDown) | |
| this._interrupted = true; | |
| this._hasErrors = true; | |
| } | |
| } | |
| debug4("pw:test:task")(`"${task.title}" finished`); | |
| } | |
| }; | |
| await Promise.race([ | |
| taskLoop(), | |
| cancelPromise, | |
| sigintWatcher.promise(), | |
| timeoutWatcher.promise | |
| ]); | |
| sigintWatcher.disarm(); | |
| timeoutWatcher.disarm(); | |
| this._interrupted = true; | |
| let status = "passed"; | |
| if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) { | |
| status = "interrupted"; | |
| } else if (timeoutWatcher.timedOut()) { | |
| this._reporter.onError?.({ message: colors5.red(`Timed out waiting ${this._globalTimeoutForError / 1e3}s for the ${currentTaskName} to run`) }); | |
| status = "timedout"; | |
| } else if (this._hasErrors) { | |
| status = "failed"; | |
| } | |
| cancelPromise?.resolve(); | |
| const cleanup = () => teardownRunner.runDeferCleanup(context, deadline).then((r) => r.status); | |
| return { status, cleanup }; | |
| } | |
| }; | |
| var TimeoutWatcher = class { | |
| constructor(deadline) { | |
| this._timedOut = false; | |
| this.promise = new ManualPromise4(); | |
| if (!deadline) | |
| return; | |
| if (deadline - monotonicTime5() <= 0) { | |
| this._timedOut = true; | |
| this.promise.resolve(); | |
| return; | |
| } | |
| this._timer = setTimeout(() => { | |
| this._timedOut = true; | |
| this.promise.resolve(); | |
| }, deadline - monotonicTime5()); | |
| } | |
| timedOut() { | |
| return this._timedOut; | |
| } | |
| disarm() { | |
| clearTimeout(this._timer); | |
| } | |
| }; | |
| // packages/playwright/src/runner/vcs.ts | |
| var import_child_process2 = __toESM(require("child_process")); | |
| var import_path15 = __toESM(require("path")); | |
| var import_common8 = require("../common"); | |
| async function detectChangedTestFiles(baseCommit, configDir) { | |
| function gitFileList(args) { | |
| try { | |
| return import_child_process2.default.execFileSync( | |
| "git", | |
| args, | |
| { encoding: "utf-8", stdio: "pipe", cwd: configDir } | |
| ).split("\n").filter(Boolean); | |
| } catch (_error) { | |
| const error = _error; | |
| const unknownRevision = error.output.some((line) => line?.includes("unknown revision")); | |
| if (unknownRevision) { | |
| const isShallowClone = import_child_process2.default.execFileSync("git", ["rev-parse", "--is-shallow-repository"], { encoding: "utf-8", stdio: "pipe", cwd: configDir }).trim() === "true"; | |
| if (isShallowClone) { | |
| throw new Error([ | |
| `The repository is a shallow clone and does not have '${baseCommit}' available locally.`, | |
| `Note that GitHub Actions checkout is shallow by default: https://github.com/actions/checkout` | |
| ].join("\n")); | |
| } | |
| } | |
| throw new Error([ | |
| `Cannot detect changed files for --only-changed mode:`, | |
| `git ${args.join(" ")}`, | |
| "", | |
| ...error.output | |
| ].join("\n")); | |
| } | |
| } | |
| const untrackedFiles = gitFileList(["ls-files", "--others", "--exclude-standard"]).map((file) => import_path15.default.join(configDir, file)); | |
| const [gitRoot] = gitFileList(["rev-parse", "--show-toplevel"]); | |
| const trackedFilesWithChanges = gitFileList(["diff", baseCommit, "--name-only"]).map((file) => import_path15.default.join(gitRoot, file)); | |
| return new Set(import_common8.cc.affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges])); | |
| } | |
| // packages/playwright/src/runner/tasks.ts | |
| var import_common9 = require("../common"); | |
| var import_util15 = require("../util"); | |
| var debug5 = require("playwright-core/lib/utilsBundle").debug; | |
| var { ManualPromise: ManualPromise5 } = require("playwright-core/lib/coreBundle").iso; | |
| var { monotonicTime: monotonicTime6 } = require("playwright-core/lib/coreBundle").iso; | |
| var { removeFolders: removeFolders4 } = require("playwright-core/lib/coreBundle").utils; | |
| var readDirAsync2 = (0, import_util14.promisify)(import_fs12.default.readdir); | |
| var TestRun = class { | |
| constructor(config2, reporter, options) { | |
| this.rootSuite = void 0; | |
| this.phases = []; | |
| this.projectFiles = /* @__PURE__ */ new Map(); | |
| this.projectSuites = /* @__PURE__ */ new Map(); | |
| this.topLevelProjects = []; | |
| this.hasWorkerErrors = false; | |
| this.failedTestCount = 0; | |
| this.loadFileFilters = []; | |
| this.preOnlyTestFilters = []; | |
| this.postShardTestFilters = []; | |
| this.config = config2; | |
| this.options = options ?? {}; | |
| this.reporter = reporter; | |
| this.filteredProjects = filterProjects(config2.projects, this.options.projectFilter); | |
| } | |
| onTestPaused(params) { | |
| this.options.onTestPaused?.(params); | |
| } | |
| hasReachedMaxFailures() { | |
| const max = this.config.config.maxFailures; | |
| return max > 0 && this.failedTestCount >= max; | |
| } | |
| result() { | |
| const hasFailedTests = this.rootSuite?.allTests().some((test) => !test.ok()); | |
| const hasFlakyTests = this.rootSuite?.allTests().some((test) => test.outcome() === "flaky"); | |
| return this.hasWorkerErrors || this.reporter.hasReporterErrors() || this.hasReachedMaxFailures() || hasFailedTests || this.config.config.failOnFlakyTests && hasFlakyTests ? "failed" : "passed"; | |
| } | |
| }; | |
| async function runTasks(testRun, tasks, globalTimeout, cancelPromise) { | |
| const deadline = globalTimeout ? monotonicTime6() + globalTimeout : 0; | |
| const taskRunner = new TaskRunner(testRun.reporter, globalTimeout || 0); | |
| for (const task of tasks) | |
| taskRunner.addTask(task); | |
| testRun.reporter.onConfigure(testRun.config.config); | |
| const status = await taskRunner.run(testRun, deadline, cancelPromise); | |
| return await finishTaskRun(testRun, status); | |
| } | |
| async function runTasksDeferCleanup(testRun, tasks) { | |
| const taskRunner = new TaskRunner(testRun.reporter, 0); | |
| for (const task of tasks) | |
| taskRunner.addTask(task); | |
| testRun.reporter.onConfigure(testRun.config.config); | |
| const { status, cleanup } = await taskRunner.runDeferCleanup(testRun, 0); | |
| return { status: await finishTaskRun(testRun, status), cleanup }; | |
| } | |
| async function finishTaskRun(testRun, status) { | |
| if (status === "passed") | |
| status = testRun.result(); | |
| const modifiedResult = await testRun.reporter.onEnd({ status }); | |
| if (modifiedResult && modifiedResult.status) | |
| status = modifiedResult.status; | |
| await testRun.reporter.onExit(); | |
| if (status === "passed" && testRun.reporter.hasReporterErrors()) | |
| status = "failed"; | |
| return status; | |
| } | |
| function createGlobalSetupTasks(config2) { | |
| return [ | |
| createRemoveOutputDirsTask(), | |
| ...createPluginSetupTasks(config2), | |
| ...config2.globalTeardowns.map((file) => createGlobalTeardownTask(file, config2)).reverse(), | |
| ...config2.globalSetups.map((file) => createGlobalSetupTask(file, config2)) | |
| ]; | |
| } | |
| function createRunTestsTasks(config2) { | |
| return [ | |
| createPhasesTask(), | |
| createReportBeginTask(), | |
| createRunTestsTask() | |
| ]; | |
| } | |
| function createClearCacheTask(config2) { | |
| return { | |
| title: "clear cache", | |
| setup: async () => { | |
| await (0, import_util15.removeDirAndLogToConsole)(import_common9.cc.cacheDir); | |
| } | |
| }; | |
| } | |
| function createReportBeginTask() { | |
| return { | |
| title: "report begin", | |
| setup: async (testRun) => { | |
| testRun.reporter.onBegin?.(testRun.rootSuite); | |
| }, | |
| teardown: async ({}) => { | |
| } | |
| }; | |
| } | |
| function createPluginSetupTasks(config2) { | |
| return config2.plugins.map((plugin) => ({ | |
| title: "plugin setup", | |
| setup: async ({ reporter }) => { | |
| if (typeof plugin.factory === "function") | |
| plugin.instance = await plugin.factory(); | |
| else | |
| plugin.instance = plugin.factory; | |
| await plugin.instance?.setup?.(config2.config, config2.configDir, reporter); | |
| }, | |
| teardown: async () => { | |
| await plugin.instance?.teardown?.(); | |
| } | |
| })); | |
| } | |
| function createGlobalSetupTask(file, config2) { | |
| let title = "global setup"; | |
| if (config2.globalSetups.length > 1) | |
| title += ` (${file})`; | |
| let globalSetupResult; | |
| return { | |
| title, | |
| setup: async ({ config: config3 }) => { | |
| const setupHook = await loadGlobalHook(config3, file); | |
| globalSetupResult = await setupHook(config3.config); | |
| }, | |
| teardown: async () => { | |
| if (typeof globalSetupResult === "function") | |
| await globalSetupResult(); | |
| } | |
| }; | |
| } | |
| function createGlobalTeardownTask(file, config2) { | |
| let title = "global teardown"; | |
| if (config2.globalTeardowns.length > 1) | |
| title += ` (${file})`; | |
| return { | |
| title, | |
| teardown: async ({ config: config3 }) => { | |
| const teardownHook = await loadGlobalHook(config3, file); | |
| await teardownHook(config3.config); | |
| } | |
| }; | |
| } | |
| function createRemoveOutputDirsTask() { | |
| return { | |
| title: "clear output", | |
| setup: async (testRun) => { | |
| if (testRun.options.preserveOutputDir) | |
| return; | |
| const outputDirs = /* @__PURE__ */ new Set(); | |
| testRun.filteredProjects.forEach((p) => outputDirs.add(p.project.outputDir)); | |
| await Promise.all(Array.from(outputDirs).map((outputDir) => removeFolders4([outputDir]).then(async ([error]) => { | |
| if (!error) | |
| return; | |
| if (error.code === "EBUSY") { | |
| const entries = await readDirAsync2(outputDir).catch((e) => []); | |
| await Promise.all(entries.map((entry) => removeFolders4([import_path16.default.join(outputDir, entry)]))); | |
| } else { | |
| throw error; | |
| } | |
| }))); | |
| } | |
| }; | |
| } | |
| function createListFilesTask() { | |
| return { | |
| title: "load tests", | |
| setup: async (testRun, errors) => { | |
| await createRootSuite(testRun, errors, false); | |
| await collectProjectsAndTestFiles(testRun, false); | |
| for (const [project, files] of testRun.projectFiles) { | |
| const projectSuite = new import_common9.test.Suite(project.project.name, "project"); | |
| projectSuite._fullProject = project; | |
| testRun.rootSuite._addSuite(projectSuite); | |
| const suites = files.map((file) => { | |
| const title = import_path16.default.relative(testRun.config.config.rootDir, file); | |
| const suite = new import_common9.test.Suite(title, "file"); | |
| suite.location = { file, line: 0, column: 0 }; | |
| projectSuite._addSuite(suite); | |
| return suite; | |
| }); | |
| testRun.projectSuites.set(project, suites); | |
| } | |
| } | |
| }; | |
| } | |
| function createLoadTask(mode, options) { | |
| return { | |
| title: "load tests", | |
| setup: async (testRun, errors, softErrors) => { | |
| if (testRun.options.locations?.length) { | |
| const { testFilter, fileFilter } = import_common9.suiteUtils.createFiltersFromArguments(testRun.options.locations); | |
| testRun.loadFileFilters.push(fileFilter); | |
| testRun.preOnlyTestFilters.push(testFilter); | |
| } | |
| if (testRun.options.testList) { | |
| const { testFilter, fileFilter } = await loadTestList(testRun.config, testRun.options.testList); | |
| testRun.preOnlyTestFilters.push(testFilter); | |
| testRun.loadFileFilters.push(fileFilter); | |
| } | |
| if (testRun.options.testListInvert) { | |
| const { testFilter } = await loadTestList(testRun.config, testRun.options.testListInvert); | |
| testRun.preOnlyTestFilters.push((test) => !testFilter(test)); | |
| } | |
| if (testRun.options.grep || testRun.options.grepInvert) { | |
| const grepMatcher = testRun.options.grep ? (0, import_util15.createTitleMatcher)((0, import_util15.forceRegExp)(testRun.options.grep)) : () => true; | |
| const grepInvertMatcher = testRun.options.grepInvert ? (0, import_util15.createTitleMatcher)((0, import_util15.forceRegExp)(testRun.options.grepInvert)) : () => false; | |
| testRun.preOnlyTestFilters.push((test) => { | |
| const grepTitle = test._grepTitleWithTags(); | |
| return !grepInvertMatcher(grepTitle) && grepMatcher(grepTitle); | |
| }); | |
| } | |
| if (testRun.options.lastFailedTestIds) { | |
| const failedTestIds = new Set(testRun.options.lastFailedTestIds); | |
| testRun.postShardTestFilters.push((test) => failedTestIds.has(test.id)); | |
| } | |
| await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter); | |
| await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); | |
| if (testRun.options.onlyChanged) { | |
| const changedFiles = await detectChangedTestFiles(testRun.options.onlyChanged, testRun.config.configDir); | |
| testRun.preOnlyTestFilters.push((test) => changedFiles.has(test.location.file)); | |
| } | |
| await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); | |
| if (options.failOnLoadErrors && !testRun.rootSuite?.allTests().length && !testRun.options.passWithNoTests && !testRun.config.config.shard && !testRun.options.onlyChanged && !testRun.options.testList && !testRun.options.testListInvert) { | |
| if (testRun.options.locations?.length) { | |
| throw new Error([ | |
| `No tests found.`, | |
| `Make sure that arguments are regular expressions matching test files.`, | |
| `You may need to escape symbols like "$" or "*" and quote the arguments.` | |
| ].join("\n")); | |
| } | |
| throw new Error(`No tests found`); | |
| } | |
| } | |
| }; | |
| } | |
| function createApplyRebaselinesTask() { | |
| return { | |
| title: "apply rebaselines", | |
| setup: async () => { | |
| clearSuggestedRebaselines(); | |
| }, | |
| teardown: async (testRun) => { | |
| await applySuggestedRebaselines(testRun.config, testRun.reporter, testRun.filteredProjects); | |
| } | |
| }; | |
| } | |
| function createPhasesTask() { | |
| return { | |
| title: "create phases", | |
| setup: async (testRun) => { | |
| let maxConcurrentTestGroups = 0; | |
| const processed = /* @__PURE__ */ new Set(); | |
| const projectToSuite = new Map(testRun.rootSuite.suites.map((suite) => [suite._fullProject, suite])); | |
| const allProjects = [...projectToSuite.keys()]; | |
| const teardownToSetups = buildTeardownToSetupsMap(allProjects); | |
| const ignoreMaxFailuresProjectIds = new Set(testRun.config.config.maxFailures > 0 ? [...teardownToSetups.keys()].map((project) => project.id) : []); | |
| const teardownToSetupsDependents = /* @__PURE__ */ new Map(); | |
| for (const [teardown, setups] of teardownToSetups) { | |
| const closure = buildDependentProjects(setups, allProjects); | |
| closure.delete(teardown); | |
| teardownToSetupsDependents.set(teardown, [...closure]); | |
| } | |
| for (let i = 0; i < projectToSuite.size; i++) { | |
| const phaseProjects = []; | |
| for (const project of projectToSuite.keys()) { | |
| if (processed.has(project)) | |
| continue; | |
| const projectsThatShouldFinishFirst = [...project.deps, ...teardownToSetupsDependents.get(project) || []]; | |
| if (projectsThatShouldFinishFirst.find((p) => !processed.has(p))) | |
| continue; | |
| phaseProjects.push(project); | |
| } | |
| for (const project of phaseProjects) | |
| processed.add(project); | |
| for (const ignoreMaxFailures of [false, true]) { | |
| const projects = phaseProjects.filter((project) => ignoreMaxFailuresProjectIds.has(project.id) === ignoreMaxFailures); | |
| if (!projects.length) | |
| continue; | |
| let testGroupsInPhase = 0; | |
| const phase = { dispatcher: new Dispatcher(testRun, { ignoreMaxFailures }), projects: [] }; | |
| testRun.phases.push(phase); | |
| for (const project of projects) { | |
| const projectSuite = projectToSuite.get(project); | |
| const testGroups = createTestGroups(projectSuite, testRun.config.config.workers); | |
| phase.projects.push({ project, projectSuite, testGroups }); | |
| testGroupsInPhase += Math.min(project.workers ?? Number.MAX_SAFE_INTEGER, testGroups.length); | |
| } | |
| debug5("pw:test:task")(`created phase #${testRun.phases.length} with ${phase.projects.map((p) => p.project.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); | |
| maxConcurrentTestGroups = Math.max(maxConcurrentTestGroups, testGroupsInPhase); | |
| } | |
| } | |
| testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups); | |
| } | |
| }; | |
| } | |
| function createRunTestsTask() { | |
| return { | |
| title: "test suite", | |
| setup: async (testRun) => { | |
| const successfulProjects = /* @__PURE__ */ new Set(); | |
| const extraEnvByProjectId = /* @__PURE__ */ new Map(); | |
| const teardownToSetups = buildTeardownToSetupsMap(testRun.phases.map((phase) => phase.projects.map((p) => p.project)).flat()); | |
| for (const { dispatcher, projects } of testRun.phases) { | |
| const phaseTestGroups = []; | |
| for (const { project, testGroups } of projects) { | |
| let extraEnv = {}; | |
| for (const dep of project.deps) | |
| extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) }; | |
| for (const setup of teardownToSetups.get(project) || []) | |
| extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setup.id) }; | |
| extraEnvByProjectId.set(project.id, extraEnv); | |
| const hasFailedDeps = project.deps.some((p) => !successfulProjects.has(p)); | |
| if (!hasFailedDeps) | |
| phaseTestGroups.push(...testGroups); | |
| } | |
| if (phaseTestGroups.length) { | |
| await dispatcher.run(phaseTestGroups, extraEnvByProjectId); | |
| await dispatcher.stop(); | |
| for (const [projectId, envProduced] of dispatcher.producedEnvByProjectId()) { | |
| const extraEnv = extraEnvByProjectId.get(projectId) || {}; | |
| extraEnvByProjectId.set(projectId, { ...extraEnv, ...envProduced }); | |
| } | |
| } | |
| if (!testRun.hasWorkerErrors) { | |
| for (const { project, projectSuite } of projects) { | |
| const hasFailedDeps = project.deps.some((p) => !successfulProjects.has(p)); | |
| if (!hasFailedDeps && !projectSuite.allTests().some((test) => !test.ok())) | |
| successfulProjects.add(project); | |
| } | |
| } | |
| } | |
| }, | |
| teardown: async ({ phases }) => { | |
| for (const { dispatcher } of phases.reverse()) | |
| await dispatcher.stop(); | |
| } | |
| }; | |
| } | |
| // packages/playwright/src/runner/lastRun.ts | |
| var import_fs13 = __toESM(require("fs")); | |
| var import_path17 = __toESM(require("path")); | |
| function didNotRun(test) { | |
| if (test.outcome() !== "skipped") | |
| return false; | |
| if (test.results.some((result) => result.status === "interrupted")) | |
| return false; | |
| return !test.results.length || test.expectedStatus !== "skipped"; | |
| } | |
| var LastRunReporter = class { | |
| constructor(filteredProjects, listMode, lastFailedFileOverride) { | |
| this._listMode = !!listMode; | |
| const override = lastFailedFileOverride ?? process.env.PLAYWRIGHT_LAST_RUN_OUTPUT_FILE; | |
| if (override) { | |
| this._lastRunFile = import_path17.default.resolve(process.cwd(), override); | |
| } else { | |
| const [project] = filteredProjects; | |
| if (project) | |
| this._lastRunFile = import_path17.default.join(project.project.outputDir, ".last-run.json"); | |
| } | |
| } | |
| async filterLastFailed() { | |
| if (!this._lastRunFile) | |
| return void 0; | |
| try { | |
| const lastRunInfo = JSON.parse(await import_fs13.default.promises.readFile(this._lastRunFile, "utf8")); | |
| return lastRunInfo.failedTests; | |
| } catch { | |
| return void 0; | |
| } | |
| } | |
| version() { | |
| return "v2"; | |
| } | |
| printsToStdio() { | |
| return false; | |
| } | |
| onBegin(suite) { | |
| this._suite = suite; | |
| } | |
| async onEnd(result) { | |
| if (!this._lastRunFile || this._listMode) | |
| return; | |
| const lastRunInfo = { | |
| status: result.status, | |
| failedTests: this._suite?.allTests().filter((t2) => !t2.ok() || didNotRun(t2)).map((t2) => t2.id) || [] | |
| }; | |
| await import_fs13.default.promises.mkdir(import_path17.default.dirname(this._lastRunFile), { recursive: true }); | |
| await import_fs13.default.promises.writeFile(this._lastRunFile, JSON.stringify(lastRunInfo, void 0, 2)); | |
| } | |
| }; | |
| // packages/playwright/src/runner/testRunner.ts | |
| var { ManualPromise: ManualPromise6 } = require("playwright-core/lib/coreBundle").iso; | |
| var { setPlaywrightTestProcessEnv } = require("playwright-core/lib/coreBundle").utils; | |
| var { gracefullyProcessExitDoNotHang: gracefullyProcessExitDoNotHang2 } = require("playwright-core/lib/coreBundle").utils; | |
| var TestRunnerEvent = { | |
| TestFilesChanged: "testFilesChanged", | |
| TestPaused: "testPaused" | |
| }; | |
| var TestRunner = class extends import_events2.default { | |
| constructor(configLocation, configCLIOverrides) { | |
| super(); | |
| this._watchedProjectDirs = /* @__PURE__ */ new Set(); | |
| this._ignoredProjectOutputs = /* @__PURE__ */ new Set(); | |
| this._watchedTestDependencies = /* @__PURE__ */ new Set(); | |
| this._queue = Promise.resolve(); | |
| this._watchTestDirs = false; | |
| this._startingEnv = {}; | |
| this.configLocation = configLocation; | |
| this._configCLIOverrides = configCLIOverrides; | |
| this._watcher = new FSWatcher((events) => { | |
| const collector = /* @__PURE__ */ new Set(); | |
| events.forEach((f) => import_common10.cc.collectAffectedTestFiles(f.file, collector)); | |
| this.emit(TestRunnerEvent.TestFilesChanged, [...collector]); | |
| }); | |
| } | |
| async initialize(params) { | |
| setPlaywrightTestProcessEnv(); | |
| this._watchTestDirs = !!params.watchTestDirs; | |
| this._startingEnv = { ...process.env }; | |
| } | |
| resizeTerminal(params) { | |
| process.stdout.columns = params.cols; | |
| process.stdout.rows = params.rows; | |
| process.stderr.columns = params.cols; | |
| process.stderr.rows = params.rows; | |
| } | |
| hasSomeBrowsers() { | |
| for (const browserName of ["chromium", "webkit", "firefox"]) { | |
| try { | |
| import_coreBundle3.registry.registry.findExecutable(browserName).executablePathOrDie("javascript"); | |
| return true; | |
| } catch { | |
| } | |
| } | |
| return false; | |
| } | |
| async installBrowsers() { | |
| const executables = import_coreBundle3.registry.registry.defaultExecutables(); | |
| await import_coreBundle3.registry.registry.install(executables); | |
| } | |
| async loadConfig() { | |
| const { config: config2, error } = await this._loadConfig(this._configCLIOverrides); | |
| if (config2) | |
| return config2; | |
| throw new Error("Failed to load config: " + (error ? error.message : "Unknown error")); | |
| } | |
| async runGlobalSetup(userReporters) { | |
| await this.runGlobalTeardown(); | |
| const reporter = new InternalReporter(userReporters); | |
| const config2 = await this._loadConfigOrReportError(reporter, this._configCLIOverrides); | |
| if (!config2) | |
| return { status: "failed", env: [] }; | |
| const { status, cleanup } = await runTasksDeferCleanup(new TestRun(config2, reporter), [ | |
| ...createGlobalSetupTasks(config2) | |
| ]); | |
| const env = []; | |
| for (const key of /* @__PURE__ */ new Set([...Object.keys(process.env), ...Object.keys(this._startingEnv)])) { | |
| if (this._startingEnv[key] !== process.env[key]) | |
| env.push([key, process.env[key] ?? null]); | |
| } | |
| if (status !== "passed") | |
| await cleanup(); | |
| else | |
| this._globalSetup = { cleanup }; | |
| return { status, env }; | |
| } | |
| async runGlobalTeardown() { | |
| const globalSetup = this._globalSetup; | |
| const status = await globalSetup?.cleanup(); | |
| this._globalSetup = void 0; | |
| return { status }; | |
| } | |
| async clearCache(userReporter) { | |
| const reporter = new InternalReporter(userReporter ? [userReporter] : []); | |
| const config2 = await this._loadConfigOrReportError(reporter); | |
| if (!config2) | |
| return { status: "failed" }; | |
| const status = await runTasks(new TestRun(config2, reporter), [ | |
| createClearCacheTask(config2) | |
| ]); | |
| return { status }; | |
| } | |
| async listFiles(userReporter, projects) { | |
| const reporter = new InternalReporter([userReporter]); | |
| const config2 = await this._loadConfigOrReportError(reporter); | |
| if (!config2) | |
| return { status: "failed" }; | |
| const options = { projectFilter: projects?.length ? projects : void 0 }; | |
| const status = await runTasks(new TestRun(config2, reporter, options), [ | |
| createListFilesTask(), | |
| createReportBeginTask() | |
| ]); | |
| return { status }; | |
| } | |
| async listTests(userReporter, params) { | |
| let result; | |
| this._queue = this._queue.then(async () => { | |
| const { config: config2, status } = await this._innerListTests(userReporter, params); | |
| if (config2) | |
| await this._updateWatchedDirs(config2); | |
| result = { status }; | |
| }).catch(printInternalError); | |
| await this._queue; | |
| return result; | |
| } | |
| async _innerListTests(userReporter, params) { | |
| const overrides = { | |
| ...this._configCLIOverrides, | |
| repeatEach: 1, | |
| retries: 0 | |
| }; | |
| const reporter = new InternalReporter([userReporter]); | |
| const config2 = await this._loadConfigOrReportError(reporter, overrides); | |
| if (!config2) | |
| return { status: "failed" }; | |
| const options = { | |
| locations: params.locations?.length ? params.locations : void 0, | |
| grep: params.grep, | |
| grepInvert: params.grepInvert, | |
| projectFilter: params.projects?.length ? params.projects : void 0, | |
| onlyChanged: params.onlyChanged ? "HEAD" : void 0, | |
| listMode: true | |
| }; | |
| const status = await runTasks(new TestRun(config2, reporter, options), [ | |
| createLoadTask("out-of-process", { failOnLoadErrors: false, filterOnly: false }), | |
| createReportBeginTask() | |
| ]); | |
| return { config: config2, status }; | |
| } | |
| async _updateWatchedDirs(config2) { | |
| this._watchedProjectDirs = /* @__PURE__ */ new Set(); | |
| this._ignoredProjectOutputs = /* @__PURE__ */ new Set(); | |
| for (const p of config2.projects) { | |
| this._watchedProjectDirs.add(p.project.testDir); | |
| this._ignoredProjectOutputs.add(p.project.outputDir); | |
| } | |
| if (this._watchTestDirs) | |
| await this._updateWatcher(false); | |
| } | |
| async _updateWatcher(reportPending) { | |
| await this._watcher.update([...this._watchedProjectDirs, ...this._watchedTestDependencies], [...this._ignoredProjectOutputs], reportPending); | |
| } | |
| async runTests(userReporter, params) { | |
| let result = { status: "passed" }; | |
| this._queue = this._queue.then(async () => { | |
| result = await this._innerRunTests(userReporter, params).catch((e) => { | |
| printInternalError(e); | |
| return { status: "failed" }; | |
| }); | |
| }); | |
| await this._queue; | |
| return result; | |
| } | |
| async _innerRunTests(userReporter, params) { | |
| await this.stopTests(); | |
| const overrides = { | |
| ...this._configCLIOverrides, | |
| repeatEach: 1, | |
| retries: 0, | |
| timeout: params.timeout, | |
| reporter: params.reporters ? params.reporters.map((r) => [r]) : void 0, | |
| use: { | |
| ...this._configCLIOverrides.use, | |
| ...params.trace === "on" ? { trace: { mode: "on", sources: false, live: true } } : {}, | |
| ...params.trace === "off" ? { trace: "off" } : {}, | |
| ...params.video === "on" || params.video === "off" ? { video: params.video } : {}, | |
| ...params.headed !== void 0 ? { headless: !params.headed } : {}, | |
| _optionContextReuseMode: params.reuseContext ? "when-possible" : void 0, | |
| _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : void 0, | |
| actionTimeout: params.actionTimeout | |
| }, | |
| ...params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}, | |
| ...params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}, | |
| ...params.workers ? { workers: params.workers } : {}, | |
| ...params.maxFailures ? { maxFailures: params.maxFailures } : {} | |
| }; | |
| const config2 = await this._loadConfigOrReportError(new InternalReporter([userReporter]), overrides); | |
| if (!config2) | |
| return { status: "failed" }; | |
| const options = { | |
| passWithNoTests: true, | |
| locations: params.locations?.length ? params.locations : void 0, | |
| grep: params.grep, | |
| grepInvert: params.grepInvert, | |
| projectFilter: params.projects?.length ? params.projects : void 0, | |
| pauseOnError: params.pauseOnError, | |
| pauseAtEnd: params.pauseAtEnd, | |
| preserveOutputDir: true, | |
| onTestPaused: (params2) => this.emit(TestRunnerEvent.TestPaused, params2) | |
| }; | |
| const configReporters = params.disableConfigReporters ? [] : await createReporters(config2, "test", void 0, options); | |
| const reporter = new InternalReporter([...configReporters, userReporter]); | |
| const stop = new ManualPromise6(); | |
| const testRun = new TestRun(config2, reporter, options); | |
| if (params.testIds) { | |
| const testIdSet = new Set(params.testIds); | |
| testRun.preOnlyTestFilters.push((test) => testIdSet.has(test.id)); | |
| } | |
| const tasks = [ | |
| createApplyRebaselinesTask(), | |
| createLoadTask("out-of-process", { filterOnly: true, failOnLoadErrors: !!params.failOnLoadErrors, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }), | |
| ...createRunTestsTasks(config2) | |
| ]; | |
| const run = runTasks(testRun, tasks, 0, stop).then(async (status) => { | |
| this._testRun = void 0; | |
| return status; | |
| }); | |
| this._testRun = { run, stop }; | |
| return { status: await run }; | |
| } | |
| async watch(fileNames) { | |
| this._watchedTestDependencies = /* @__PURE__ */ new Set(); | |
| for (const fileName of fileNames) { | |
| this._watchedTestDependencies.add(fileName); | |
| import_common10.cc.dependenciesForTestFile(fileName).forEach((file) => this._watchedTestDependencies.add(file)); | |
| } | |
| await this._updateWatcher(true); | |
| } | |
| async findRelatedTestFiles(files, userReporter) { | |
| const errorReporter = createErrorCollectingReporter(internalScreen); | |
| const reporter = new InternalReporter(userReporter ? [userReporter, errorReporter] : [errorReporter]); | |
| const config2 = await this._loadConfigOrReportError(reporter); | |
| if (!config2) | |
| return { errors: errorReporter.errors(), testFiles: [] }; | |
| const status = await runTasks(new TestRun(config2, reporter), [ | |
| ...createPluginSetupTasks(config2), | |
| createLoadTask("out-of-process", { failOnLoadErrors: true, filterOnly: false }) | |
| ]); | |
| if (status !== "passed") | |
| return { errors: errorReporter.errors(), testFiles: [] }; | |
| return { testFiles: import_common10.cc.affectedTestFiles(files) }; | |
| } | |
| async stopTests() { | |
| this._testRun?.stop?.resolve(); | |
| await this._testRun?.run; | |
| } | |
| async closeGracefully() { | |
| gracefullyProcessExitDoNotHang2(0); | |
| } | |
| async stop() { | |
| await this.runGlobalTeardown(); | |
| } | |
| async _loadConfig(overrides) { | |
| try { | |
| const config2 = await import_common10.configLoader.loadConfig(this.configLocation, overrides); | |
| if (!this._plugins) { | |
| webServerPluginsForConfig(config2).forEach((p) => config2.plugins.push({ factory: p })); | |
| addGitCommitInfoPlugin(config2); | |
| this._plugins = config2.plugins || []; | |
| } else { | |
| config2.plugins.splice(0, config2.plugins.length, ...this._plugins); | |
| } | |
| this._lastLoadedConfig = config2; | |
| return { config: config2 }; | |
| } catch (e) { | |
| return { config: null, error: (0, import_util16.serializeError)(e) }; | |
| } | |
| } | |
| lastLoadedConfig() { | |
| return this._lastLoadedConfig; | |
| } | |
| async _loadConfigOrReportError(reporter, overrides) { | |
| const { config: config2, error } = await this._loadConfig(overrides); | |
| if (config2) | |
| return config2; | |
| reporter.onConfigure(baseFullConfig); | |
| reporter.onError(error); | |
| await reporter.onEnd({ status: "failed" }); | |
| await reporter.onExit(); | |
| return null; | |
| } | |
| }; | |
| function printInternalError(e) { | |
| console.error("Internal error:", e); | |
| } | |
| async function runAllTestsWithConfig(config2, options) { | |
| setPlaywrightTestProcessEnv(); | |
| addGitCommitInfoPlugin(config2); | |
| webServerPluginsForConfig(config2).forEach((p) => config2.plugins.push({ factory: p })); | |
| const filteredProjects = filterProjects(config2.projects, options.projectFilter); | |
| const reporters = await createReporters(config2, options.listMode ? "list" : "test", void 0, options); | |
| const lastRun = new LastRunReporter(filteredProjects, options.listMode, options.lastFailedFile); | |
| if (options.lastFailed) { | |
| const lastFailedTestIds = await lastRun.filterLastFailed(); | |
| if (lastFailedTestIds) | |
| options = { ...options, lastFailedTestIds }; | |
| } | |
| const reporter = new InternalReporter([...reporters, lastRun]); | |
| const tasks = options.listMode ? [ | |
| createLoadTask("in-process", { failOnLoadErrors: true, filterOnly: false }), | |
| createReportBeginTask() | |
| ] : [ | |
| createApplyRebaselinesTask(), | |
| ...createGlobalSetupTasks(config2), | |
| createLoadTask("in-process", { filterOnly: true, failOnLoadErrors: true }), | |
| ...createRunTestsTasks(config2) | |
| ]; | |
| const testRun = new TestRun(config2, reporter, { ...options, pauseAtEnd: config2.configCLIOverrides.pause, pauseOnError: config2.configCLIOverrides.pause }); | |
| const status = await runTasks(testRun, tasks, config2.config.globalTimeout); | |
| await new Promise((resolve) => process.stdout.write("", () => resolve())); | |
| await new Promise((resolve) => process.stderr.write("", () => resolve())); | |
| return status; | |
| } | |
| // packages/playwright/src/runner/testServer.ts | |
| var testServer_exports = {}; | |
| __export(testServer_exports, { | |
| TestServerDispatcher: () => TestServerDispatcher, | |
| runTestServer: () => runTestServer, | |
| runUIMode: () => runUIMode | |
| }); | |
| var import_util17 = __toESM(require("util")); | |
| var import_coreBundle4 = require("playwright-core/lib/coreBundle"); | |
| var import_common11 = require("../common"); | |
| // packages/playwright/src/runner/uiModeReporter.ts | |
| var UIModeReporter = class extends TeleReporterEmitter { | |
| constructor(options) { | |
| super(options._send, { omitBuffers: true }); | |
| } | |
| }; | |
| var uiModeReporter_default = UIModeReporter; | |
| // packages/playwright/src/runner/testServer.ts | |
| var debug6 = require("playwright-core/lib/utilsBundle").debug; | |
| var open2 = require("playwright-core/lib/utilsBundle").open; | |
| var { ManualPromise: ManualPromise7 } = require("playwright-core/lib/coreBundle").iso; | |
| var { isUnderTest } = require("playwright-core/lib/coreBundle").utils; | |
| var { HttpServer: HttpServer2 } = require("playwright-core/lib/coreBundle").utils; | |
| var { gracefullyProcessExitDoNotHang: gracefullyProcessExitDoNotHang3 } = require("playwright-core/lib/coreBundle").utils; | |
| var originalDebugLog = debug6.log; | |
| var originalStdoutWrite = process.stdout.write; | |
| var originalStderrWrite = process.stderr.write; | |
| var originalStdinIsTTY = process.stdin.isTTY; | |
| var TestServer = class { | |
| constructor(configLocation, configCLIOverrides) { | |
| this._configLocation = configLocation; | |
| this._configCLIOverrides = configCLIOverrides; | |
| } | |
| async start(options) { | |
| this._dispatcher = new TestServerDispatcher(this._configLocation, this._configCLIOverrides); | |
| return await import_coreBundle4.server.startTraceViewerServer({ | |
| host: options.host, | |
| port: options.port, | |
| allowedFileRoots: () => this._allowedFileRoots(), | |
| transport: this._dispatcher.transport | |
| }); | |
| } | |
| _allowedFileRoots() { | |
| const roots = /* @__PURE__ */ new Set([process.cwd(), this._configLocation.configDir]); | |
| const config2 = this._dispatcher?._testRunner.lastLoadedConfig(); | |
| if (config2) { | |
| for (const project of config2.projects) { | |
| roots.add(project.project.outputDir); | |
| roots.add(project.project.testDir); | |
| } | |
| } | |
| return [...roots]; | |
| } | |
| async stop() { | |
| await this._dispatcher?.stop(); | |
| } | |
| }; | |
| var TestServerDispatcher = class { | |
| constructor(configLocation, configCLIOverrides) { | |
| this._closeOnDisconnect = false; | |
| this._testRunner = new TestRunner(configLocation, configCLIOverrides); | |
| this.transport = { | |
| onconnect: () => { | |
| }, | |
| dispatch: (method, params) => this[method](params), | |
| onclose: () => { | |
| if (this._closeOnDisconnect) | |
| gracefullyProcessExitDoNotHang3(0); | |
| } | |
| }; | |
| this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); | |
| this._testRunner.on(TestRunnerEvent.TestFilesChanged, (testFiles) => this._dispatchEvent("testFilesChanged", { testFiles })); | |
| this._testRunner.on(TestRunnerEvent.TestPaused, (params) => this._dispatchEvent("testPaused", { errors: params.errors })); | |
| } | |
| async _wireReporter(messageSink) { | |
| return await createReporterForTestServer(this._serializer, messageSink); | |
| } | |
| async _collectingReporter() { | |
| const report = []; | |
| return { | |
| reporter: await createReporterForTestServer(this._serializer, (e) => report.push(e)), | |
| report | |
| }; | |
| } | |
| async initialize(params) { | |
| this._serializer = params.serializer; | |
| this._closeOnDisconnect = !!params.closeOnDisconnect; | |
| await this._testRunner.initialize({ | |
| ...params | |
| }); | |
| this._setInterceptStdio(!!params.interceptStdio); | |
| } | |
| async ping() { | |
| } | |
| async open(params) { | |
| if (isUnderTest()) | |
| return; | |
| open2("vscode://file/" + params.location.file + ":" + params.location.line).catch((e) => console.error(e)); | |
| } | |
| async resizeTerminal(params) { | |
| this._testRunner.resizeTerminal(params); | |
| } | |
| async checkBrowsers() { | |
| return { hasBrowsers: this._testRunner.hasSomeBrowsers() }; | |
| } | |
| async installBrowsers() { | |
| await this._testRunner.installBrowsers(); | |
| } | |
| async runGlobalSetup(params) { | |
| const { reporter, report } = await this._collectingReporter(); | |
| this._globalSetupReport = report; | |
| const { status, env } = await this._testRunner.runGlobalSetup([reporter, new list_default()]); | |
| return { report, status, env }; | |
| } | |
| async runGlobalTeardown() { | |
| const { status } = await this._testRunner.runGlobalTeardown(); | |
| const report = this._globalSetupReport || []; | |
| this._globalSetupReport = void 0; | |
| return { status, report }; | |
| } | |
| async clearCache(params) { | |
| await this._testRunner.clearCache(); | |
| } | |
| async listFiles(params) { | |
| const { reporter, report } = await this._collectingReporter(); | |
| const { status } = await this._testRunner.listFiles(reporter, params.projects); | |
| return { report, status }; | |
| } | |
| async listTests(params) { | |
| const { reporter, report } = await this._collectingReporter(); | |
| const { status } = await this._testRunner.listTests(reporter, params); | |
| return { report, status }; | |
| } | |
| async runTests(params) { | |
| const wireReporter = await this._wireReporter((e) => this._dispatchEvent("report", e)); | |
| const { status } = await this._testRunner.runTests(wireReporter, { | |
| ...params, | |
| doNotRunDepsOutsideProjectFilter: true, | |
| pauseAtEnd: params.pauseAtEnd, | |
| pauseOnError: params.pauseOnError | |
| }); | |
| return { status }; | |
| } | |
| async watch(params) { | |
| await this._testRunner.watch(params.fileNames); | |
| } | |
| async findRelatedTestFiles(params) { | |
| return this._testRunner.findRelatedTestFiles(params.files); | |
| } | |
| async stopTests() { | |
| await this._testRunner.stopTests(); | |
| } | |
| async stop() { | |
| this._setInterceptStdio(false); | |
| await this._testRunner.stop(); | |
| } | |
| async closeGracefully() { | |
| await this._testRunner.closeGracefully(); | |
| } | |
| _setInterceptStdio(interceptStdio) { | |
| if (process.env.PWTEST_DEBUG) | |
| return; | |
| if (interceptStdio) { | |
| if (debug6.log === originalDebugLog) { | |
| debug6.log = (...args) => { | |
| const string = import_util17.default.format(...args) + "\n"; | |
| return originalStderrWrite.apply(process.stderr, [string]); | |
| }; | |
| } | |
| const stdoutWrite = (chunk) => { | |
| this._dispatchEvent("stdio", chunkToPayload("stdout", chunk)); | |
| return true; | |
| }; | |
| const stderrWrite = (chunk) => { | |
| this._dispatchEvent("stdio", chunkToPayload("stderr", chunk)); | |
| return true; | |
| }; | |
| process.stdout.write = stdoutWrite; | |
| process.stderr.write = stderrWrite; | |
| process.stdin.isTTY = void 0; | |
| } else { | |
| debug6.log = originalDebugLog; | |
| process.stdout.write = originalStdoutWrite; | |
| process.stderr.write = originalStderrWrite; | |
| process.stdin.isTTY = originalStdinIsTTY; | |
| } | |
| } | |
| }; | |
| async function runUIMode(configFile, configCLIOverrides, options) { | |
| const configLocation = import_common11.configLoader.resolveConfigLocation(configFile); | |
| return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server, cancelPromise) => { | |
| await import_coreBundle4.server.installRootRedirect(server, void 0, { ...options, webApp: "uiMode.html" }); | |
| if (options.host !== void 0 || options.port !== void 0) { | |
| await import_coreBundle4.server.openTraceInBrowser(server.urlPrefix("human-readable")); | |
| } else { | |
| const channel = await installedChromiumChannelForUI(configLocation, configCLIOverrides); | |
| const page = await import_coreBundle4.server.openTraceViewerApp(server.urlPrefix("precise"), "chromium", { | |
| headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== "1", | |
| persistentContextOptions: { | |
| handleSIGINT: false, | |
| channel | |
| } | |
| }); | |
| page.on("close", () => cancelPromise.resolve()); | |
| } | |
| }); | |
| } | |
| async function installedChromiumChannelForUI(configLocation, configCLIOverrides) { | |
| const config2 = await import_common11.configLoader.loadConfig(configLocation, configCLIOverrides).catch((e) => null); | |
| if (!config2) | |
| return void 0; | |
| if (config2.projects.some((p) => (!p.project.use.browserName || p.project.use.browserName === "chromium") && !p.project.use.channel)) | |
| return void 0; | |
| for (const channel of ["chromium", "chrome", "msedge"]) { | |
| if (config2.projects.some((p) => p.project.use.channel === channel)) | |
| return channel; | |
| } | |
| return void 0; | |
| } | |
| async function runTestServer(configFile, configCLIOverrides, options) { | |
| const configLocation = import_common11.configLoader.resolveConfigLocation(configFile); | |
| return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server) => { | |
| console.log("Listening on " + server.urlPrefix("precise").replace("http:", "ws:") + "/" + server.wsGuid()); | |
| }); | |
| } | |
| async function innerRunTestServer(configLocation, configCLIOverrides, options, openUI) { | |
| const testServer = new TestServer(configLocation, configCLIOverrides); | |
| const cancelPromise = new ManualPromise7(); | |
| const sigintWatcher = new SigIntWatcher(); | |
| process.stdin.on("close", () => gracefullyProcessExitDoNotHang3(0)); | |
| void sigintWatcher.promise().then(() => cancelPromise.resolve()); | |
| try { | |
| const server = await testServer.start(options); | |
| await openUI(server, cancelPromise); | |
| await cancelPromise; | |
| } finally { | |
| await testServer.stop(); | |
| sigintWatcher.disarm(); | |
| } | |
| return sigintWatcher.hadSignal() ? "interrupted" : "passed"; | |
| } | |
| function chunkToPayload(type, chunk) { | |
| if (chunk instanceof Uint8Array) | |
| return { type, buffer: chunk.toString("base64") }; | |
| return { type, text: chunk }; | |
| } | |
| async function createReporterForTestServer(file, messageSink) { | |
| const reporterConstructor = file ? await loadReporter(null, file) : uiModeReporter_default; | |
| return wrapReporterAsV2(new reporterConstructor({ | |
| _send: messageSink | |
| })); | |
| } | |
| // packages/playwright/src/runner/watchMode.ts | |
| var watchMode_exports = {}; | |
| __export(watchMode_exports, { | |
| runWatchModeLoop: () => runWatchModeLoop | |
| }); | |
| var import_path18 = __toESM(require("path")); | |
| var import_readline = __toESM(require("readline")); | |
| var import_events3 = require("events"); | |
| var import_coreBundle5 = require("playwright-core/lib/coreBundle"); | |
| // packages/playwright/src/isomorphic/testTree.ts | |
| var statusEx = /* @__PURE__ */ Symbol("statusEx"); | |
| // packages/playwright/src/isomorphic/teleSuiteUpdater.ts | |
| var TeleSuiteUpdater = class { | |
| constructor(options) { | |
| this.loadErrors = []; | |
| this.progress = { | |
| total: 0, | |
| passed: 0, | |
| failed: 0, | |
| skipped: 0 | |
| }; | |
| this._lastRunTestCount = 0; | |
| this._receiver = new TeleReporterReceiver(this._createReporter(), { | |
| mergeProjects: true, | |
| mergeTestCases: true, | |
| resolvePath: createPathResolve(options.pathSeparator), | |
| clearPreviousResultsWhenTestBegins: true | |
| }); | |
| this._options = options; | |
| } | |
| _createReporter() { | |
| return { | |
| version: () => "v2", | |
| onConfigure: (config2) => { | |
| this.config = config2; | |
| this._lastRunReceiver = new TeleReporterReceiver({ | |
| version: () => "v2", | |
| onBegin: (suite) => { | |
| this._lastRunTestCount = suite.allTests().length; | |
| this._lastRunReceiver = void 0; | |
| } | |
| }, { | |
| mergeProjects: true, | |
| mergeTestCases: false, | |
| resolvePath: createPathResolve(this._options.pathSeparator) | |
| }); | |
| void this._lastRunReceiver.dispatch({ method: "onConfigure", params: { config: config2 } }); | |
| }, | |
| onBegin: (suite) => { | |
| if (!this.rootSuite) | |
| this.rootSuite = suite; | |
| if (this._testResultsSnapshot) { | |
| for (const test of this.rootSuite.allTests()) | |
| test.results = this._testResultsSnapshot?.get(test.id) || test.results; | |
| this._testResultsSnapshot = void 0; | |
| } | |
| this.progress.total = this._lastRunTestCount; | |
| this.progress.passed = 0; | |
| this.progress.failed = 0; | |
| this.progress.skipped = 0; | |
| this._options.onUpdate(true); | |
| }, | |
| onEnd: () => { | |
| this._options.onUpdate(true); | |
| }, | |
| onTestBegin: (test, testResult) => { | |
| testResult[statusEx] = "running"; | |
| this._options.onUpdate(); | |
| }, | |
| onTestEnd: (test, testResult) => { | |
| if (test.outcome() === "skipped") | |
| ++this.progress.skipped; | |
| else if (test.outcome() === "unexpected") | |
| ++this.progress.failed; | |
| else | |
| ++this.progress.passed; | |
| testResult[statusEx] = testResult.status; | |
| this._options.onUpdate(); | |
| }, | |
| onError: (error) => this._handleOnError(error), | |
| printsToStdio: () => false | |
| }; | |
| } | |
| processGlobalReport(report) { | |
| const receiver = new TeleReporterReceiver({ | |
| version: () => "v2", | |
| onConfigure: (c) => { | |
| this.config = c; | |
| }, | |
| onError: (error) => this._handleOnError(error) | |
| }); | |
| for (const message of report) | |
| void receiver.dispatch(message); | |
| } | |
| processListReport(report) { | |
| const tests = this.rootSuite?.allTests() || []; | |
| this._testResultsSnapshot = new Map(tests.map((test) => [test.id, test.results])); | |
| this._receiver.reset(); | |
| for (const message of report) | |
| void this._receiver.dispatch(message); | |
| } | |
| processTestReportEvent(message) { | |
| this._lastRunReceiver?.dispatch(message)?.catch(() => { | |
| }); | |
| this._receiver.dispatch(message)?.catch(() => { | |
| }); | |
| } | |
| _handleOnError(error) { | |
| this.loadErrors.push(error); | |
| this._options.onError?.(error); | |
| this._options.onUpdate(); | |
| } | |
| asModel() { | |
| return { | |
| rootSuite: this.rootSuite || new TeleSuite("", "root"), | |
| config: this.config, | |
| loadErrors: this.loadErrors, | |
| progress: this.progress | |
| }; | |
| } | |
| }; | |
| function createPathResolve(pathSeparator) { | |
| return (rootDir, relativePath) => { | |
| const segments = []; | |
| for (const segment of [...rootDir.split(pathSeparator), ...relativePath.split(pathSeparator)]) { | |
| const isAfterDrive = pathSeparator === "\\" && segments.length === 1 && segments[0].endsWith(":"); | |
| const isFirst = !segments.length; | |
| if (!segment && !isFirst && !isAfterDrive) | |
| continue; | |
| if (segment === ".") | |
| continue; | |
| if (segment === "..") { | |
| segments.pop(); | |
| continue; | |
| } | |
| segments.push(segment); | |
| } | |
| return segments.join(pathSeparator); | |
| }; | |
| } | |
| // packages/playwright/src/isomorphic/events.ts | |
| var Disposable; | |
| ((Disposable2) => { | |
| function disposeAll(disposables) { | |
| for (const disposable of disposables.splice(0)) | |
| disposable.dispose(); | |
| } | |
| Disposable2.disposeAll = disposeAll; | |
| })(Disposable || (Disposable = {})); | |
| var EventEmitter3 = class { | |
| constructor() { | |
| this._listeners = /* @__PURE__ */ new Set(); | |
| this.event = (listener, disposables) => { | |
| this._listeners.add(listener); | |
| let disposed = false; | |
| const self = this; | |
| const result = { | |
| dispose() { | |
| if (!disposed) { | |
| disposed = true; | |
| self._listeners.delete(listener); | |
| } | |
| } | |
| }; | |
| if (disposables) | |
| disposables.push(result); | |
| return result; | |
| }; | |
| } | |
| fire(event) { | |
| const dispatch = !this._deliveryQueue; | |
| if (!this._deliveryQueue) | |
| this._deliveryQueue = []; | |
| for (const listener of this._listeners) | |
| this._deliveryQueue.push({ listener, event }); | |
| if (!dispatch) | |
| return; | |
| for (let index = 0; index < this._deliveryQueue.length; index++) { | |
| const { listener, event: event2 } = this._deliveryQueue[index]; | |
| listener.call(null, event2); | |
| } | |
| this._deliveryQueue = void 0; | |
| } | |
| dispose() { | |
| this._listeners.clear(); | |
| if (this._deliveryQueue) | |
| this._deliveryQueue = []; | |
| } | |
| }; | |
| // packages/playwright/src/isomorphic/testServerConnection.ts | |
| var TestServerConnectionClosedError = class extends Error { | |
| }; | |
| var TestServerConnection = class { | |
| constructor(transport) { | |
| this._onCloseEmitter = new EventEmitter3(); | |
| this._onReportEmitter = new EventEmitter3(); | |
| this._onStdioEmitter = new EventEmitter3(); | |
| this._onTestFilesChangedEmitter = new EventEmitter3(); | |
| this._onLoadTraceRequestedEmitter = new EventEmitter3(); | |
| this._onTestPausedEmitter = new EventEmitter3(); | |
| this._lastId = 0; | |
| this._callbacks = /* @__PURE__ */ new Map(); | |
| this._isClosed = false; | |
| this.onClose = this._onCloseEmitter.event; | |
| this.onReport = this._onReportEmitter.event; | |
| this.onStdio = this._onStdioEmitter.event; | |
| this.onTestFilesChanged = this._onTestFilesChangedEmitter.event; | |
| this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event; | |
| this.onTestPaused = this._onTestPausedEmitter.event; | |
| this._transport = transport; | |
| this._transport.onmessage((data) => { | |
| const message = JSON.parse(data); | |
| const { id, result, error, method, params } = message; | |
| if (id) { | |
| const callback = this._callbacks.get(id); | |
| if (!callback) | |
| return; | |
| this._callbacks.delete(id); | |
| if (error) | |
| callback.reject(new Error(error)); | |
| else | |
| callback.resolve(result); | |
| } else { | |
| this._dispatchEvent(method, params); | |
| } | |
| }); | |
| const pingInterval = setInterval(() => this._sendMessage("ping").catch(() => { | |
| }), 3e4); | |
| this._connectedPromise = new Promise((f, r) => { | |
| this._transport.onopen(f); | |
| this._transport.onerror(r); | |
| }); | |
| this._transport.onclose(() => { | |
| this._isClosed = true; | |
| this._onCloseEmitter.fire(); | |
| clearInterval(pingInterval); | |
| for (const callback of this._callbacks.values()) | |
| callback.reject(callback.error); | |
| this._callbacks.clear(); | |
| }); | |
| } | |
| isClosed() { | |
| return this._isClosed; | |
| } | |
| async _sendMessage(method, params) { | |
| const logForTest = globalThis.__logForTest; | |
| logForTest?.({ method, params }); | |
| await this._connectedPromise; | |
| const id = ++this._lastId; | |
| const message = { id, method, params }; | |
| const error = new TestServerConnectionClosedError(`${method}: test server connection closed`); | |
| this._transport.send(JSON.stringify(message)); | |
| return new Promise((resolve, reject) => { | |
| this._callbacks.set(id, { resolve, reject, error }); | |
| }); | |
| } | |
| _sendMessageNoReply(method, params) { | |
| this._sendMessage(method, params).catch(() => { | |
| }); | |
| } | |
| _dispatchEvent(method, params) { | |
| if (method === "report") | |
| this._onReportEmitter.fire(params); | |
| else if (method === "stdio") | |
| this._onStdioEmitter.fire(params); | |
| else if (method === "testFilesChanged") | |
| this._onTestFilesChangedEmitter.fire(params); | |
| else if (method === "loadTraceRequested") | |
| this._onLoadTraceRequestedEmitter.fire(params); | |
| else if (method === "testPaused") | |
| this._onTestPausedEmitter.fire(params); | |
| } | |
| async initialize(params) { | |
| await this._sendMessage("initialize", params); | |
| } | |
| async ping(params) { | |
| await this._sendMessage("ping", params); | |
| } | |
| async pingNoReply(params) { | |
| this._sendMessageNoReply("ping", params); | |
| } | |
| async watch(params) { | |
| await this._sendMessage("watch", params); | |
| } | |
| watchNoReply(params) { | |
| this._sendMessageNoReply("watch", params); | |
| } | |
| async open(params) { | |
| await this._sendMessage("open", params); | |
| } | |
| openNoReply(params) { | |
| this._sendMessageNoReply("open", params); | |
| } | |
| async resizeTerminal(params) { | |
| await this._sendMessage("resizeTerminal", params); | |
| } | |
| resizeTerminalNoReply(params) { | |
| this._sendMessageNoReply("resizeTerminal", params); | |
| } | |
| async checkBrowsers(params) { | |
| return await this._sendMessage("checkBrowsers", params); | |
| } | |
| async installBrowsers(params) { | |
| await this._sendMessage("installBrowsers", params); | |
| } | |
| async runGlobalSetup(params) { | |
| return await this._sendMessage("runGlobalSetup", params); | |
| } | |
| async runGlobalTeardown(params) { | |
| return await this._sendMessage("runGlobalTeardown", params); | |
| } | |
| async clearCache(params) { | |
| return await this._sendMessage("clearCache", params); | |
| } | |
| async listFiles(params) { | |
| return await this._sendMessage("listFiles", params); | |
| } | |
| async listTests(params) { | |
| return await this._sendMessage("listTests", params); | |
| } | |
| async runTests(params) { | |
| return await this._sendMessage("runTests", params); | |
| } | |
| async findRelatedTestFiles(params) { | |
| return await this._sendMessage("findRelatedTestFiles", params); | |
| } | |
| async stopTests(params) { | |
| await this._sendMessage("stopTests", params); | |
| } | |
| stopTestsNoReply(params) { | |
| this._sendMessageNoReply("stopTests", params); | |
| } | |
| async closeGracefully(params) { | |
| await this._sendMessage("closeGracefully", params); | |
| } | |
| close() { | |
| try { | |
| this._transport.close(); | |
| } catch { | |
| } | |
| } | |
| }; | |
| // packages/playwright/src/runner/watchMode.ts | |
| var colors6 = require("playwright-core/lib/utilsBundle").colors; | |
| var enquirer = require("playwright-core/lib/utilsBundle").enquirer; | |
| var { ManualPromise: ManualPromise8 } = require("playwright-core/lib/coreBundle").iso; | |
| var { createGuid: createGuid3 } = require("playwright-core/lib/coreBundle").utils; | |
| var { getPackageManagerExecCommand: getPackageManagerExecCommand3 } = require("playwright-core/lib/coreBundle").utils; | |
| var { eventsHelper: eventsHelper2 } = require("playwright-core/lib/coreBundle").utils; | |
| var InMemoryTransport = class extends import_events3.EventEmitter { | |
| constructor(send) { | |
| super(); | |
| this._send = send; | |
| } | |
| close() { | |
| this.emit("close"); | |
| } | |
| onclose(listener) { | |
| this.on("close", listener); | |
| } | |
| onerror(listener) { | |
| } | |
| onmessage(listener) { | |
| this.on("message", listener); | |
| } | |
| onopen(listener) { | |
| this.on("open", listener); | |
| } | |
| send(data) { | |
| this._send(data); | |
| } | |
| }; | |
| async function runWatchModeLoop(configLocation, initialOptions) { | |
| const options = { ...initialOptions }; | |
| let bufferMode = false; | |
| const testServerDispatcher = new TestServerDispatcher(configLocation, {}); | |
| const transport = new InMemoryTransport( | |
| async (data) => { | |
| const { id, method, params } = JSON.parse(data); | |
| try { | |
| const result2 = await testServerDispatcher.transport.dispatch(method, params); | |
| transport.emit("message", JSON.stringify({ id, result: result2 })); | |
| } catch (e) { | |
| transport.emit("message", JSON.stringify({ id, error: String(e) })); | |
| } | |
| } | |
| ); | |
| testServerDispatcher.transport.sendEvent = (method, params) => { | |
| transport.emit("message", JSON.stringify({ method, params })); | |
| }; | |
| const testServerConnection = new TestServerConnection(transport); | |
| transport.emit("open"); | |
| const teleSuiteUpdater = new TeleSuiteUpdater({ pathSeparator: import_path18.default.sep, onUpdate() { | |
| } }); | |
| const dirtyTestFiles = /* @__PURE__ */ new Set(); | |
| const dirtyTestIds = /* @__PURE__ */ new Set(); | |
| let onDirtyTests = new ManualPromise8(); | |
| let queue = Promise.resolve(); | |
| const changedFiles = /* @__PURE__ */ new Set(); | |
| testServerConnection.onTestFilesChanged(({ testFiles }) => { | |
| testFiles.forEach((file) => changedFiles.add(file)); | |
| queue = queue.then(async () => { | |
| if (changedFiles.size === 0) | |
| return; | |
| const { report: report2 } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep }); | |
| teleSuiteUpdater.processListReport(report2); | |
| for (const test of teleSuiteUpdater.rootSuite.allTests()) { | |
| if (changedFiles.has(test.location.file)) { | |
| dirtyTestFiles.add(test.location.file); | |
| dirtyTestIds.add(test.id); | |
| } | |
| } | |
| changedFiles.clear(); | |
| if (dirtyTestIds.size > 0) { | |
| onDirtyTests.resolve("changed"); | |
| onDirtyTests = new ManualPromise8(); | |
| } | |
| }); | |
| }); | |
| testServerConnection.onReport((report2) => teleSuiteUpdater.processTestReportEvent(report2)); | |
| await testServerConnection.initialize({ | |
| interceptStdio: false, | |
| watchTestDirs: true | |
| }); | |
| await testServerConnection.runGlobalSetup({}); | |
| const { report } = await testServerConnection.listTests({}); | |
| teleSuiteUpdater.processListReport(report); | |
| const projectNames = teleSuiteUpdater.rootSuite.suites.map((s) => s.title); | |
| let lastRun = { type: "regular" }; | |
| let result = "passed"; | |
| while (true) { | |
| if (bufferMode) | |
| printBufferPrompt(dirtyTestFiles, teleSuiteUpdater.config.rootDir); | |
| else | |
| printPrompt(); | |
| const waitForCommand = readCommand(); | |
| const command = await Promise.race([ | |
| onDirtyTests, | |
| waitForCommand.result | |
| ]); | |
| if (command === "changed") | |
| waitForCommand.dispose(); | |
| if (bufferMode && command === "changed") | |
| continue; | |
| const shouldRunChangedFiles = bufferMode ? command === "run" : command === "changed"; | |
| if (shouldRunChangedFiles) { | |
| if (dirtyTestIds.size === 0) | |
| continue; | |
| const testIds = [...dirtyTestIds]; | |
| dirtyTestIds.clear(); | |
| dirtyTestFiles.clear(); | |
| await runTests(options, testServerConnection, { testIds, title: "files changed" }); | |
| lastRun = { type: "changed", dirtyTestIds: testIds }; | |
| continue; | |
| } | |
| if (command === "run") { | |
| await runTests(options, testServerConnection); | |
| lastRun = { type: "regular" }; | |
| continue; | |
| } | |
| if (command === "project") { | |
| const { selectedProjects } = await enquirer.prompt({ | |
| type: "multiselect", | |
| name: "selectedProjects", | |
| message: "Select projects", | |
| choices: projectNames | |
| }).catch(() => ({ selectedProjects: null })); | |
| if (!selectedProjects) | |
| continue; | |
| options.projects = selectedProjects.length ? selectedProjects : void 0; | |
| await runTests(options, testServerConnection); | |
| lastRun = { type: "regular" }; | |
| continue; | |
| } | |
| if (command === "file") { | |
| const { filePattern } = await enquirer.prompt({ | |
| type: "text", | |
| name: "filePattern", | |
| message: "Input filename pattern (regex)" | |
| }).catch(() => ({ filePattern: null })); | |
| if (filePattern === null) | |
| continue; | |
| if (filePattern.trim()) | |
| options.files = filePattern.split(" "); | |
| else | |
| options.files = void 0; | |
| await runTests(options, testServerConnection); | |
| lastRun = { type: "regular" }; | |
| continue; | |
| } | |
| if (command === "grep") { | |
| const { testPattern } = await enquirer.prompt({ | |
| type: "text", | |
| name: "testPattern", | |
| message: "Input test name pattern (regex)" | |
| }).catch(() => ({ testPattern: null })); | |
| if (testPattern === null) | |
| continue; | |
| if (testPattern.trim()) | |
| options.grep = testPattern; | |
| else | |
| options.grep = void 0; | |
| await runTests(options, testServerConnection); | |
| lastRun = { type: "regular" }; | |
| continue; | |
| } | |
| if (command === "failed") { | |
| const failedTestIds = teleSuiteUpdater.rootSuite.allTests().filter((t2) => !t2.ok()).map((t2) => t2.id); | |
| await runTests({}, testServerConnection, { title: "running failed tests", testIds: failedTestIds }); | |
| lastRun = { type: "failed", failedTestIds }; | |
| continue; | |
| } | |
| if (command === "repeat") { | |
| if (lastRun.type === "regular") { | |
| await runTests(options, testServerConnection, { title: "re-running tests" }); | |
| continue; | |
| } else if (lastRun.type === "changed") { | |
| await runTests(options, testServerConnection, { title: "re-running tests", testIds: lastRun.dirtyTestIds }); | |
| } else if (lastRun.type === "failed") { | |
| await runTests({}, testServerConnection, { title: "re-running tests", testIds: lastRun.failedTestIds }); | |
| } | |
| continue; | |
| } | |
| if (command === "toggle-show-browser") { | |
| await toggleShowBrowser(); | |
| continue; | |
| } | |
| if (command === "toggle-buffer-mode") { | |
| bufferMode = !bufferMode; | |
| continue; | |
| } | |
| if (command === "exit") | |
| break; | |
| if (command === "interrupted") { | |
| result = "interrupted"; | |
| break; | |
| } | |
| } | |
| const teardown = await testServerConnection.runGlobalTeardown({}); | |
| return result === "passed" ? teardown.status : result; | |
| } | |
| function readKeyPress(handler) { | |
| const promise = new ManualPromise8(); | |
| const rl = import_readline.default.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); | |
| import_readline.default.emitKeypressEvents(process.stdin, rl); | |
| if (process.stdin.isTTY) | |
| process.stdin.setRawMode(true); | |
| const listener = eventsHelper2.addEventListener(process.stdin, "keypress", (text, key) => { | |
| const result = handler(text, key); | |
| if (result) | |
| promise.resolve(result); | |
| }); | |
| const dispose = () => { | |
| eventsHelper2.removeEventListeners([listener]); | |
| rl.close(); | |
| if (process.stdin.isTTY) | |
| process.stdin.setRawMode(false); | |
| }; | |
| void promise.finally(dispose); | |
| return { result: promise, dispose }; | |
| } | |
| var isInterrupt = (text, key) => text === "" || text === "\x1B" || key && key.name === "escape" || key && key.ctrl && key.name === "c"; | |
| async function runTests(watchOptions, testServerConnection, options) { | |
| printConfiguration(watchOptions, options?.title); | |
| const waitForDone = readKeyPress((text, key) => { | |
| if (isInterrupt(text, key)) { | |
| testServerConnection.stopTestsNoReply({}); | |
| return "done"; | |
| } | |
| }); | |
| await testServerConnection.runTests({ | |
| grep: watchOptions.grep, | |
| testIds: options?.testIds, | |
| locations: watchOptions?.files ?? [], | |
| // TODO: always collect locations based on knowledge about tree, so that we don't have to load all tests | |
| projects: watchOptions.projects, | |
| connectWsEndpoint, | |
| reuseContext: connectWsEndpoint ? true : void 0, | |
| workers: connectWsEndpoint ? 1 : void 0, | |
| headed: connectWsEndpoint ? true : void 0 | |
| }).finally(() => waitForDone.dispose()); | |
| } | |
| function readCommand() { | |
| return readKeyPress((text, key) => { | |
| if (isInterrupt(text, key)) | |
| return "interrupted"; | |
| if (process.platform !== "win32" && key && key.ctrl && key.name === "z") { | |
| process.kill(process.ppid, "SIGTSTP"); | |
| process.kill(process.pid, "SIGTSTP"); | |
| } | |
| const name = key?.name; | |
| if (name === "q") | |
| return "exit"; | |
| if (name === "h") { | |
| process.stdout.write(`${separator(terminalScreen)} | |
| Run tests | |
| ${colors6.bold("enter")} ${colors6.dim("run tests")} | |
| ${colors6.bold("f")} ${colors6.dim("run failed tests")} | |
| ${colors6.bold("r")} ${colors6.dim("repeat last run")} | |
| ${colors6.bold("q")} ${colors6.dim("quit")} | |
| Change settings | |
| ${colors6.bold("c")} ${colors6.dim("set project")} | |
| ${colors6.bold("p")} ${colors6.dim("set file filter")} | |
| ${colors6.bold("t")} ${colors6.dim("set title filter")} | |
| ${colors6.bold("s")} ${colors6.dim("toggle show & reuse the browser")} | |
| ${colors6.bold("b")} ${colors6.dim("toggle buffer mode")} | |
| `); | |
| return; | |
| } | |
| switch (name) { | |
| case "return": | |
| return "run"; | |
| case "r": | |
| return "repeat"; | |
| case "c": | |
| return "project"; | |
| case "p": | |
| return "file"; | |
| case "t": | |
| return "grep"; | |
| case "f": | |
| return "failed"; | |
| case "s": | |
| return "toggle-show-browser"; | |
| case "b": | |
| return "toggle-buffer-mode"; | |
| } | |
| }); | |
| } | |
| var showBrowserServer; | |
| var connectWsEndpoint = void 0; | |
| var seq = 1; | |
| function printConfiguration(options, title) { | |
| const packageManagerCommand = getPackageManagerExecCommand3(); | |
| const tokens = []; | |
| tokens.push(`${packageManagerCommand} playwright test`); | |
| if (options.projects) | |
| tokens.push(...options.projects.map((p) => colors6.blue(`--project ${p}`))); | |
| if (options.grep) | |
| tokens.push(colors6.red(`--grep ${options.grep}`)); | |
| if (options.files) | |
| tokens.push(...options.files.map((a) => colors6.bold(a))); | |
| if (title) | |
| tokens.push(colors6.dim(`(${title})`)); | |
| tokens.push(colors6.dim(`#${seq++}`)); | |
| const lines = []; | |
| const sep = separator(terminalScreen); | |
| lines.push("\x1Bc" + sep); | |
| lines.push(`${tokens.join(" ")}`); | |
| lines.push(`${colors6.dim("Show & reuse browser:")} ${colors6.bold(showBrowserServer ? "on" : "off")}`); | |
| process.stdout.write(lines.join("\n")); | |
| } | |
| function printBufferPrompt(dirtyTestFiles, rootDir) { | |
| const sep = separator(terminalScreen); | |
| process.stdout.write("\x1Bc"); | |
| process.stdout.write(`${sep} | |
| `); | |
| if (dirtyTestFiles.size === 0) { | |
| process.stdout.write(`${colors6.dim("Waiting for file changes. Press")} ${colors6.bold("q")} ${colors6.dim("to quit or")} ${colors6.bold("h")} ${colors6.dim("for more options.")} | |
| `); | |
| return; | |
| } | |
| process.stdout.write(`${colors6.dim(`${dirtyTestFiles.size} test ${dirtyTestFiles.size === 1 ? "file" : "files"} changed:`)} | |
| `); | |
| for (const file of dirtyTestFiles) | |
| process.stdout.write(` \xB7 ${import_path18.default.relative(rootDir, file)} | |
| `); | |
| process.stdout.write(` | |
| ${colors6.dim(`Press`)} ${colors6.bold("enter")} ${colors6.dim("to run")}, ${colors6.bold("q")} ${colors6.dim("to quit or")} ${colors6.bold("h")} ${colors6.dim("for more options.")} | |
| `); | |
| } | |
| function printPrompt() { | |
| const sep = separator(terminalScreen); | |
| process.stdout.write(` | |
| ${sep} | |
| ${colors6.dim("Waiting for file changes. Press")} ${colors6.bold("enter")} ${colors6.dim("to run tests")}, ${colors6.bold("q")} ${colors6.dim("to quit or")} ${colors6.bold("h")} ${colors6.dim("for more options.")} | |
| `); | |
| } | |
| async function toggleShowBrowser() { | |
| if (!showBrowserServer) { | |
| showBrowserServer = new import_coreBundle5.remote.PlaywrightServer({ mode: "extension", path: "/" + createGuid3(), maxConnections: 1 }); | |
| connectWsEndpoint = await showBrowserServer.listen(); | |
| process.stdout.write(`${colors6.dim("Show & reuse browser:")} ${colors6.bold("on")} | |
| `); | |
| } else { | |
| await showBrowserServer?.close(); | |
| showBrowserServer = void 0; | |
| connectWsEndpoint = void 0; | |
| process.stdout.write(`${colors6.dim("Show & reuse browser:")} ${colors6.bold("off")} | |
| `); | |
| } | |
| } | |
| // packages/playwright/src/reporters/merge.ts | |
| var merge_exports = {}; | |
| __export(merge_exports, { | |
| createMergedReport: () => createMergedReport | |
| }); | |
| var import_fs14 = __toESM(require("fs")); | |
| var import_path19 = __toESM(require("path")); | |
| // packages/playwright/src/isomorphic/stringInternPool.ts | |
| var StringInternPool = class { | |
| constructor() { | |
| this._stringCache = /* @__PURE__ */ new Map(); | |
| } | |
| internString(s) { | |
| let result = this._stringCache.get(s); | |
| if (!result) { | |
| this._stringCache.set(s, s); | |
| result = s; | |
| } | |
| return result; | |
| } | |
| }; | |
| var JsonStringInternalizer = class { | |
| constructor(pool) { | |
| this._pool = pool; | |
| } | |
| traverse(value) { | |
| if (typeof value !== "object") | |
| return; | |
| if (Array.isArray(value)) { | |
| for (let i = 0; i < value.length; i++) { | |
| if (typeof value[i] === "string") | |
| value[i] = this.intern(value[i]); | |
| else | |
| this.traverse(value[i]); | |
| } | |
| } else { | |
| for (const name in value) { | |
| if (typeof value[name] === "string") | |
| value[name] = this.intern(value[name]); | |
| else | |
| this.traverse(value[name]); | |
| } | |
| } | |
| } | |
| intern(value) { | |
| return this._pool.internString(value); | |
| } | |
| }; | |
| // packages/playwright/src/reporters/merge.ts | |
| var import_util18 = require("../util"); | |
| var { isPathInside } = require("playwright-core/lib/coreBundle").utils; | |
| var { ZipFile } = require("playwright-core/lib/coreBundle").utils; | |
| async function createMergedReport(config2, dir, reporterDescriptions, rootDirOverride) { | |
| const reporters = await createReporters(config2, "merge", reporterDescriptions); | |
| const multiplexer = new Multiplexer(reporters); | |
| const stringPool = new StringInternPool(); | |
| let printStatus = () => { | |
| }; | |
| if (!multiplexer.printsToStdio()) { | |
| printStatus = printStatusToStdout; | |
| printStatus(`merging reports from ${dir}`); | |
| } | |
| const shardFiles = await sortedShardFiles(dir); | |
| if (shardFiles.length === 0) | |
| throw new Error(`No report files found in ${dir}`); | |
| const eventData = await mergeEvents(dir, shardFiles, stringPool, printStatus, rootDirOverride); | |
| const pathSeparator = rootDirOverride ? import_path19.default.sep : eventData.pathSeparatorFromMetadata ?? import_path19.default.sep; | |
| const pathPackage = pathSeparator === "/" ? import_path19.default.posix : import_path19.default.win32; | |
| const receiver = new TeleReporterReceiver(multiplexer, { | |
| mergeProjects: false, | |
| mergeTestCases: false, | |
| // When merging on a different OS, an absolute path like `C:\foo\bar` from win may look like | |
| // a relative path on posix, and vice versa. | |
| // Therefore, we cannot use `path.resolve()` here - it will resolve relative-looking paths | |
| // against `process.cwd()`, while we just want to normalize ".." and "." segments. | |
| resolvePath: (rootDir, relativePath) => stringPool.internString(pathPackage.normalize(pathPackage.join(rootDir, relativePath))), | |
| configOverrides: config2.config | |
| }); | |
| printStatus(`processing test events`); | |
| const dispatchEvents = async (events) => { | |
| for (const event of events) { | |
| if (event.method === "onEnd") | |
| printStatus(`building final report`); | |
| await receiver.dispatch(event); | |
| if (event.method === "onEnd") | |
| printStatus(`finished building report`); | |
| } | |
| }; | |
| await dispatchEvents(eventData.prologue); | |
| let usedWorkers = 0; | |
| for (const { reportFile, zipFile, eventPatchers, metadata, config: config3, fullResult } of eventData.reports) { | |
| multiplexer.onReportConfigure({ | |
| reportPath: zipFile, | |
| config: asFullConfig(config3) | |
| }); | |
| const reportJsonl = await import_fs14.default.promises.readFile(reportFile); | |
| const events = parseTestEvents(reportJsonl); | |
| new JsonStringInternalizer(stringPool).traverse(events); | |
| eventPatchers.patchers.push(new AttachmentPathPatcher(dir)); | |
| if (metadata.name) | |
| eventPatchers.patchers.push(new GlobalErrorPatcher(metadata.name)); | |
| if (config3?.tags?.length) | |
| eventPatchers.patchers.push(new GlobalErrorPatcher(config3.tags.join(" "))); | |
| const workerIndexPatcher = new WorkerIndexPatcher(usedWorkers); | |
| eventPatchers.patchers.push(workerIndexPatcher); | |
| eventPatchers.patchEvents(events); | |
| usedWorkers += workerIndexPatcher.usedWorkers(); | |
| await dispatchEvents(events); | |
| multiplexer.onReportEnd({ | |
| reportPath: zipFile, | |
| result: asFullResult(fullResult) | |
| }); | |
| } | |
| await dispatchEvents(eventData.epilogue); | |
| return multiplexer.hasReporterErrors() ? "failed" : "passed"; | |
| } | |
| var commonEventNames = ["onBlobReportMetadata", "onConfigure", "onProject", "onBegin", "onEnd"]; | |
| var commonEvents = new Set(commonEventNames); | |
| var commonEventRegex = new RegExp(`${commonEventNames.join("|")}`); | |
| function parseCommonEvents(reportJsonl) { | |
| return splitBufferLines(reportJsonl).map((line) => line.toString("utf8")).filter((line) => commonEventRegex.test(line)).map((line) => JSON.parse(line)).filter((event) => commonEvents.has(event.method)); | |
| } | |
| function parseTestEvents(reportJsonl) { | |
| return splitBufferLines(reportJsonl).map((line) => line.toString("utf8")).filter((line) => line.length).map((line) => JSON.parse(line)).filter((event) => !commonEvents.has(event.method)); | |
| } | |
| function splitBufferLines(buffer) { | |
| const lines = []; | |
| let start = 0; | |
| while (start < buffer.length) { | |
| const end = buffer.indexOf(10, start); | |
| if (end === -1) { | |
| lines.push(buffer.slice(start)); | |
| break; | |
| } | |
| lines.push(buffer.slice(start, end)); | |
| start = end + 1; | |
| } | |
| return lines; | |
| } | |
| async function extractAndParseReports(dir, shardFiles, internalizer, printStatus) { | |
| const shardEvents = []; | |
| await import_fs14.default.promises.mkdir(import_path19.default.join(dir, "resources"), { recursive: true }); | |
| const reportNames = new UniqueFileNameGenerator(); | |
| for (const file of shardFiles) { | |
| const absolutePath = import_path19.default.join(dir, file); | |
| printStatus(`extracting: ${(0, import_util18.relativeFilePath)(absolutePath)}`); | |
| const zipFile = new ZipFile(absolutePath); | |
| const entryNames = await zipFile.entries(); | |
| for (const entryName of entryNames.sort()) { | |
| let reportFile = import_path19.default.join(dir, entryName); | |
| const content = await zipFile.read(entryName); | |
| if (entryName.endsWith(".jsonl")) { | |
| reportFile = reportNames.makeUnique(reportFile); | |
| let parsedEvents = parseCommonEvents(content); | |
| internalizer.traverse(parsedEvents); | |
| const metadata = findMetadata(parsedEvents, file); | |
| parsedEvents = modernizer.modernize(metadata.version, parsedEvents); | |
| shardEvents.push({ | |
| zipFile: absolutePath, | |
| reportFile, | |
| metadata, | |
| parsedEvents | |
| }); | |
| } | |
| await import_fs14.default.promises.writeFile(reportFile, content); | |
| } | |
| zipFile.close(); | |
| } | |
| return shardEvents; | |
| } | |
| function findMetadata(events, file) { | |
| if (events[0]?.method !== "onBlobReportMetadata") | |
| throw new Error(`No metadata event found in ${file}`); | |
| const metadata = events[0].params; | |
| if (metadata.version > currentBlobReportVersion) | |
| throw new Error(`Blob report ${file} was created with a newer version of Playwright.`); | |
| return metadata; | |
| } | |
| async function mergeEvents(dir, shardReportFiles, stringPool, printStatus, rootDirOverride) { | |
| const internalizer = new JsonStringInternalizer(stringPool); | |
| const configureEvents = []; | |
| const projectEvents = []; | |
| const endEvents = []; | |
| const blobs = await extractAndParseReports(dir, shardReportFiles, internalizer, printStatus); | |
| blobs.sort((a, b) => { | |
| const nameA = a.metadata.name ?? ""; | |
| const nameB = b.metadata.name ?? ""; | |
| if (nameA !== nameB) | |
| return nameA.localeCompare(nameB); | |
| const shardA = a.metadata.shard?.current ?? 0; | |
| const shardB = b.metadata.shard?.current ?? 0; | |
| if (shardA !== shardB) | |
| return shardA - shardB; | |
| return a.zipFile.localeCompare(b.zipFile); | |
| }); | |
| printStatus(`merging events`); | |
| const reports = []; | |
| const globalTestIdSet = /* @__PURE__ */ new Set(); | |
| for (let i = 0; i < blobs.length; ++i) { | |
| const { parsedEvents, metadata, reportFile, zipFile } = blobs[i]; | |
| const eventPatchers = new JsonEventPatchers(); | |
| eventPatchers.patchers.push(new IdsPatcher( | |
| stringPool, | |
| metadata.name, | |
| String(i), | |
| globalTestIdSet | |
| )); | |
| if (rootDirOverride) | |
| eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator)); | |
| eventPatchers.patchEvents(parsedEvents); | |
| let config2; | |
| let fullResult; | |
| for (const event of parsedEvents) { | |
| if (event.method === "onConfigure") { | |
| configureEvents.push(event); | |
| config2 = event.params.config; | |
| } else if (event.method === "onProject") { | |
| projectEvents.push(event); | |
| } else if (event.method === "onEnd") { | |
| fullResult = event.params.result; | |
| endEvents.push({ event, metadata }); | |
| } | |
| } | |
| reports.push({ | |
| eventPatchers, | |
| reportFile, | |
| zipFile, | |
| metadata, | |
| config: config2, | |
| fullResult | |
| }); | |
| } | |
| return { | |
| prologue: [ | |
| mergeConfigureEvents(configureEvents, rootDirOverride), | |
| ...projectEvents, | |
| { method: "onBegin", params: void 0 } | |
| ], | |
| reports, | |
| epilogue: [ | |
| mergeEndEvents(endEvents), | |
| { method: "onExit", params: void 0 } | |
| ], | |
| pathSeparatorFromMetadata: blobs[0]?.metadata.pathSeparator | |
| }; | |
| } | |
| function mergeConfigureEvents(configureEvents, rootDirOverride) { | |
| if (!configureEvents.length) | |
| throw new Error("No configure events found"); | |
| let config2 = { | |
| configFile: void 0, | |
| globalTimeout: 0, | |
| maxFailures: 0, | |
| metadata: {}, | |
| shard: null, | |
| rootDir: "", | |
| version: "", | |
| workers: 0, | |
| globalSetup: null, | |
| globalTeardown: null | |
| }; | |
| for (const event of configureEvents) | |
| config2 = mergeConfigs(config2, event.params.config); | |
| if (rootDirOverride) { | |
| config2.rootDir = rootDirOverride; | |
| } else { | |
| const rootDirs = new Set(configureEvents.map((e) => e.params.config.rootDir)); | |
| if (rootDirs.size > 1) { | |
| throw new Error([ | |
| `Blob reports being merged were recorded with different test directories, and`, | |
| `merging cannot proceed. This may happen if you are merging reports from`, | |
| `machines with different environments, like different operating systems or`, | |
| `if the tests ran with different playwright configs.`, | |
| ``, | |
| `You can force merge by specifying a merge config file with "-c" option. If`, | |
| `you'd like all test paths to be correct, make sure 'testDir' in the merge config`, | |
| `file points to the actual tests location.`, | |
| ``, | |
| `Found directories:`, | |
| ...rootDirs | |
| ].join("\n")); | |
| } | |
| } | |
| return { | |
| method: "onConfigure", | |
| params: { | |
| config: config2 | |
| } | |
| }; | |
| } | |
| function mergeConfigs(to, from) { | |
| return { | |
| ...to, | |
| ...from, | |
| metadata: { | |
| ...to.metadata, | |
| ...from.metadata, | |
| actualWorkers: (to.metadata.actualWorkers || 0) + (from.metadata.actualWorkers || 0) | |
| }, | |
| shard: null, | |
| workers: to.workers + from.workers | |
| }; | |
| } | |
| function mergeEndEvents(endEvents) { | |
| let startTime = endEvents.length ? 1e13 : Date.now(); | |
| let status = "passed"; | |
| let endTime = 0; | |
| for (const { event } of endEvents) { | |
| const shardResult = event.params.result; | |
| if (shardResult.status === "failed") | |
| status = "failed"; | |
| else if (shardResult.status === "timedout" && status !== "failed") | |
| status = "timedout"; | |
| else if (shardResult.status === "interrupted" && status !== "failed" && status !== "timedout") | |
| status = "interrupted"; | |
| startTime = Math.min(startTime, shardResult.startTime); | |
| endTime = Math.max(endTime, shardResult.startTime + shardResult.duration); | |
| } | |
| const result = { | |
| status, | |
| startTime, | |
| duration: endTime - startTime | |
| }; | |
| return { | |
| method: "onEnd", | |
| params: { | |
| result | |
| } | |
| }; | |
| } | |
| async function sortedShardFiles(dir) { | |
| const files = await import_fs14.default.promises.readdir(dir); | |
| return files.filter((file) => file.endsWith(".zip")).sort(); | |
| } | |
| function printStatusToStdout(message) { | |
| process.stdout.write(`${message} | |
| `); | |
| } | |
| var UniqueFileNameGenerator = class { | |
| constructor() { | |
| this._usedNames = /* @__PURE__ */ new Set(); | |
| } | |
| makeUnique(name) { | |
| if (!this._usedNames.has(name)) { | |
| this._usedNames.add(name); | |
| return name; | |
| } | |
| const extension = import_path19.default.extname(name); | |
| name = name.substring(0, name.length - extension.length); | |
| let index = 0; | |
| while (true) { | |
| const candidate = `${name}-${++index}${extension}`; | |
| if (!this._usedNames.has(candidate)) { | |
| this._usedNames.add(candidate); | |
| return candidate; | |
| } | |
| } | |
| } | |
| }; | |
| var IdsPatcher = class { | |
| constructor(stringPool, botName, salt, globalTestIdSet) { | |
| this._stringPool = stringPool; | |
| this._botName = botName; | |
| this._salt = salt; | |
| this._testIdsMap = /* @__PURE__ */ new Map(); | |
| this._globalTestIdSet = globalTestIdSet; | |
| } | |
| patchEvent(event) { | |
| const { method, params } = event; | |
| switch (method) { | |
| case "onProject": | |
| this._onProject(params.project); | |
| return; | |
| case "onAttach": | |
| case "onTestBegin": | |
| case "onStepBegin": | |
| case "onStepEnd": | |
| case "onStdIO": | |
| params.testId = params.testId ? this._mapTestId(params.testId) : void 0; | |
| return; | |
| case "onTestEnd": | |
| params.test.testId = this._mapTestId(params.test.testId); | |
| return; | |
| } | |
| } | |
| _onProject(project) { | |
| project.metadata ??= {}; | |
| project.suites.forEach((suite) => this._updateTestIds(suite)); | |
| } | |
| _updateTestIds(suite) { | |
| suite.entries.forEach((entry) => { | |
| if ("testId" in entry) | |
| this._updateTestId(entry); | |
| else | |
| this._updateTestIds(entry); | |
| }); | |
| } | |
| _updateTestId(test) { | |
| test.testId = this._mapTestId(test.testId); | |
| if (this._botName) { | |
| test.tags = test.tags || []; | |
| test.tags.unshift("@" + this._botName); | |
| } | |
| } | |
| _mapTestId(testId) { | |
| const t1 = this._stringPool.internString(testId); | |
| if (this._testIdsMap.has(t1)) | |
| return this._testIdsMap.get(t1); | |
| if (this._globalTestIdSet.has(t1)) { | |
| const t2 = this._stringPool.internString(testId + this._salt); | |
| this._globalTestIdSet.add(t2); | |
| this._testIdsMap.set(t1, t2); | |
| return t2; | |
| } | |
| this._globalTestIdSet.add(t1); | |
| this._testIdsMap.set(t1, t1); | |
| return t1; | |
| } | |
| }; | |
| var AttachmentPathPatcher = class { | |
| constructor(_resourceDir) { | |
| this._resourceDir = _resourceDir; | |
| } | |
| patchEvent(event) { | |
| if (event.method === "onAttach") | |
| this._patchAttachments(event.params.attachments); | |
| else if (event.method === "onTestEnd") | |
| this._patchAttachments(event.params.result.attachments ?? []); | |
| } | |
| _patchAttachments(attachments) { | |
| const resourceRoot = import_path19.default.resolve(this._resourceDir); | |
| for (const attachment of attachments) { | |
| if (!attachment.path) | |
| continue; | |
| const joined = import_path19.default.resolve(resourceRoot, attachment.path); | |
| if (!isPathInside(resourceRoot, joined)) { | |
| attachment.path = void 0; | |
| continue; | |
| } | |
| attachment.path = joined; | |
| } | |
| } | |
| }; | |
| var PathSeparatorPatcher = class { | |
| constructor(from) { | |
| this._from = from ?? (import_path19.default.sep === "/" ? "\\" : "/"); | |
| this._to = import_path19.default.sep; | |
| } | |
| patchEvent(jsonEvent) { | |
| if (this._from === this._to) | |
| return; | |
| if (jsonEvent.method === "onProject") { | |
| this._updateProject(jsonEvent.params.project); | |
| return; | |
| } | |
| if (jsonEvent.method === "onTestEnd") { | |
| const test = jsonEvent.params.test; | |
| test.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation)); | |
| const testResult = jsonEvent.params.result; | |
| testResult.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation)); | |
| testResult.errors.forEach((error) => this._updateErrorLocations(error)); | |
| (testResult.attachments ?? []).forEach((attachment) => { | |
| if (attachment.path) | |
| attachment.path = this._updatePath(attachment.path); | |
| }); | |
| return; | |
| } | |
| if (jsonEvent.method === "onStepBegin") { | |
| const step = jsonEvent.params.step; | |
| this._updateLocation(step.location); | |
| return; | |
| } | |
| if (jsonEvent.method === "onStepEnd") { | |
| const step = jsonEvent.params.step; | |
| this._updateErrorLocations(step.error); | |
| step.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation)); | |
| return; | |
| } | |
| if (jsonEvent.method === "onAttach") { | |
| const attach = jsonEvent.params; | |
| attach.attachments.forEach((attachment) => { | |
| if (attachment.path) | |
| attachment.path = this._updatePath(attachment.path); | |
| }); | |
| return; | |
| } | |
| } | |
| _updateProject(project) { | |
| project.outputDir = this._updatePath(project.outputDir); | |
| project.testDir = this._updatePath(project.testDir); | |
| project.snapshotDir = this._updatePath(project.snapshotDir); | |
| project.suites.forEach((suite) => this._updateSuite(suite, true)); | |
| } | |
| _updateSuite(suite, isFileSuite = false) { | |
| this._updateLocation(suite.location); | |
| if (isFileSuite) | |
| suite.title = this._updatePath(suite.title); | |
| for (const entry of suite.entries) { | |
| if ("testId" in entry) { | |
| this._updateLocation(entry.location); | |
| entry.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation)); | |
| } else { | |
| this._updateSuite(entry); | |
| } | |
| } | |
| } | |
| _updateErrorLocations(error) { | |
| while (error) { | |
| this._updateLocation(error.location); | |
| error = error.cause; | |
| } | |
| } | |
| _updateAnnotationLocation(annotation) { | |
| this._updateLocation(annotation.location); | |
| } | |
| _updateLocation(location) { | |
| if (location) | |
| location.file = this._updatePath(location.file); | |
| } | |
| _updatePath(text) { | |
| return text.split(this._from).join(this._to); | |
| } | |
| }; | |
| var GlobalErrorPatcher = class { | |
| constructor(botName) { | |
| this._prefix = `(${botName}) `; | |
| } | |
| patchEvent(event) { | |
| if (event.method !== "onError") | |
| return; | |
| const error = event.params.error; | |
| if (error.message !== void 0) | |
| error.message = this._prefix + error.message; | |
| if (error.stack !== void 0) | |
| error.stack = this._prefix + error.stack; | |
| } | |
| }; | |
| var WorkerIndexPatcher = class { | |
| constructor(baseWorkerIndex) { | |
| this._maxWorkerIndex = 0; | |
| this._baseWorkerIndex = baseWorkerIndex; | |
| } | |
| patchEvent(event) { | |
| if (event.method === "onTestBegin") { | |
| this._maxWorkerIndex = Math.max(this._maxWorkerIndex, event.params.result.workerIndex); | |
| event.params.result.workerIndex += this._baseWorkerIndex; | |
| } | |
| } | |
| usedWorkers() { | |
| return this._maxWorkerIndex + 1; | |
| } | |
| }; | |
| var JsonEventPatchers = class { | |
| constructor() { | |
| this.patchers = []; | |
| } | |
| patchEvents(events) { | |
| for (const event of events) { | |
| for (const patcher of this.patchers) | |
| patcher.patchEvent(event); | |
| } | |
| } | |
| }; | |
| var BlobModernizer = class { | |
| modernize(fromVersion, events) { | |
| const result = []; | |
| for (const event of events) | |
| result.push(...this._modernize(fromVersion, event)); | |
| return result; | |
| } | |
| _modernize(fromVersion, event) { | |
| let events = [event]; | |
| for (let version = fromVersion; version < currentBlobReportVersion; ++version) | |
| events = this[`_modernize_${version}_to_${version + 1}`].call(this, events); | |
| return events; | |
| } | |
| _modernize_1_to_2(events) { | |
| return events.map((event) => { | |
| if (event.method === "onProject") { | |
| const modernizeSuite = (suite) => { | |
| const newSuites = suite.suites.map(modernizeSuite); | |
| const { suites, tests, ...remainder } = suite; | |
| return { entries: [...newSuites, ...tests], ...remainder }; | |
| }; | |
| const project = event.params.project; | |
| project.suites = project.suites.map(modernizeSuite); | |
| } | |
| return event; | |
| }); | |
| } | |
| }; | |
| var modernizer = new BlobModernizer(); | |
| // Annotate the CommonJS export names for ESM import in node: | |
| 0 && (module.exports = { | |
| ListModeReporter, | |
| ListReporter, | |
| TestServerConnection, | |
| base, | |
| html, | |
| merge, | |
| projectUtils, | |
| runnerReporters, | |
| testRunner, | |
| testServer, | |
| watchMode, | |
| webServer | |
| }); | |