| "use strict"; |
| var __create = Object.create; |
| var __defProp = Object.defineProperty; |
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; |
| var __getOwnPropNames = Object.getOwnPropertyNames; |
| var __getProtoOf = Object.getPrototypeOf; |
| var __hasOwnProp = Object.prototype.hasOwnProperty; |
| var __export = (target, all) => { |
| for (var name in all) |
| __defProp(target, name, { get: all[name], enumerable: true }); |
| }; |
| var __copyProps = (to, from, except, desc) => { |
| if (from && typeof from === "object" || typeof from === "function") { |
| for (let key of __getOwnPropNames(from)) |
| if (!__hasOwnProp.call(to, key) && key !== except) |
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); |
| } |
| return to; |
| }; |
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( |
| |
| |
| |
| |
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, |
| mod |
| )); |
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); |
|
|
| |
| 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); |
|
|
| |
| var testRunner_exports = {}; |
| __export(testRunner_exports, { |
| TestRunner: () => TestRunner, |
| TestRunnerEvent: () => TestRunnerEvent, |
| runAllTestsWithConfig: () => runAllTestsWithConfig |
| }); |
| var import_events2 = __toESM(require("events")); |
| var import_fs13 = __toESM(require("fs")); |
| var import_path17 = __toESM(require("path")); |
| var import_coreBundle2 = require("playwright-core/lib/coreBundle"); |
| var import_common9 = require("../common"); |
|
|
| |
| var chokidar = require("playwright-core/lib/utilsBundle").chokidar; |
| var FSWatcher = class { |
| constructor(onChange) { |
| this._watchedPaths = []; |
| this._ignoredFolders = []; |
| this._collector = []; |
| this._onChange = onChange; |
| } |
| async update(watchedPaths, ignoredFolders, reportPending) { |
| if (JSON.stringify([this._watchedPaths, this._ignoredFolders]) === JSON.stringify([watchedPaths, ignoredFolders])) |
| return; |
| if (reportPending) |
| this._reportEventsIfAny(); |
| this._watchedPaths = watchedPaths; |
| this._ignoredFolders = ignoredFolders; |
| void this._fsWatcher?.close(); |
| this._fsWatcher = void 0; |
| this._collector.length = 0; |
| clearTimeout(this._throttleTimer); |
| this._throttleTimer = void 0; |
| if (!this._watchedPaths.length) |
| return; |
| const ignored = [...this._ignoredFolders, "**/node_modules/**"]; |
| this._fsWatcher = chokidar.watch(watchedPaths, { ignoreInitial: true, ignored }).on("all", async (event, file) => { |
| if (this._throttleTimer) |
| clearTimeout(this._throttleTimer); |
| this._collector.push({ event, file }); |
| this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250); |
| }); |
| await new Promise((resolve, reject) => this._fsWatcher.once("ready", resolve).once("error", reject)); |
| } |
| async close() { |
| await this._fsWatcher?.close(); |
| } |
| _reportEventsIfAny() { |
| if (this._collector.length) |
| this._onChange(this._collector.slice()); |
| this._collector.length = 0; |
| } |
| }; |
|
|
| |
| var TeleReporterReceiver = class { |
| constructor(reporter, options = {}) { |
| this.isListing = false; |
| this._tests = 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 = new Map(); |
| } |
| _onStepBegin(testId, resultId, payload) { |
| const test = this._tests.get(testId); |
| const result = test.results.find((r) => r._id === resultId); |
| const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : void 0; |
| const location = this._absoluteLocation(payload.location); |
| const step = new TeleTestStep(payload, parentStep, location, result); |
| if (parentStep) |
| parentStep.steps.push(step); |
| else |
| result.steps.push(step); |
| result._stepMap.set(payload.id, step); |
| this._reporter.onStepBegin?.(test, result, step); |
| } |
| _onStepEnd(testId, resultId, payload) { |
| const test = this._tests.get(testId); |
| const result = test.results.find((r) => r._id === resultId); |
| const step = result._stepMap.get(payload.id); |
| step._endPayload = payload; |
| step.duration = payload.duration; |
| step.error = payload.error; |
| this._reporter.onStepEnd?.(test, result, step); |
| } |
| _onAttach(testId, resultId, attachments) { |
| const test = this._tests.get(testId); |
| const result = test.results.find((r) => r._id === resultId); |
| result.attachments.push(...attachments.map((a) => ({ |
| name: a.name, |
| contentType: a.contentType, |
| path: a.path, |
| body: a.base64 && globalThis.Buffer ? Buffer.from(a.base64, "base64") : void 0 |
| }))); |
| } |
| _onError(error, workerInfo) { |
| let fullWorkerInfo; |
| if (workerInfo) { |
| const project = this._config.projects.find((p) => p.name === workerInfo.projectName); |
| if (project) { |
| fullWorkerInfo = { |
| workerIndex: workerInfo.workerIndex, |
| parallelIndex: workerInfo.parallelIndex, |
| config: this._config, |
| project |
| }; |
| } |
| } |
| this._reporter.onError?.(error, fullWorkerInfo); |
| } |
| _onStdIO(type, testId, resultId, data, isBase64) { |
| const chunk = isBase64 ? globalThis.Buffer ? Buffer.from(data, "base64") : atob(data) : data; |
| const test = testId ? this._tests.get(testId) : void 0; |
| const result = test && resultId ? test.results.find((r) => r._id === resultId) : void 0; |
| if (type === "stdout") { |
| result?.stdout.push(chunk); |
| this._reporter.onStdOut?.(chunk, test, result); |
| } else { |
| result?.stderr.push(chunk); |
| this._reporter.onStdErr?.(chunk, test, result); |
| } |
| } |
| async _onEnd(result) { |
| await this._reporter.onEnd?.(asFullResult(result)); |
| } |
| _onExit() { |
| return this._reporter.onExit?.(); |
| } |
| _parseConfig(config2) { |
| const result = asFullConfig(config2); |
| if (this._options.configOverrides) { |
| result.configFile = this._options.configOverrides.configFile; |
| result.reportSlowTests = this._options.configOverrides.reportSlowTests; |
| result.quiet = this._options.configOverrides.quiet; |
| result.reporter = [...this._options.configOverrides.reporter]; |
| } |
| return result; |
| } |
| _parseProject(project) { |
| return { |
| metadata: project.metadata, |
| name: project.name, |
| outputDir: this._absolutePath(project.outputDir), |
| repeatEach: project.repeatEach, |
| retries: project.retries, |
| testDir: this._absolutePath(project.testDir), |
| testIgnore: parseRegexPatterns(project.testIgnore), |
| testMatch: parseRegexPatterns(project.testMatch), |
| timeout: project.timeout, |
| grep: parseRegexPatterns(project.grep), |
| grepInvert: parseRegexPatterns(project.grepInvert), |
| dependencies: project.dependencies, |
| teardown: project.teardown, |
| snapshotDir: this._absolutePath(project.snapshotDir), |
| ignoreSnapshots: project.ignoreSnapshots ?? false, |
| use: project.use |
| }; |
| } |
| _parseAttachments(attachments) { |
| return attachments.map((a) => { |
| return { |
| ...a, |
| body: a.base64 && globalThis.Buffer ? Buffer.from(a.base64, "base64") : void 0 |
| }; |
| }); |
| } |
| _mergeSuiteInto(jsonSuite, parent) { |
| let targetSuite = parent.suites.find((s) => s.title === jsonSuite.title); |
| if (!targetSuite) { |
| targetSuite = new TeleSuite(jsonSuite.title, parent.type === "project" ? "file" : "describe"); |
| parent._addSuite(targetSuite); |
| } |
| targetSuite.location = this._absoluteLocation(jsonSuite.location); |
| jsonSuite.entries.forEach((e) => { |
| if ("testId" in e) |
| this._mergeTestInto(e, targetSuite); |
| else |
| this._mergeSuiteInto(e, targetSuite); |
| }); |
| } |
| _mergeTestInto(jsonTest, parent) { |
| let targetTest = this._options.mergeTestCases ? parent.tests.find((s) => s.title === jsonTest.title && s.repeatEachIndex === jsonTest.repeatEachIndex) : void 0; |
| if (!targetTest) { |
| targetTest = new TeleTestCase(jsonTest.testId, jsonTest.title, this._absoluteLocation(jsonTest.location), jsonTest.repeatEachIndex); |
| parent._addTest(targetTest); |
| this._tests.set(targetTest.id, targetTest); |
| } |
| this._updateTest(jsonTest, targetTest); |
| } |
| _updateTest(payload, test) { |
| test.id = payload.testId; |
| test.location = this._absoluteLocation(payload.location); |
| test.retries = payload.retries; |
| test.tags = payload.tags ?? []; |
| test.annotations = payload.annotations ?? []; |
| this._absoluteAnnotationLocationsInplace(test.annotations); |
| return test; |
| } |
| _absoluteAnnotationLocationsInplace(annotations) { |
| for (const annotation of annotations) { |
| if (annotation.location) |
| annotation.location = this._absoluteLocation(annotation.location); |
| } |
| } |
| _absoluteLocation(location) { |
| if (!location) |
| return location; |
| return { |
| ...location, |
| file: this._absolutePath(location.file) |
| }; |
| } |
| _absolutePath(relativePath) { |
| if (relativePath === void 0) |
| return; |
| return this._options.resolvePath ? this._options.resolvePath(this._rootDir, relativePath) : this._rootDir + "/" + relativePath; |
| } |
| }; |
| var TeleSuite = class { |
| constructor(title, type) { |
| this._entries = []; |
| this._requireFile = ""; |
| this._parallelMode = "none"; |
| this.title = title; |
| this._type = type; |
| } |
| get type() { |
| return this._type; |
| } |
| get suites() { |
| return this._entries.filter((e) => e.type !== "test"); |
| } |
| get tests() { |
| return this._entries.filter((e) => e.type === "test"); |
| } |
| entries() { |
| return this._entries; |
| } |
| allTests() { |
| const result = []; |
| const visit = (suite) => { |
| for (const entry of suite.entries()) { |
| if (entry.type === "test") |
| result.push(entry); |
| else |
| visit(entry); |
| } |
| }; |
| visit(this); |
| return result; |
| } |
| titlePath() { |
| const titlePath = this.parent ? this.parent.titlePath() : []; |
| if (this.title || this._type !== "describe") |
| titlePath.push(this.title); |
| return titlePath; |
| } |
| project() { |
| return this._project ?? this.parent?.project(); |
| } |
| _addTest(test) { |
| test.parent = this; |
| this._entries.push(test); |
| } |
| _addSuite(suite) { |
| suite.parent = this; |
| this._entries.push(suite); |
| } |
| }; |
| var TeleTestCase = class { |
| constructor(id, title, location, repeatEachIndex) { |
| this.fn = () => { |
| }; |
| this.results = []; |
| this.type = "test"; |
| this.expectedStatus = "passed"; |
| this.timeout = 0; |
| this.annotations = []; |
| this.retries = 0; |
| this.tags = []; |
| this.repeatEachIndex = 0; |
| this.id = id; |
| this.title = title; |
| this.location = location; |
| this.repeatEachIndex = repeatEachIndex; |
| } |
| titlePath() { |
| const titlePath = this.parent ? this.parent.titlePath() : []; |
| titlePath.push(this.title); |
| return titlePath; |
| } |
| outcome() { |
| return computeTestCaseOutcome(this); |
| } |
| ok() { |
| const status = this.outcome(); |
| return status === "expected" || status === "flaky" || status === "skipped"; |
| } |
| _createTestResult(id) { |
| const result = new TeleTestResult(this.results.length, id); |
| this.results.push(result); |
| return result; |
| } |
| }; |
| var TeleTestStep = class { |
| constructor(payload, parentStep, location, result) { |
| this.duration = -1; |
| this.steps = []; |
| this._startTime = 0; |
| this.title = payload.title; |
| this.category = payload.category; |
| this.location = location; |
| this.parent = parentStep; |
| this._startTime = payload.startTime; |
| this._result = result; |
| } |
| titlePath() { |
| const parentPath = this.parent?.titlePath() || []; |
| return [...parentPath, this.title]; |
| } |
| get startTime() { |
| return new Date(this._startTime); |
| } |
| set startTime(value) { |
| this._startTime = +value; |
| } |
| get attachments() { |
| return this._endPayload?.attachments?.map((index) => this._result.attachments[index]) ?? []; |
| } |
| get annotations() { |
| return this._endPayload?.annotations ?? []; |
| } |
| }; |
| var TeleTestResult = class { |
| constructor(retry, id) { |
| this.parallelIndex = -1; |
| this.workerIndex = -1; |
| this.duration = -1; |
| this.stdout = []; |
| this.stderr = []; |
| this.attachments = []; |
| this.annotations = []; |
| this.status = "skipped"; |
| this.steps = []; |
| this.errors = []; |
| this._stepMap = new Map(); |
| this._startTime = 0; |
| this.retry = retry; |
| this._id = id; |
| } |
| setStartTimeNumber(startTime) { |
| this._startTime = startTime; |
| } |
| get startTime() { |
| return new Date(this._startTime); |
| } |
| set startTime(value) { |
| this._startTime = +value; |
| } |
| }; |
| var baseFullConfig = { |
| forbidOnly: false, |
| fullyParallel: false, |
| globalSetup: null, |
| globalTeardown: null, |
| globalTimeout: 0, |
| grep: /.*/, |
| grepInvert: null, |
| maxFailures: 0, |
| metadata: {}, |
| preserveOutput: "always", |
| projects: [], |
| reporter: [[process.env.CI ? "dot" : "list"]], |
| reportSlowTests: { |
| max: 5, |
| threshold: 3e5 |
| |
| }, |
| configFile: "", |
| rootDir: "", |
| quiet: false, |
| shard: null, |
| tags: [], |
| updateSnapshots: "missing", |
| updateSourceMethod: "patch", |
| version: "", |
| workers: 0, |
| webServer: null |
| }; |
| function serializeRegexPatterns(patterns) { |
| if (!Array.isArray(patterns)) |
| patterns = [patterns]; |
| return patterns.map((s) => { |
| if (typeof s === "string") |
| return { s }; |
| return { r: { source: s.source, flags: s.flags } }; |
| }); |
| } |
| function parseRegexPatterns(patterns) { |
| return patterns.map((p) => { |
| if (p.s !== void 0) |
| return p.s; |
| return new RegExp(p.r.source, p.r.flags); |
| }); |
| } |
| function computeTestCaseOutcome(test) { |
| let skipped = 0; |
| let didNotRun = 0; |
| let expected = 0; |
| let interrupted = 0; |
| let unexpected = 0; |
| for (const result of test.results) { |
| if (result.status === "interrupted") { |
| ++interrupted; |
| } else if (result.status === "skipped" && test.expectedStatus === "skipped") { |
| ++skipped; |
| } else if (result.status === "skipped") { |
| ++didNotRun; |
| } else if (result.status === test.expectedStatus) { |
| ++expected; |
| } else { |
| ++unexpected; |
| } |
| } |
| if (expected === 0 && unexpected === 0) |
| return "skipped"; |
| if (unexpected === 0) |
| return "expected"; |
| if (expected === 0 && skipped === 0) |
| return "unexpected"; |
| return "flaky"; |
| } |
| function asFullResult(result) { |
| return { |
| status: result.status, |
| startTime: new Date(result.startTime), |
| duration: result.duration |
| }; |
| } |
| function asFullConfig(config2) { |
| return { ...baseFullConfig, ...config2 }; |
| } |
|
|
| |
| 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", |
| |
| "%h", |
| |
| "%s", |
| |
| "%B", |
| |
| "%an", |
| |
| "%ae", |
| |
| "%at", |
| |
| "%cn", |
| |
| "%ce", |
| |
| "%ct", |
| |
| "" |
| |
| ]; |
| const output = await runGit(`git log -1 --pretty=format:"${tokens.join(separator2)}" && git rev-parse --abbrev-ref HEAD`, gitDir); |
| if (!output) |
| return void 0; |
| const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = output.split(separator2); |
| return { |
| shortHash, |
| hash, |
| subject, |
| body, |
| author: { |
| name: authorName, |
| email: authorEmail, |
| time: +authorTime * 1e3 |
| }, |
| committer: { |
| name: committerName, |
| email: committerEmail, |
| time: +committerTime * 1e3 |
| }, |
| branch: branch.trim() |
| }; |
| } |
| async function gitDiff(gitDir, ci) { |
| const diffLimit = 1e5; |
| if (ci?.prBaseHash) { |
| await runGit(`git fetch origin ${ci.prBaseHash} --depth=1 --no-auto-maintenance --no-auto-gc --no-tags --no-recurse-submodules`, gitDir); |
| const diff3 = await runGit(`git diff ${ci.prBaseHash} HEAD`, gitDir); |
| if (diff3) |
| return diff3.substring(0, diffLimit); |
| } |
| if (ci) |
| return; |
| const uncommitted = await runGit("git diff", gitDir); |
| if (uncommitted === void 0) { |
| return; |
| } |
| if (uncommitted) |
| return uncommitted.substring(0, diffLimit); |
| const diff2 = await runGit("git diff HEAD~1", gitDir); |
| return diff2?.substring(0, diffLimit); |
| } |
| async function runGit(command, cwd) { |
| debug(`running "${command}"`); |
| const start = monotonicTime(); |
| const result = await spawnAsync( |
| command, |
| [], |
| { stdio: "pipe", cwd, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true } |
| ); |
| if (monotonicTime() - start > GIT_OPERATIONS_TIMEOUT_MS) { |
| print(`timeout of ${GIT_OPERATIONS_TIMEOUT_MS}ms exceeded while running "${command}"`); |
| return; |
| } |
| if (result.code) |
| debug(`failure, code=${result.code} |
| |
| ${result.stderr}`); |
| else |
| debug(`success`); |
| return result.code ? void 0 : result.stdout.trim(); |
| } |
|
|
| |
| var import_net = __toESM(require("net")); |
| var import_path = __toESM(require("path")); |
| var colors = require("playwright-core/lib/utilsBundle").colors; |
| var debug2 = require("playwright-core/lib/utilsBundle").debug; |
| var { ManualPromise } = require("playwright-core/lib/coreBundle").iso; |
| var { monotonicTime: monotonicTime2 } = require("playwright-core/lib/coreBundle").iso; |
| var { raceAgainstDeadline } = require("playwright-core/lib/coreBundle").iso; |
| var { isURLAvailable } = require("playwright-core/lib/coreBundle").utils; |
| var { launchProcess } = require("playwright-core/lib/coreBundle").utils; |
| var DEFAULT_ENVIRONMENT_VARIABLES = { |
| "BROWSER": "none", |
| |
| "FORCE_COLOR": "1", |
| "DEBUG_COLORS": "1" |
| }; |
| var debugWebServer = debug2("pw:webserver"); |
| var WebServerPlugin = class { |
| constructor(options, checkPortOnly) { |
| this.name = "playwright:webserver"; |
| this._options = options; |
| this._checkPortOnly = checkPortOnly; |
| } |
| async setup(config2, configDir, reporter) { |
| this._reporter = reporter; |
| if (this._options.url) |
| this._isAvailableCallback = getIsAvailableFunction(this._options.url, this._checkPortOnly, !!this._options.ignoreHTTPSErrors, this._reporter.onStdErr?.bind(this._reporter)); |
| this._options.cwd = this._options.cwd ? import_path.default.resolve(configDir, this._options.cwd) : configDir; |
| try { |
| await this._startProcess(); |
| await this._waitForProcess(); |
| } catch (error) { |
| await this.teardown(); |
| throw error; |
| } |
| } |
| async teardown() { |
| debugWebServer(`Terminating the WebServer`); |
| await this._killProcess?.(); |
| debugWebServer(`Terminated the WebServer`); |
| } |
| async _startProcess() { |
| let processExitedReject = (error) => { |
| }; |
| this._processExitedPromise = new Promise((_, reject) => processExitedReject = reject); |
| const isAlreadyAvailable = await this._isAvailableCallback?.(); |
| if (isAlreadyAvailable) { |
| debugWebServer(`WebServer is already available`); |
| if (this._options.reuseExistingServer) |
| return; |
| const port = new URL(this._options.url).port; |
| throw new Error(`${this._options.url ?? `http://localhost${port ? ":" + port : ""}`} is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer.`); |
| } |
| if (!this._options.command) |
| throw new Error("config.webServer.command cannot be empty"); |
| debugWebServer(`Starting WebServer process ${this._options.command}...`); |
| const { launchedProcess, gracefullyClose } = await launchProcess({ |
| command: this._options.command, |
| env: { |
| ...DEFAULT_ENVIRONMENT_VARIABLES, |
| ...process.env, |
| ...this._options.env |
| }, |
| cwd: this._options.cwd, |
| stdio: "stdin", |
| shell: true, |
| attemptToGracefullyClose: async () => { |
| if (process.platform === "win32") |
| throw new Error("Graceful shutdown is not supported on Windows"); |
| if (!this._options.gracefulShutdown) |
| throw new Error("skip graceful shutdown"); |
| const { signal, timeout = 0 } = this._options.gracefulShutdown; |
| process.kill(-launchedProcess.pid, signal); |
| return new Promise((resolve, reject) => { |
| const timer = timeout !== 0 ? setTimeout(() => reject(new Error(`process didn't close gracefully within timeout`)), timeout) : void 0; |
| launchedProcess.once("close", (...args) => { |
| clearTimeout(timer); |
| resolve(); |
| }); |
| }); |
| }, |
| log: () => { |
| }, |
| onExit: (code) => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : "Process from config.webServer exited early.")), |
| tempDirectories: [] |
| }); |
| this._killProcess = gracefullyClose; |
| debugWebServer(`Process started`); |
| if (this._options.wait?.stdout || this._options.wait?.stderr) |
| this._waitForStdioPromise = new ManualPromise(); |
| const stdioWaitCollectors = { |
| stdout: this._options.wait?.stdout ? "" : void 0, |
| stderr: this._options.wait?.stderr ? "" : void 0 |
| }; |
| launchedProcess.stdout.on("data", (data) => { |
| if (debugWebServer.enabled || this._options.stdout === "pipe") |
| this._reporter.onStdOut?.(prefixOutputLines(data.toString(), this._options.name)); |
| }); |
| launchedProcess.stderr.on("data", (data) => { |
| if (debugWebServer.enabled || (this._options.stderr === "pipe" || !this._options.stderr)) |
| this._reporter.onStdErr?.(prefixOutputLines(data.toString(), this._options.name)); |
| }); |
| const resolveStdioPromise = () => { |
| stdioWaitCollectors.stdout = void 0; |
| stdioWaitCollectors.stderr = void 0; |
| this._waitForStdioPromise?.resolve(); |
| }; |
| for (const stdio of ["stdout", "stderr"]) { |
| launchedProcess[stdio].on("data", (data) => { |
| if (!this._options.wait?.[stdio] || stdioWaitCollectors[stdio] === void 0) |
| return; |
| stdioWaitCollectors[stdio] += data.toString(); |
| this._options.wait[stdio].lastIndex = 0; |
| const result = this._options.wait[stdio].exec(stdioWaitCollectors[stdio]); |
| if (result) { |
| for (const [key, value] of Object.entries(result.groups || {})) |
| process.env[key.toUpperCase()] = value; |
| resolveStdioPromise(); |
| } |
| }); |
| } |
| } |
| async _waitForProcess() { |
| if (!this._isAvailableCallback && !this._waitForStdioPromise) { |
| this._processExitedPromise.catch(() => { |
| }); |
| return; |
| } |
| debugWebServer(`Waiting for availability...`); |
| const launchTimeout = this._options.timeout || 60 * 1e3; |
| const cancellationToken = { canceled: false }; |
| const deadline = monotonicTime2() + launchTimeout; |
| const racingPromises = [this._processExitedPromise]; |
| if (this._isAvailableCallback) |
| racingPromises.push(raceAgainstDeadline(() => waitFor(this._isAvailableCallback, cancellationToken), deadline)); |
| if (this._waitForStdioPromise) |
| racingPromises.push(raceAgainstDeadline(() => this._waitForStdioPromise, deadline)); |
| const { timedOut } = await Promise.race(racingPromises); |
| cancellationToken.canceled = true; |
| if (timedOut) |
| throw new Error(`Timed out waiting ${launchTimeout}ms from config.webServer.`); |
| debugWebServer(`WebServer available`); |
| } |
| }; |
| async function isPortUsed(port) { |
| const innerIsPortUsed = (host) => new Promise((resolve) => { |
| const conn = import_net.default.connect(port, host).on("error", () => { |
| resolve(false); |
| }).on("connect", () => { |
| conn.end(); |
| resolve(true); |
| }); |
| }); |
| return new Promise((resolve) => { |
| let pending = 2; |
| const onResult = (result) => { |
| if (result) |
| resolve(true); |
| else if (--pending === 0) |
| resolve(false); |
| }; |
| void innerIsPortUsed("127.0.0.1").then(onResult); |
| void innerIsPortUsed("::1").then(onResult); |
| }); |
| } |
| async function waitFor(waitFn, cancellationToken) { |
| const logScale = [100, 250, 500]; |
| while (!cancellationToken.canceled) { |
| const connected = await waitFn(); |
| if (connected) |
| return; |
| const delay = logScale.shift() || 1e3; |
| debugWebServer(`Waiting ${delay}ms`); |
| await new Promise((x) => setTimeout(x, delay)); |
| } |
| } |
| function getIsAvailableFunction(url, checkPortOnly, ignoreHTTPSErrors, onStdErr) { |
| const urlObject = new URL(url); |
| if (!checkPortOnly) |
| return () => isURLAvailable(urlObject, ignoreHTTPSErrors, debugWebServer, onStdErr); |
| const port = urlObject.port; |
| return () => isPortUsed(+port); |
| } |
| var webServer = (options) => { |
| return new WebServerPlugin(options, false); |
| }; |
| var webServerPluginsForConfig = (config2) => { |
| const shouldSetBaseUrl = !!config2.config.webServer; |
| const webServerPlugins = []; |
| for (const webServerConfig of config2.webServers) { |
| if (webServerConfig.port && webServerConfig.url) |
| throw new Error(`Either 'port' or 'url' should be specified in config.webServer.`); |
| let url; |
| if (webServerConfig.port || webServerConfig.url) { |
| url = webServerConfig.url || `http://localhost:${webServerConfig.port}`; |
| if (shouldSetBaseUrl && !webServerConfig.url) |
| process.env.PLAYWRIGHT_TEST_BASE_URL = url; |
| } |
| webServerPlugins.push(new WebServerPlugin({ ...webServerConfig, url }, webServerConfig.port !== void 0)); |
| } |
| return webServerPlugins; |
| }; |
| function prefixOutputLines(output, prefixName = "WebServer") { |
| const lastIsNewLine = output[output.length - 1] === "\n"; |
| let lines = output.split("\n"); |
| if (lastIsNewLine) |
| lines.pop(); |
| lines = lines.map((line) => colors.dim(`[${prefixName}] `) + line); |
| if (lastIsNewLine) |
| lines.push(""); |
| return lines.join("\n"); |
| } |
|
|
| |
| var base_exports = {}; |
| __export(base_exports, { |
| TerminalReporter: () => TerminalReporter, |
| formatError: () => formatError, |
| formatFailure: () => formatFailure, |
| formatResultFailure: () => formatResultFailure, |
| formatRetry: () => formatRetry, |
| internalScreen: () => internalScreen, |
| kOutputSymbol: () => kOutputSymbol, |
| markErrorsAsReported: () => markErrorsAsReported, |
| nonTerminalScreen: () => nonTerminalScreen, |
| prepareErrorStack: () => prepareErrorStack, |
| relativeFilePath: () => relativeFilePath, |
| resolveOutputFile: () => resolveOutputFile, |
| separator: () => separator, |
| stepSuffix: () => stepSuffix, |
| terminalScreen: () => terminalScreen |
| }); |
| var import_path2 = __toESM(require("path")); |
| var import_util = require("../util"); |
| var realColors = require("playwright-core/lib/utilsBundle").colors; |
| var { noColors } = require("playwright-core/lib/coreBundle").iso; |
| var { msToString } = require("playwright-core/lib/coreBundle").iso; |
| var { parseErrorStack } = require("playwright-core/lib/coreBundle").iso; |
| var { getPackageManagerExecCommand } = require("playwright-core/lib/coreBundle").utils; |
| var { fitToWidth } = require("playwright-core/lib/coreBundle").utils; |
| var kOutputSymbol = Symbol("output"); |
| var DEFAULT_TTY_WIDTH = 100; |
| var DEFAULT_TTY_HEIGHT = 40; |
| var originalProcessStdout = process.stdout; |
| var originalProcessStderr = process.stderr; |
| var terminalScreen = (() => { |
| let isTTY = !!originalProcessStdout.isTTY; |
| let ttyWidth = originalProcessStdout.columns || 0; |
| let ttyHeight = originalProcessStdout.rows || 0; |
| if (process.env.PLAYWRIGHT_FORCE_TTY === "false" || process.env.PLAYWRIGHT_FORCE_TTY === "0") { |
| isTTY = false; |
| ttyWidth = 0; |
| ttyHeight = 0; |
| } else if (process.env.PLAYWRIGHT_FORCE_TTY === "true" || process.env.PLAYWRIGHT_FORCE_TTY === "1") { |
| isTTY = true; |
| ttyWidth = originalProcessStdout.columns || DEFAULT_TTY_WIDTH; |
| ttyHeight = originalProcessStdout.rows || DEFAULT_TTY_HEIGHT; |
| } else if (process.env.PLAYWRIGHT_FORCE_TTY) { |
| isTTY = true; |
| const sizeMatch = process.env.PLAYWRIGHT_FORCE_TTY.match(/^(\d+)x(\d+)$/); |
| if (sizeMatch) { |
| ttyWidth = +sizeMatch[1]; |
| ttyHeight = +sizeMatch[2]; |
| } else { |
| ttyWidth = +process.env.PLAYWRIGHT_FORCE_TTY; |
| ttyHeight = DEFAULT_TTY_HEIGHT; |
| } |
| if (isNaN(ttyWidth)) |
| ttyWidth = DEFAULT_TTY_WIDTH; |
| if (isNaN(ttyHeight)) |
| ttyHeight = DEFAULT_TTY_HEIGHT; |
| } |
| let useColors = isTTY; |
| if (process.env.DEBUG_COLORS === "0" || process.env.DEBUG_COLORS === "false" || process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") |
| useColors = false; |
| else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR) |
| useColors = true; |
| const colors7 = useColors ? realColors : noColors; |
| return { |
| resolveFiles: "cwd", |
| isTTY, |
| ttyWidth, |
| ttyHeight, |
| colors: colors7, |
| stdout: originalProcessStdout, |
| stderr: originalProcessStderr |
| }; |
| })(); |
| var nonTerminalScreen = { |
| colors: terminalScreen.colors, |
| isTTY: false, |
| ttyWidth: 0, |
| ttyHeight: 0, |
| resolveFiles: "rootDir" |
| }; |
| var internalScreen = { |
| colors: realColors, |
| isTTY: false, |
| ttyWidth: 0, |
| ttyHeight: 0, |
| resolveFiles: "rootDir" |
| }; |
| var TerminalReporter = class { |
| constructor(options = {}) { |
| this.totalTestCount = 0; |
| this.fileDurations = 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: new Set() }; |
| entry.duration += result.duration; |
| entry.workers.add(result.workerIndex); |
| this.fileDurations.set(fileAndProject, entry); |
| } |
| onError(error) { |
| this._fatalErrors.push(error); |
| } |
| async onEnd(result) { |
| this.result = result; |
| } |
| fitToScreen(line, prefix) { |
| if (!this.screen.ttyWidth) { |
| return line; |
| } |
| return fitToWidth(line, this.screen.ttyWidth, prefix); |
| } |
| generateStartingMessage() { |
| const jobs = this.config.metadata.actualWorkers ?? this.config.workers; |
| const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : ""; |
| if (!this.totalTestCount) |
| return ""; |
| return "\n" + this.screen.colors.dim("Running ") + this.totalTestCount + this.screen.colors.dim(` test${this.totalTestCount !== 1 ? "s" : ""} using `) + jobs + this.screen.colors.dim(` worker${jobs !== 1 ? "s" : ""}${shardDetails}`); |
| } |
| getSlowTests() { |
| if (!this.config.reportSlowTests) |
| return []; |
| const fileDurations = [...this.fileDurations.entries()].filter(([key, value]) => value.workers.size === 1).map(([key, value]) => [key, value.duration]); |
| fileDurations.sort((a, b) => b[1] - a[1]); |
| const count = Math.min(fileDurations.length, this.config.reportSlowTests.max || Number.POSITIVE_INFINITY); |
| const threshold = this.config.reportSlowTests.threshold; |
| return fileDurations.filter(([, duration]) => duration > threshold).slice(0, count); |
| } |
| generateSummaryMessage({ didNotRun, skipped, expected, interrupted, unexpected, flaky, fatalErrors }) { |
| const tokens = []; |
| if (unexpected.length) { |
| tokens.push(this.screen.colors.red(` ${unexpected.length} failed`)); |
| for (const test of unexpected) |
| tokens.push(this.screen.colors.red(this.formatTestHeader(test, { indent: " " }))); |
| } |
| if (interrupted.length) { |
| tokens.push(this.screen.colors.yellow(` ${interrupted.length} interrupted`)); |
| for (const test of interrupted) |
| tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " }))); |
| } |
| if (flaky.length) { |
| tokens.push(this.screen.colors.yellow(` ${flaky.length} flaky`)); |
| for (const test of flaky) |
| tokens.push(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " }))); |
| } |
| if (skipped) |
| tokens.push(this.screen.colors.yellow(` ${skipped} skipped`)); |
| if (didNotRun) |
| tokens.push(this.screen.colors.yellow(` ${didNotRun} did not run`)); |
| if (expected) |
| tokens.push(this.screen.colors.green(` ${expected} passed`) + this.screen.colors.dim(` (${msToString(this.result.duration)})`)); |
| if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0) |
| tokens.push(this.screen.colors.red(` ${fatalErrors.length === 1 ? "1 error was not a part of any test" : fatalErrors.length + " errors were not a part of any test"}, see above for details`)); |
| return tokens.join("\n"); |
| } |
| generateSummary() { |
| let didNotRun = 0; |
| let skipped = 0; |
| let expected = 0; |
| const interrupted = []; |
| const interruptedToPrint = []; |
| const unexpected = []; |
| const flaky = []; |
| this.suite.allTests().forEach((test) => { |
| switch (test.outcome()) { |
| case "skipped": { |
| if (test.results.some((result) => result.status === "interrupted")) { |
| if (test.results.some((result) => !!result.error)) |
| interruptedToPrint.push(test); |
| interrupted.push(test); |
| } else if (!test.results.length || test.expectedStatus !== "skipped") { |
| ++didNotRun; |
| } else { |
| ++skipped; |
| } |
| break; |
| } |
| case "expected": |
| ++expected; |
| break; |
| case "unexpected": |
| unexpected.push(test); |
| break; |
| case "flaky": |
| flaky.push(test); |
| break; |
| } |
| }); |
| const failuresToPrint = [...unexpected, ...flaky, ...interruptedToPrint]; |
| return { |
| didNotRun, |
| skipped, |
| expected, |
| interrupted, |
| unexpected, |
| flaky, |
| failuresToPrint, |
| fatalErrors: this._fatalErrors |
| }; |
| } |
| epilogue(full) { |
| const summary = this.generateSummary(); |
| const summaryMessage = this.generateSummaryMessage(summary); |
| if (full && summary.failuresToPrint.length && !this._options.omitFailures) |
| this._printFailures(summary.failuresToPrint); |
| this._printSlowTests(); |
| this._printSummary(summaryMessage); |
| } |
| _printFailures(failures) { |
| this.writeLine(""); |
| failures.forEach((test, index) => { |
| this.writeLine(this.formatFailure(test, index + 1)); |
| }); |
| } |
| _printSlowTests() { |
| const slowTests = this.getSlowTests(); |
| slowTests.forEach(([file, duration]) => { |
| this.writeLine(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${msToString(duration)})`)); |
| }); |
| if (slowTests.length) |
| this.writeLine(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel")); |
| } |
| _printSummary(summary) { |
| if (summary.trim()) |
| this.writeLine(summary); |
| } |
| willRetry(test) { |
| return test.outcome() === "unexpected" && test.results.length <= test.retries; |
| } |
| formatTestTitle(test, step) { |
| return formatTestTitle(this.screen, this.config, test, step, this._options); |
| } |
| formatTestHeader(test, options = {}) { |
| return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId }); |
| } |
| formatFailure(test, index) { |
| return formatFailure(this.screen, this.config, test, index, this._options); |
| } |
| formatError(error) { |
| return formatError(this.screen, error); |
| } |
| formatResultErrors(test, result) { |
| return formatResultErrors(this.screen, test, result); |
| } |
| writeLine(line) { |
| this.screen.stdout?.write(line ? line + "\n" : "\n"); |
| } |
| }; |
| function formatResultErrors(screen, test, result) { |
| const lines = []; |
| if (test.outcome() === "unexpected") { |
| const errorDetails = formatResultFailure(screen, test, result, " "); |
| if (errorDetails.length > 0) |
| lines.push(""); |
| for (const error of errorDetails) |
| lines.push(error.message, ""); |
| } |
| return lines.join("\n"); |
| } |
| function formatFailure(screen, config2, test, index, options) { |
| const lines = []; |
| let printedHeader = false; |
| for (const result of test.results) { |
| const resultLines = []; |
| const errors = formatResultFailure(screen, test, result, " "); |
| if (!errors.length) |
| continue; |
| if (!printedHeader) { |
| const header = formatTestHeader(screen, config2, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId }); |
| lines.push(screen.colors.red(header)); |
| printedHeader = true; |
| } |
| if (result.retry) { |
| resultLines.push(""); |
| resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`))); |
| } |
| resultLines.push(...errors.map((error) => "\n" + error.message)); |
| const attachmentGroups = groupAttachments(result.attachments); |
| for (let i = 0; i < attachmentGroups.length; ++i) { |
| const attachment = attachmentGroups[i]; |
| if (attachment.name === "error-context" && attachment.path) { |
| resultLines.push(""); |
| resultLines.push(screen.colors.dim(` Error Context: ${relativeFilePath(screen, config2, attachment.path)}`)); |
| continue; |
| } |
| if (attachment.name.startsWith("_")) |
| continue; |
| const hasPrintableContent = attachment.contentType.startsWith("text/"); |
| if (!attachment.path && !hasPrintableContent) |
| continue; |
| resultLines.push(""); |
| resultLines.push(screen.colors.dim(separator(screen, ` attachment #${i + 1}: ${screen.colors.bold(attachment.name)} (${attachment.contentType})`))); |
| if (attachment.actual?.path) { |
| if (attachment.expected?.path) { |
| const expectedPath = relativeFilePath(screen, config2, attachment.expected.path); |
| resultLines.push(screen.colors.dim(` Expected: ${expectedPath}`)); |
| } |
| const actualPath = relativeFilePath(screen, config2, attachment.actual.path); |
| resultLines.push(screen.colors.dim(` Received: ${actualPath}`)); |
| if (attachment.previous?.path) { |
| const previousPath = relativeFilePath(screen, config2, attachment.previous.path); |
| resultLines.push(screen.colors.dim(` Previous: ${previousPath}`)); |
| } |
| if (attachment.diff?.path) { |
| const diffPath = relativeFilePath(screen, config2, attachment.diff.path); |
| resultLines.push(screen.colors.dim(` Diff: ${diffPath}`)); |
| } |
| } else if (attachment.path) { |
| const relativePath = relativeFilePath(screen, config2, attachment.path); |
| resultLines.push(screen.colors.dim(` ${relativePath}`)); |
| if (attachment.name === "trace") { |
| const packageManagerCommand = getPackageManagerExecCommand(); |
| resultLines.push(screen.colors.dim(` Usage:`)); |
| resultLines.push(""); |
| resultLines.push(screen.colors.dim(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`)); |
| resultLines.push(""); |
| } |
| } else { |
| if (attachment.contentType.startsWith("text/") && attachment.body) { |
| let text = attachment.body.toString(); |
| if (text.length > 300) |
| text = text.slice(0, 300) + "..."; |
| for (const line of text.split("\n")) |
| resultLines.push(screen.colors.dim(` ${line}`)); |
| } |
| } |
| resultLines.push(screen.colors.dim(separator(screen, " "))); |
| } |
| lines.push(...resultLines); |
| } |
| lines.push(""); |
| return lines.join("\n"); |
| } |
| function formatRetry(screen, result) { |
| const retryLines = []; |
| if (result.retry) { |
| retryLines.push(""); |
| retryLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`))); |
| } |
| return retryLines; |
| } |
| function quotePathIfNeeded(path20) { |
| if (/\s/.test(path20)) |
| return `"${path20}"`; |
| return path20; |
| } |
| var kReportedSymbol = Symbol("reported"); |
| function markErrorsAsReported(result) { |
| result[kReportedSymbol] = result.errors.length; |
| } |
| function formatResultFailure(screen, test, result, initialIndent) { |
| const errorDetails = []; |
| if (result.status === "passed" && test.expectedStatus === "failed") { |
| errorDetails.push({ |
| message: indent(screen.colors.red(`Expected to fail, but passed.`), initialIndent) |
| }); |
| } |
| if (result.status === "interrupted") { |
| errorDetails.push({ |
| message: indent(screen.colors.red(`Test was interrupted.`), initialIndent) |
| }); |
| } |
| const reportedIndex = result[kReportedSymbol] || 0; |
| for (const error of result.errors.slice(reportedIndex)) { |
| const formattedError = formatError(screen, error); |
| errorDetails.push({ |
| message: indent(formattedError.message, initialIndent), |
| location: formattedError.location |
| }); |
| } |
| return errorDetails; |
| } |
| function relativeFilePath(screen, config2, file) { |
| if (screen.resolveFiles === "cwd") |
| return import_path2.default.relative(process.cwd(), file); |
| return import_path2.default.relative(config2.rootDir, file); |
| } |
| function relativeTestPath(screen, config2, test) { |
| return relativeFilePath(screen, config2, test.location.file); |
| } |
| function stepSuffix(step) { |
| const stepTitles = step ? step.titlePath() : []; |
| return stepTitles.map((t2) => t2.split("\n")[0]).map((t2) => " \u203A " + t2).join(""); |
| } |
| function formatTestTitle(screen, config2, test, step, options = {}) { |
| const [, projectName, , ...titles] = test.titlePath(); |
| const location = `${relativeTestPath(screen, config2, test)}:${test.location.line}:${test.location.column}`; |
| const testId = options.includeTestId ? `[id=${test.id}] ` : ""; |
| const projectLabel = options.includeTestId ? `project=` : ""; |
| const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : ""; |
| const testTitle = `${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`; |
| const extraTags = 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 = new Set(); |
| for (const result of test.results.filter((r) => !!r.errors.length)) { |
| const stepPath = []; |
| const visit = (steps) => { |
| const errors = steps.filter((s) => s.error); |
| if (errors.length > 1) |
| return; |
| if (errors.length === 1 && errors[0].category === "test.step") { |
| stepPath.push(errors[0].title); |
| visit(errors[0].steps); |
| } |
| }; |
| visit(result.steps); |
| stepPaths.add(["", ...stepPath].join(" \u203A ")); |
| } |
| fullHeader = header + (stepPaths.size === 1 ? stepPaths.values().next().value : ""); |
| } |
| return separator(screen, fullHeader); |
| } |
| function formatError(screen, error) { |
| const message = error.message || error.value || ""; |
| const stack = error.stack; |
| if (!stack && !error.location) |
| return { message }; |
| const tokens = []; |
| const parsedStack = stack ? prepareErrorStack(stack) : void 0; |
| tokens.push(parsedStack?.message || message); |
| if (error.snippet) { |
| let snippet = error.snippet; |
| if (!screen.colors.enabled) |
| snippet = (0, import_util.stripAnsiEscapes)(snippet); |
| tokens.push(""); |
| tokens.push(snippet); |
| } |
| if (parsedStack && parsedStack.stackLines.length) |
| tokens.push(screen.colors.dim(parsedStack.stackLines.join("\n"))); |
| let location = error.location; |
| if (parsedStack && !location) |
| location = parsedStack.location; |
| if (error.cause) |
| tokens.push(screen.colors.dim("[cause]: ") + formatError(screen, error.cause).message); |
| return { |
| location, |
| message: tokens.join("\n") |
| }; |
| } |
| function separator(screen, text = "") { |
| if (text) |
| text += " "; |
| const columns = Math.min(100, screen.ttyWidth || 100); |
| return text + screen.colors.dim("\u2500".repeat(Math.max(0, columns - (0, import_util.stripAnsiEscapes)(text).length))); |
| } |
| function indent(lines, tab) { |
| return lines.replace(/^(?=.+$)/gm, tab); |
| } |
| function prepareErrorStack(stack) { |
| return parseErrorStack(stack, import_path2.default.sep, !!process.env.PWDEBUGIMPL); |
| } |
| 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_util.resolveReporterOutputPath)(options.default.outputDir, options.configDir, void 0); |
| if (!outputDir) |
| outputDir = options.configDir; |
| const reportName = process.env[`PLAYWRIGHT_${name}_OUTPUT_NAME`] ?? options.fileName ?? options.default?.fileName; |
| if (!reportName) |
| return void 0; |
| outputFile = import_path2.default.resolve(outputDir, reportName); |
| return { outputFile, outputDir }; |
| } |
| function groupAttachments(attachments) { |
| const result = []; |
| const attachmentsByPrefix = 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; |
| } |
|
|
| |
| var import_fs = __toESM(require("fs")); |
|
|
| |
| var Multiplexer = class { |
| constructor(reporters) { |
| this._reporters = reporters; |
| } |
| version() { |
| return "v2"; |
| } |
| onConfigure(config2) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onConfigure?.(config2)); |
| } |
| onBegin(suite) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onBegin?.(suite)); |
| } |
| onTestBegin(test, result) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onTestBegin?.(test, result)); |
| } |
| onStdOut(chunk, test, result) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onStdOut?.(chunk, test, result)); |
| } |
| onStdErr(chunk, test, result) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onStdErr?.(chunk, test, result)); |
| } |
| async onTestPaused(test, result) { |
| for (const reporter of this._reporters) |
| await wrapAsync(() => reporter.onTestPaused?.(test, result)); |
| } |
| onTestEnd(test, result) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onTestEnd?.(test, result)); |
| } |
| onReportConfigure(params) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onReportConfigure?.(params)); |
| } |
| onReportEnd(params) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onReportEnd?.(params)); |
| } |
| async onEnd(result) { |
| for (const reporter of this._reporters) { |
| const outResult = await wrapAsync(() => reporter.onEnd?.(result)); |
| if (outResult?.status) |
| result.status = outResult.status; |
| } |
| return result; |
| } |
| async onExit() { |
| for (const reporter of this._reporters) |
| await wrapAsync(() => reporter.onExit?.()); |
| } |
| onError(error, workerInfo) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onError?.(error, workerInfo)); |
| } |
| onStepBegin(test, result, step) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onStepBegin?.(test, result, step)); |
| } |
| onStepEnd(test, result, step) { |
| for (const reporter of this._reporters) |
| wrap(() => reporter.onStepEnd?.(test, result, step)); |
| } |
| printsToStdio() { |
| return this._reporters.some((r) => { |
| let prints = false; |
| wrap(() => prints = r.printsToStdio ? r.printsToStdio() : true); |
| return prints; |
| }); |
| } |
| }; |
| async function wrapAsync(callback) { |
| try { |
| return await callback(); |
| } catch (e) { |
| console.error("Error in reporter", e); |
| } |
| } |
| function wrap(callback) { |
| try { |
| callback(); |
| } catch (e) { |
| console.error("Error in reporter", e); |
| } |
| } |
|
|
| |
| var import_common = require("../common"); |
| var babel = __toESM(require("../transform/babelBundle")); |
|
|
| |
| 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; |
| } |
| 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; |
| } |
| }; |
|
|
| |
| 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"; |
| } |
| onConfigure(config2) { |
| this._config = config2; |
| this._startTime = new Date(); |
| this._monotonicStartTime = monotonicTime3(); |
| this._reporter.onConfigure?.(config2); |
| } |
| 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 ? 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) { |
| } |
| } |
|
|
| |
| var import_util13 = require("../util"); |
|
|
| |
| var reporters_exports = {}; |
| __export(reporters_exports, { |
| createErrorCollectingReporter: () => createErrorCollectingReporter, |
| createReporters: () => createReporters |
| }); |
| var import_fs8 = __toESM(require("fs")); |
|
|
| |
| var import_path4 = __toESM(require("path")); |
| var import_fs3 = __toESM(require("fs")); |
|
|
| |
| 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 = new Map(); |
| this._producedEnv = {}; |
| this._requestHandlers = 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, { |
| |
| |
| |
| 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); |
| const result = await raceAgainstDeadline2(() => exitPromise, monotonicTime4() + timeout); |
| if (result.timedOut) { |
| this.emit("processError", { message: `Error: ${this._processName} process did not exit within ${timeout}ms after stop, force-killed it` }); |
| this._forceKill(); |
| await exitPromise; |
| } |
| } |
| _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); |
| } |
| }; |
|
|
| |
| 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.esm.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(); |
| } |
| }; |
|
|
| |
| var import_util4 = require("../util"); |
|
|
| |
| 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_util2 = require("util"); |
| var import_util3 = require("../util"); |
| var minimatch = require("playwright-core/lib/utilsBundle").minimatch; |
| var { escapeRegExp } = require("playwright-core/lib/coreBundle").iso; |
| var readFileAsync = (0, import_util2.promisify)(import_fs2.default.readFile); |
| var readDirAsync = (0, import_util2.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 = new Set(); |
| const unmatchedProjectNames = new Map(); |
| const patterns = 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 = 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 = 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 = 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 = new Map()) { |
| const extensions = 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_util3.createFileMatcher)(project.project.testMatch); |
| const testIgnore = (0, import_util3.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; |
| } |
|
|
| |
| function createTestGroups(projectSuite, expectedParallelism) { |
| const groups = new Map(); |
| const createGroup = (test) => { |
| return { |
| workerHash: test._workerHash, |
| requireFile: test._requireFile, |
| repeatEachIndex: test.repeatEachIndex, |
| projectId: test._projectId, |
| tests: [] |
| }; |
| }; |
| for (const test of projectSuite.allTests()) { |
| let withWorkerHash = groups.get(test._workerHash); |
| if (!withWorkerHash) { |
| withWorkerHash = new Map(); |
| groups.set(test._workerHash, withWorkerHash); |
| } |
| let withRequireFile = withWorkerHash.get(test._requireFile); |
| if (!withRequireFile) { |
| withRequireFile = { |
| general: createGroup(test), |
| parallel: 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); |
| } |
| } |
| } |
| 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 = new Set(); |
| for (const group of testGroups) { |
| if (current >= from && current < to) |
| result.add(group); |
| current += group.tests.length; |
| } |
| return result; |
| } |
|
|
| |
| var import_common3 = 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 = new Map(); |
| const sourceMapCache = new Map(); |
| const allFilesForProject = new Map(); |
| for (const project of testRun.filteredProjects) { |
| const files = await collectFilesForProject(project, fsCache); |
| allFilesForProject.set(project, files); |
| } |
| const filesToRunByProject = 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 = new Map(); |
| } |
| async function loadFileSuites(testRun, mode, errors) { |
| const config2 = testRun.config; |
| const allTestFiles = new Set(); |
| for (const files of testRun.projectFiles.values()) |
| files.forEach((file) => allTestFiles.add(file)); |
| const fileSuiteByFile = 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_common3.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_common3.test.Suite("", "root"); |
| const projectSuites = new Map(); |
| const filteredProjectSuites = 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_common3.test.Suite("", "root"); |
| for (const filteredProjectSuite of filteredProjectSuites.values()) |
| filteredRoot._addSuite(filteredProjectSuite); |
| import_common3.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)); |
| } |
| } |
| if (config2.config.shard) { |
| 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 = new Set(); |
| for (const group of testGroupsInThisShard) { |
| for (const test of group.tests) |
| testsInThisShard.add(test); |
| } |
| import_common3.suiteUtils.filterTestsRemoveEmptySuites(rootSuite, (test) => testsInThisShard.has(test)); |
| } |
| if (testRun.postShardTestFilters.length) |
| import_common3.suiteUtils.filterTestsRemoveEmptySuites(rootSuite, (test) => testRun.postShardTestFilters.every((filter) => filter(test))); |
| const topLevelProjects = []; |
| { |
| const projectClosure2 = new Map(buildProjectsClosure(rootSuite.suites.map((suite) => suite._fullProject))); |
| for (const [project, level] of projectClosure2.entries()) { |
| if (level === "dependency") |
| rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project))); |
| else |
| topLevelProjects.push(project); |
| } |
| } |
| testRun.rootSuite = rootSuite; |
| testRun.topLevelProjects = topLevelProjects; |
| } |
| function createProjectSuite(project, fileSuites) { |
| const projectSuite = new import_common3.test.Suite(project.project.name, "project"); |
| for (const fileSuite of fileSuites) |
| projectSuite._addSuite(import_common3.suiteUtils.bindFileSuiteToProject(project, fileSuite)); |
| const grepMatcher = (0, import_util4.createTitleMatcher)(project.project.grep); |
| const grepInvertMatcher = project.project.grepInvert ? (0, import_util4.createTitleMatcher)(project.project.grepInvert) : null; |
| import_common3.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_common3.suiteUtils.filterTestsRemoveEmptySuites(result, (test) => testFilters.every((filter) => filter(test))); |
| return result; |
| } |
| function buildProjectSuite(project, projectSuite) { |
| const result = new import_common3.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_common3.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 = 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_common3.transform.requireOrImport(file); |
| if (func && typeof func === "object" && "default" in func) |
| func = func["default"]; |
| if (typeof func !== "function") |
| throw (0, import_util4.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_util4.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_util4.errorWithFile)(filePath, "Cannot read test list file: " + e.message); |
| } |
| } |
|
|
| |
| var import_fs4 = __toESM(require("fs")); |
| var import_path6 = __toESM(require("path")); |
| var import_stream = require("stream"); |
| var import_coreBundle = require("playwright-core/lib/coreBundle"); |
|
|
| |
| var import_path5 = __toESM(require("path")); |
| var { createGuid } = require("playwright-core/lib/coreBundle").utils; |
| var TeleReporterEmitter = class { |
| constructor(messageSink, options = {}) { |
| this._resultKnownAttachmentCounts = new Map(); |
| this._resultKnownErrorCounts = new Map(); |
| |
| |
| this._idSymbol = 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 |
| }; |
| } |
| _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, |
| |
| 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, |
| category: step.category, |
| 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); |
| } |
| }; |
|
|
| |
| 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)(), |
| |
| 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_stream.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 |
| }; |
| }); |
| } |
| }; |
|
|
| |
| var DotReporter = class extends TerminalReporter { |
| constructor() { |
| super(...arguments); |
| 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; |
|
|
| |
| var EmptyReporter = class { |
| version() { |
| return "v2"; |
| } |
| printsToStdio() { |
| return false; |
| } |
| }; |
| var empty_default = EmptyReporter; |
|
|
| |
| var import_path7 = __toESM(require("path")); |
| var import_util5 = require("../util"); |
| var { noColors: noColors2 } = require("playwright-core/lib/coreBundle").iso; |
| var { msToString: msToString2 } = require("playwright-core/lib/coreBundle").iso; |
| var GitHubLogger = class { |
| _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_util5.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); |
| 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++; |
| 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; |
|
|
| |
| 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_stream2 = require("stream"); |
| var babel2 = __toESM(require("../transform/babelBundle")); |
| var import_util6 = 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 = 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 = 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_util6.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 builder = new HtmlBuilder(yazl2, this.config, this._outputFolder, this._attachmentsBaseURL, doNotInlineAssets, { |
| title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, |
| noSnippets, |
| noCopyPrompt |
| }); |
| 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_util6.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 = 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((f) => { |
| this._dataZipFile.end(void 0, () => { |
| this._dataZipFile.outputStream.pipe(new Base64Encoder()).pipe(import_fs5.default.createWriteStream(filePath, { flags: "a" })).on("close", f); |
| }); |
| }); |
| 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, |
| |
| 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, |
| |
| 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_util6.stripAnsiEscapes)(a.body); |
| return null; |
| } |
| a.body = (0, import_util6.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, |
| 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_stream2.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.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_util6.stripAnsiEscapes)(message).split("\n")[0] || void 0 |
| } |
| ); |
| } |
| function writeLine(line) { |
| process.stdout.write(line + "\n"); |
| } |
| var html_default = HtmlReporter; |
|
|
| |
| var import_fs6 = __toESM(require("fs")); |
| var import_path9 = __toESM(require("path")); |
| var import_common4 = 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_common4.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_common4.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)), |
| |
| 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, |
| 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; |
|
|
| |
| var import_fs7 = __toESM(require("fs")); |
| var import_path10 = __toESM(require("path")); |
| var import_util7 = require("../util"); |
| var { getAsBooleanFromENV } = 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.stripANSIControlSequences = getAsBooleanFromENV("PLAYWRIGHT_JUNIT_STRIP_ANSI", !!options.stripANSIControlSequences); |
| this.includeProjectInTestName = getAsBooleanFromENV("PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME", !!options.includeProjectInTestName); |
| this.includeRetries = getAsBooleanFromENV("PLAYWRIGHT_JUNIT_INCLUDE_RETRIES", !!options.includeRetries); |
| 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 = 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: { |
| |
| name: namePrefix + test.titlePath().slice(3).join(" \u203A "), |
| |
| 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_util7.stripAnsiEscapes)(formatFailure(nonTerminalScreen, this.config, test)) |
| }); |
| 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_util7.stripAnsiEscapes)(formatFailure(nonTerminalScreen, this.config, test)) |
| }); |
| 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_util7.stripAnsiEscapes)(stackTrace) }); |
| await this._appendStdIO(entry, [result]); |
| return entry; |
| } |
| }; |
| function classifyResultError(result) { |
| const error = result.error; |
| if (!error) |
| return null; |
| const rawMessage = (0, import_util7.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_util7.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; |
|
|
| |
| var LineReporter = class extends TerminalReporter { |
| constructor() { |
| super(...arguments); |
| 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; |
|
|
| |
| var import_util8 = require("../util"); |
| var { msToString: msToString3 } = require("playwright-core/lib/coreBundle").iso; |
| var { getAsBooleanFromENV: getAsBooleanFromENV2 } = 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); |
| this._lastRow = 0; |
| this._lastColumn = 0; |
| this._testRows = new Map(); |
| this._stepRows = new Map(); |
| this._resultIndex = new Map(); |
| this._stepIndex = new Map(); |
| this._needNewLine = false; |
| this._paused = new Set(); |
| this._printSteps = getAsBooleanFromENV2("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps); |
| } |
| 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); |
| } |
| _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_util8.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(true); |
| } |
| }; |
| var lastStepOrdinalSymbol = Symbol("lastStepOrdinal"); |
| var list_default = ListReporter; |
|
|
| |
| 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 = 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; |
|
|
| |
| var { calculateSha1: calculateSha13 } = require("playwright-core/lib/coreBundle").utils; |
| async function createReporters(config2, mode, descriptions, runOptions) { |
| const defaultReporters = { |
| blob: BlobReporter, |
| 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; |
| if (runOptions?.additionalReporters) |
| descriptions = [...descriptions, ...runOptions.additionalReporters]; |
| 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_fs8.default.readFileSync(runOptions.testList)); |
| if (runOptions?.testListInvert) |
| command.testListInvert = calculateSha13(import_fs8.default.readFileSync(runOptions.testListInvert)); |
| if (Object.keys(command).length) |
| parts.push(calculateSha13(JSON.stringify(command)).substring(0, 7)); |
| return parts.join("-"); |
| } |
|
|
| |
| var import_fs11 = __toESM(require("fs")); |
| var import_path15 = __toESM(require("path")); |
| var import_util11 = require("util"); |
|
|
| |
| var import_fs9 = __toESM(require("fs")); |
| var import_path12 = __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 = new Map(); |
| const patchFile = import_path12.default.join(project.project.outputDir, "rebaselines.patch"); |
| for (const fileName of [...suggestedRebaselines.keys()].sort()) { |
| const source = await import_fs9.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_path12.default.relative(process.cwd(), fileName); |
| files.push(relativeName); |
| if (config2.config.updateSourceMethod === "overwrite") { |
| await import_fs9.default.promises.writeFile(fileName, result); |
| } else if (config2.config.updateSourceMethod === "3way") { |
| await import_fs9.default.promises.writeFile(fileName, applyPatchWithConflictMarkers(source, result)); |
| } else { |
| const gitFolder = findGitRoot(import_path12.default.dirname(fileName), gitCache); |
| const relativeToGit = import_path12.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_fs9.default.promises.mkdir(import_path12.default.dirname(patchFile), { recursive: true }); |
| await import_fs9.default.promises.writeFile(patchFile, patches.join("\n")); |
| reporter.onStdErr(` |
| ` + colors3.cyan("git apply " + import_path12.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_path12.default.join(dir, ".git"); |
| if (import_fs9.default.existsSync(gitPath) && import_fs9.default.lstatSync(gitPath).isDirectory()) { |
| cache.set(dir, dir); |
| return dir; |
| } |
| const parentDir = import_path12.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; |
| } |
|
|
| |
| var import_fs10 = __toESM(require("fs")); |
| var import_path13 = __toESM(require("path")); |
| var import_common5 = require("../common"); |
|
|
| |
| function artifactsFolderName(workerIndex) { |
| return `.playwright-artifacts-${workerIndex}`; |
| } |
|
|
| |
| 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_path13.default.join(options.outputDir, artifactsFolderName(workerIndex)), |
| pauseOnError: options.pauseOnError, |
| pauseAtEnd: options.pauseAtEnd |
| }; |
| } |
| async start() { |
| await import_fs10.default.promises.mkdir(this._params.artifactsDir, { recursive: true }); |
| return await this.startRunner(this._params, { |
| onStdOut: (chunk) => this.emit("stdOut", import_common5.ipc.stdioChunkToParams(chunk)), |
| onStdErr: (chunk) => this.emit("stdErr", import_common5.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; |
| } |
| }; |
|
|
| |
| var import_common6 = require("../common"); |
| var import_util9 = 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) { |
| |
| this._workerSlots = []; |
| this._queue = []; |
| this._workerLimitPerProjectId = new Map(); |
| this._queuedOrRunningHashCount = new Map(); |
| this._finished = new ManualPromise3(); |
| this._isStopped = true; |
| this._extraEnvByProjectId = new Map(); |
| this._producedEnvByProjectId = new Map(); |
| this._testRun = testRun; |
| for (const project of testRun.config.projects) { |
| if (project.workers) |
| this._workerLimitPerProjectId.set(project.id, project.workers); |
| } |
| } |
| _findFirstJobToRun() { |
| for (let index = 0; index < this._queue.length; index++) { |
| const job = this._queue[index]; |
| 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; |
| } |
| _scheduleJob() { |
| if (this._isStopped) |
| return; |
| const jobIndex = this._findFirstJobToRun(); |
| if (jobIndex === -1) |
| return; |
| 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; |
| } |
| this._queue.splice(jobIndex, 1); |
| const jobDispatcher = new JobDispatcher(job, this._testRun, () => this.stop().catch(() => { |
| })); |
| this._workerSlots[workerIndex].jobDispatcher = jobDispatcher; |
| void this._runJobInWorker(workerIndex, jobDispatcher).then(() => { |
| this._workerSlots[workerIndex].jobDispatcher = void 0; |
| this._checkFinished(); |
| this._scheduleJob(); |
| }); |
| } |
| 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_common6.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 |
| |
| ); |
| else if (this._isWorkerRedundant(worker)) |
| void worker.stop(); |
| if (!this._isStopped && result.newJob) { |
| this._queue.unshift(result.newJob); |
| this._updateCounterForWorkerHash(result.newJob.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._testRun.hasReachedMaxFailures()) |
| void this.stop(); |
| for (let i = 0; i < this._testRun.config.config.workers; i++) |
| this._workerSlots.push({}); |
| for (let i = 0; i < this._workerSlots.length; i++) |
| this._scheduleJob(); |
| 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, stopCallback) { |
| this.jobResult = new ManualPromise3(); |
| this._listeners = []; |
| this._failedTests = new Set(); |
| this._failedWithNonRetriableError = new Set(); |
| this._remainingByTestId = new Map(); |
| this._dataByTestId = new Map(); |
| this._parallelIndex = 0; |
| this._workerIndex = 0; |
| this.job = job; |
| this._testRun = testRun; |
| this._stopCallback = stopCallback; |
| this._remainingByTestId = new Map(this.job.tests.map((e) => [e.id, e])); |
| } |
| _onTestBegin(params) { |
| const test = this._remainingByTestId.get(params.testId); |
| if (!test) { |
| return; |
| } |
| const result = test._appendTestResult(); |
| this._dataByTestId.set(test.id, { test, result, steps: 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._testRun.hasReachedMaxFailures()) { |
| 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, |
| titlePath: () => { |
| const parentPath = parentStep?.titlePath() || []; |
| return [...parentPath, params.title]; |
| }, |
| parent: parentStep, |
| category: params.category, |
| 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._testRun.hasReachedMaxFailures()) { |
| 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( new Set([this._currentlyRunning.test.id]), [params.unexpectedExitError]); |
| else |
| this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError]); |
| } |
| const retryCandidates = new Set(); |
| const serialSuitesWithFailures = 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()]; |
| for (const test of retryCandidates) { |
| if (test.results.length < test.retries + 1) |
| remaining.push(test); |
| } |
| const newJob = remaining.length ? { ...this.job, tests: remaining } : void 0; |
| this._finished({ didFail: true, newJob }); |
| } |
| 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 }; |
| }) |
| }; |
| 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_util9.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._testRun.hasReachedMaxFailures()) { |
| 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._testRun.hasReachedMaxFailures(); |
| if (test.outcome() === "unexpected" && test.results.length > test.retries) |
| ++this._testRun.failedTestCount; |
| if (!hadMaxFailures && this._testRun.hasReachedMaxFailures()) { |
| this._stopCallback(); |
| 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"); |
| } |
|
|
| |
| 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(); |
| } |
| }; |
|
|
| |
| var import_util10 = 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_util10.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); |
| } |
| }; |
|
|
| |
| var import_child_process2 = __toESM(require("child_process")); |
| var import_path14 = __toESM(require("path")); |
| var import_common7 = 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_path14.default.join(configDir, file)); |
| const [gitRoot] = gitFileList(["rev-parse", "--show-toplevel"]); |
| const trackedFilesWithChanges = gitFileList(["diff", baseCommit, "--name-only"]).map((file) => import_path14.default.join(gitRoot, file)); |
| return new Set(import_common7.cc.affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges])); |
| } |
|
|
| |
| var import_common8 = require("../common"); |
| var import_util12 = 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_util11.promisify)(import_fs11.default.readdir); |
| var TestRun = class { |
| constructor(config2, reporter, options) { |
| this.rootSuite = void 0; |
| this.phases = []; |
| this.projectFiles = new Map(); |
| this.projectSuites = 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.hasReachedMaxFailures() || hasFailedTests || this.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(); |
| 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(), |
| ...config2.plugins.map((plugin) => createPluginBeginTask(plugin)), |
| createRunTestsTask() |
| ]; |
| } |
| function createClearCacheTask(config2) { |
| return { |
| title: "clear cache", |
| setup: async () => { |
| await (0, import_util12.removeDirAndLogToConsole)(import_common8.cc.cacheDir); |
| for (const plugin of config2.plugins) |
| await plugin.instance?.clearCache?.(); |
| } |
| }; |
| } |
| 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 createPluginBeginTask(plugin) { |
| return { |
| title: "plugin begin", |
| setup: async (testRun) => { |
| await plugin.instance?.begin?.(testRun.rootSuite); |
| }, |
| teardown: async () => { |
| await plugin.instance?.end?.(); |
| } |
| }; |
| } |
| 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 = 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_path15.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_common8.test.Suite(project.project.name, "project"); |
| projectSuite._fullProject = project; |
| testRun.rootSuite._addSuite(projectSuite); |
| const suites = files.map((file) => { |
| const title = import_path15.default.relative(testRun.config.config.rootDir, file); |
| const suite = new import_common8.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_common8.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_util12.createTitleMatcher)((0, import_util12.forceRegExp)(testRun.options.grep)) : () => true; |
| const grepInvertMatcher = testRun.options.grepInvert ? (0, import_util12.createTitleMatcher)((0, import_util12.forceRegExp)(testRun.options.grepInvert)) : () => false; |
| testRun.preOnlyTestFilters.push((test) => { |
| const grepTitle = test._grepTitleWithTags(); |
| return !grepInvertMatcher(grepTitle) && grepMatcher(grepTitle); |
| }); |
| } |
| if (testRun.options.lastFailedTestIds?.length) { |
| 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 || options.populateDependencies) { |
| for (const plugin of testRun.config.plugins) |
| await plugin.instance?.populateDependencies?.(); |
| } |
| 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 = new Set(); |
| const projectToSuite = new Map(testRun.rootSuite.suites.map((suite) => [suite._fullProject, suite])); |
| const allProjects = [...projectToSuite.keys()]; |
| const teardownToSetups = buildTeardownToSetupsMap(allProjects); |
| const teardownToSetupsDependents = 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); |
| if (phaseProjects.length) { |
| let testGroupsInPhase = 0; |
| const phase = { dispatcher: new Dispatcher(testRun), projects: [] }; |
| testRun.phases.push(phase); |
| for (const project of phaseProjects) { |
| 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 = new Set(); |
| const extraEnvByProjectId = 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(); |
| } |
| }; |
| } |
|
|
| |
| var import_fs12 = __toESM(require("fs")); |
| var import_path16 = __toESM(require("path")); |
| var LastRunReporter = class { |
| constructor(filteredProjects, listMode) { |
| this._listMode = !!listMode; |
| const [project] = filteredProjects; |
| if (project) |
| this._lastRunFile = import_path16.default.join(project.project.outputDir, ".last-run.json"); |
| } |
| async filterLastFailed() { |
| if (!this._lastRunFile) |
| return []; |
| try { |
| const lastRunInfo = JSON.parse(await import_fs12.default.promises.readFile(this._lastRunFile, "utf8")); |
| return lastRunInfo.failedTests; |
| } catch { |
| return []; |
| } |
| } |
| 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()).map((t2) => t2.id) || [] |
| }; |
| await import_fs12.default.promises.mkdir(import_path16.default.dirname(this._lastRunFile), { recursive: true }); |
| await import_fs12.default.promises.writeFile(this._lastRunFile, JSON.stringify(lastRunInfo, void 0, 2)); |
| } |
| }; |
|
|
| |
| 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 = new Set(); |
| this._ignoredProjectOutputs = new Set(); |
| this._watchedTestDependencies = new Set(); |
| this._queue = Promise.resolve(); |
| this._watchTestDirs = false; |
| this._populateDependenciesOnList = false; |
| this._startingEnv = {}; |
| this.configLocation = configLocation; |
| this._configCLIOverrides = configCLIOverrides; |
| this._watcher = new FSWatcher((events) => { |
| const collector = new Set(); |
| events.forEach((f) => import_common9.cc.collectAffectedTestFiles(f.file, collector)); |
| this.emit(TestRunnerEvent.TestFilesChanged, [...collector]); |
| }); |
| } |
| async initialize(params) { |
| setPlaywrightTestProcessEnv(); |
| this._watchTestDirs = !!params.watchTestDirs; |
| this._populateDependenciesOnList = !!params.populateDependenciesOnList; |
| 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_coreBundle2.registry.registry.findExecutable(browserName).executablePathOrDie("javascript"); |
| return true; |
| } catch { |
| } |
| } |
| return false; |
| } |
| async installBrowsers() { |
| const executables = import_coreBundle2.registry.registry.defaultExecutables(); |
| await import_coreBundle2.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 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), [ |
| ...createPluginSetupTasks(config2), |
| 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, populateDependencies: this._populateDependenciesOnList }), |
| createReportBeginTask() |
| ]); |
| return { config: config2, status }; |
| } |
| async _updateWatchedDirs(config2) { |
| this._watchedProjectDirs = new Set(); |
| this._ignoredProjectOutputs = new Set(); |
| for (const p of config2.projects) { |
| this._watchedProjectDirs.add(p.project.testDir); |
| this._ignoredProjectOutputs.add(p.project.outputDir); |
| } |
| const result = await resolveCtDirs(config2); |
| if (result) { |
| this._watchedProjectDirs.add(result.templateDir); |
| this._ignoredProjectOutputs.add(result.outDir); |
| } |
| 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 } : {} |
| }; |
| 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 = new Set(); |
| for (const fileName of fileNames) { |
| this._watchedTestDependencies.add(fileName); |
| import_common9.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, populateDependencies: true }) |
| ]); |
| if (status !== "passed") |
| return { errors: errorReporter.errors(), testFiles: [] }; |
| return { testFiles: import_common9.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_common9.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); |
| } |
| return { config: config2 }; |
| } catch (e) { |
| return { config: null, error: (0, import_util13.serializeError)(e) }; |
| } |
| } |
| 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 resolveCtDirs(config2) { |
| const use = config2.config.projects[0].use; |
| const relativeTemplateDir = use.ctTemplateDir || "playwright"; |
| const templateDir = await import_fs13.default.promises.realpath(import_path17.default.normalize(import_path17.default.join(config2.configDir, relativeTemplateDir))).catch(() => void 0); |
| if (!templateDir) |
| return null; |
| const outDir = use.ctCacheDir ? import_path17.default.resolve(config2.configDir, use.ctCacheDir) : import_path17.default.resolve(templateDir, ".cache"); |
| return { |
| outDir, |
| templateDir |
| }; |
| } |
| 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); |
| if (options.lastFailed) { |
| const lastFailedTestIds = await lastRun.filterLastFailed(); |
| if (lastFailedTestIds.length) |
| 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; |
| } |
|
|
| |
| var testServer_exports = {}; |
| __export(testServer_exports, { |
| TestServerDispatcher: () => TestServerDispatcher, |
| runTestServer: () => runTestServer, |
| runUIMode: () => runUIMode |
| }); |
| var import_util14 = __toESM(require("util")); |
| var import_coreBundle3 = require("playwright-core/lib/coreBundle"); |
| var import_common10 = require("../common"); |
|
|
| |
| var UIModeReporter = class extends TeleReporterEmitter { |
| constructor(options) { |
| super(options._send, { omitBuffers: true }); |
| } |
| }; |
| var uiModeReporter_default = UIModeReporter; |
|
|
| |
| 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_coreBundle3.server.startTraceViewerServer({ ...options, transport: this._dispatcher.transport }); |
| } |
| 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_util14.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_common10.configLoader.resolveConfigLocation(configFile); |
| return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server, cancelPromise) => { |
| await import_coreBundle3.server.installRootRedirect(server, void 0, { ...options, webApp: "uiMode.html" }); |
| if (options.host !== void 0 || options.port !== void 0) { |
| await import_coreBundle3.server.openTraceInBrowser(server.urlPrefix("human-readable")); |
| } else { |
| const channel = await installedChromiumChannelForUI(configLocation, configCLIOverrides); |
| const page = await import_coreBundle3.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_common10.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_common10.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 |
| })); |
| } |
|
|
| |
| var watchMode_exports = {}; |
| __export(watchMode_exports, { |
| runWatchModeLoop: () => runWatchModeLoop |
| }); |
| var import_path18 = __toESM(require("path")); |
| var import_readline = __toESM(require("readline")); |
| var import_stream3 = require("stream"); |
| var import_coreBundle4 = require("playwright-core/lib/coreBundle"); |
|
|
| |
| var statusEx = Symbol("statusEx"); |
|
|
| |
| 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); |
| }; |
| } |
|
|
| |
| 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 = 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 = []; |
| } |
| }; |
|
|
| |
| 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 = 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 { |
| } |
| } |
| }; |
|
|
| |
| 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_stream3.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 = new Set(); |
| const dirtyTestIds = new Set(); |
| let onDirtyTests = new ManualPromise8(); |
| let queue = Promise.resolve(); |
| const changedFiles = 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, |
| populateDependenciesOnList: 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 ?? [], |
| |
| 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_coreBundle4.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")} |
| `); |
| } |
| } |
|
|
| |
| var merge_exports = {}; |
| __export(merge_exports, { |
| createMergedReport: () => createMergedReport |
| }); |
| var import_fs14 = __toESM(require("fs")); |
| var import_path19 = __toESM(require("path")); |
|
|
| |
| var StringInternPool = class { |
| constructor() { |
| this._stringCache = 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); |
| } |
| }; |
|
|
| |
| var import_util15 = 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, |
| |
| |
| |
| |
| 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); |
| } |
| 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_util15.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 = 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 = 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 = 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(); |
| |
| 0 && (module.exports = { |
| ListModeReporter, |
| ListReporter, |
| TestServerConnection, |
| base, |
| html, |
| merge, |
| projectUtils, |
| runnerReporters, |
| testRunner, |
| testServer, |
| watchMode, |
| webServer |
| }); |
|
|