UNPKG

playwright

Version:

A high-level API to automate web browsers

1,272 lines (1,262 loc) • 75.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // packages/playwright/src/worker/workerProcessEntry.ts var import_common4 = require("../common"); // packages/playwright/src/worker/workerMain.ts var import_common3 = require("../common"); var globals = __toESM(require("../globals")); var import_expect = require("../matchers/expect"); // packages/playwright/src/util.ts var import_fs = __toESM(require("fs")); var import_path = __toESM(require("path")); var import_util = __toESM(require("util")); var debug = require("playwright-core/lib/utilsBundle").debug; var mime = require("playwright-core/lib/utilsBundle").mime; var minimatch = require("playwright-core/lib/utilsBundle").minimatch; var { calculateSha1 } = require("playwright-core/lib/coreBundle").utils; var { sanitizeForFilePath } = require("playwright-core/lib/coreBundle").utils; var { isRegExp } = require("playwright-core/lib/coreBundle").iso; var { parseStackFrame, stringifyStackFrames } = require("playwright-core/lib/coreBundle").iso; var { ansiRegex, isString, stripAnsiEscapes } = require("playwright-core/lib/coreBundle").iso; var PLAYWRIGHT_TEST_PATH = import_path.default.join(__dirname, ".."); var PLAYWRIGHT_CORE_PATH = import_path.default.dirname(require.resolve("playwright-core/package.json")); function filterStackTrace(e) { const name = e.name ? e.name + ": " : ""; const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : void 0; if (process.env.PWDEBUGIMPL) return { message: name + e.message, stack: e.stack || "", cause }; const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split("\n") || [])); return { message: name + e.message, stack: `${name}${e.message}${stackLines.map((line) => "\n" + line).join("")}`, cause }; } function filterStackFile(file) { if (process.env.PWDEBUGIMPL) return true; if (file.startsWith(PLAYWRIGHT_TEST_PATH)) return false; if (file.startsWith(PLAYWRIGHT_CORE_PATH)) return false; return true; } function filteredStackTrace(rawStack) { const frames = []; for (const line of rawStack) { const frame = parseStackFrame(line, import_path.default.sep, !!process.env.PWDEBUGIMPL); if (!frame || !frame.file) continue; if (!filterStackFile(frame.file)) continue; frames.push(frame); } return frames; } function serializeError(error) { if (error instanceof Error) return filterStackTrace(error); return { value: import_util.default.inspect(error) }; } function relativeFilePath(file) { if (!import_path.default.isAbsolute(file)) return file; return import_path.default.relative(process.cwd(), file); } function formatLocation(location) { return relativeFilePath(location.file) + ":" + location.line + ":" + location.column; } var windowsFilesystemFriendlyLength = 60; function addSuffixToFilePath(filePath, suffix) { const ext = import_path.default.extname(filePath); const base = filePath.substring(0, filePath.length - ext.length); return base + suffix + ext; } function sanitizeFilePathBeforeExtension(filePath, ext) { ext ??= import_path.default.extname(filePath); const base = filePath.substring(0, filePath.length - ext.length); return sanitizeForFilePath(base) + ext; } function getContainedPath(parentPath, subPath = "") { const resolvedPath = import_path.default.resolve(parentPath, subPath); if (resolvedPath === parentPath || resolvedPath.startsWith(parentPath + import_path.default.sep)) return resolvedPath; return null; } var debugTest = debug("pw:test"); async function normalizeAndSaveAttachment(outputPath, name, options = {}) { if (options.path === void 0 && options.body === void 0) return { name, contentType: "text/plain" }; if ((options.path !== void 0 ? 1 : 0) + (options.body !== void 0 ? 1 : 0) !== 1) throw new Error(`Exactly one of "path" and "body" must be specified`); if (options.path !== void 0) { const hash = calculateSha1(options.path); if (!isString(name)) throw new Error('"name" should be string.'); const sanitizedNamePrefix = sanitizeForFilePath(name) + "-"; const dest = import_path.default.join(outputPath, "attachments", sanitizedNamePrefix + hash + import_path.default.extname(options.path)); await import_fs.default.promises.mkdir(import_path.default.dirname(dest), { recursive: true }); await import_fs.default.promises.copyFile(options.path, dest); const contentType = options.contentType ?? (mime.getType(import_path.default.basename(options.path)) || "application/octet-stream"); return { name, contentType, path: dest }; } else { const contentType = options.contentType ?? (typeof options.body === "string" ? "text/plain" : "application/octet-stream"); return { name, contentType, body: typeof options.body === "string" ? Buffer.from(options.body) : options.body }; } } // packages/playwright/src/worker/fixtureRunner.ts var import_common = require("../common"); var { ManualPromise } = require("playwright-core/lib/coreBundle").iso; var { escapeWithQuotes } = require("playwright-core/lib/coreBundle").iso; var Fixture = class { constructor(runner, registration) { this.failed = false; this._deps = /* @__PURE__ */ new Set(); this._usages = /* @__PURE__ */ new Set(); this.runner = runner; this.registration = registration; this.value = null; const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file); const title = this.registration.customTitle || this.registration.name; const location = isUserFixture ? this.registration.location : void 0; this._stepInfo = { title: `Fixture ${escapeWithQuotes(title, '"')}`, category: "fixture", location }; if (this.registration.box === "self") this._stepInfo = void 0; else if (this.registration.box) this._stepInfo.group = isUserFixture ? "configuration" : "internal"; this._setupDescription = { title, phase: "setup", location, slot: this.registration.timeout !== void 0 ? { timeout: this.registration.timeout, elapsed: 0 } : this.registration.scope === "worker" ? { timeout: this.runner.workerFixtureTimeout, elapsed: 0 } : void 0 }; this._teardownDescription = { ...this._setupDescription, phase: "teardown" }; } async setup(testInfo, runnable) { this.runner.instanceForId.set(this.registration.id, this); if (typeof this.registration.fn !== "function") { this.value = this.registration.fn; return; } const run = () => testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo)); if (this._stepInfo) await testInfo._runAsStep(this._stepInfo, run); else await run(); } async _setupInternal(testInfo) { const params = {}; for (const name of this.registration.deps) { const registration = this.runner.pool.resolve(name, this.registration); const dep = this.runner.instanceForId.get(registration.id); if (!dep) { this.failed = true; return; } dep._usages.add(this); this._deps.add(dep); params[name] = dep.value; if (dep.failed) { this.failed = true; return; } } let called = false; const useFuncStarted = new ManualPromise(); const useFunc = async (value) => { if (called) throw new Error(`Cannot provide fixture value for the second time`); called = true; this.value = value; this._useFuncFinished = new ManualPromise(); useFuncStarted.resolve(); await this._useFuncFinished; }; const workerInfo = { config: testInfo.config, parallelIndex: testInfo.parallelIndex, workerIndex: testInfo.workerIndex, project: testInfo.project }; const info = this.registration.scope === "worker" ? workerInfo : testInfo; this._selfTeardownComplete = (async () => { try { await this.registration.fn(params, useFunc, info); if (!useFuncStarted.isDone()) throw new Error(`use() was not called in fixture "${this.registration.name}"`); } catch (error) { this.failed = true; if (!useFuncStarted.isDone()) useFuncStarted.reject(error); else throw error; } })(); await useFuncStarted; } async teardown(testInfo, runnable) { try { const fixtureRunnable = { ...runnable, fixture: this._teardownDescription }; if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) { const run = () => testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal()); if (this._stepInfo) await testInfo._runAsStep(this._stepInfo, run); else await run(); } } finally { for (const dep of this._deps) dep._usages.delete(this); this.runner.instanceForId.delete(this.registration.id); } } async _teardownInternal() { if (typeof this.registration.fn !== "function") return; if (this._usages.size !== 0) { console.error("Internal error: fixture integrity at", this._teardownDescription.title); this._usages.clear(); } if (this._useFuncFinished) { this._useFuncFinished.resolve(); this._useFuncFinished = void 0; await this._selfTeardownComplete; } } _collectFixturesInTeardownOrder(scope, collector) { if (this.registration.scope !== scope) return; for (const fixture of this._usages) fixture._collectFixturesInTeardownOrder(scope, collector); collector.add(this); } }; var FixtureRunner = class { constructor() { this.testScopeClean = true; this.instanceForId = /* @__PURE__ */ new Map(); this.workerFixtureTimeout = 0; } setPool(pool) { if (!this.testScopeClean) throw new Error("Did not teardown test scope"); if (this.pool && pool.digest !== this.pool.digest) { throw new Error([ `Playwright detected inconsistent test.use() options.`, `Most common mistakes that lead to this issue:`, ` - Calling test.use() outside of the test file, for example in a common helper.`, ` - One test file imports from another test file.` ].join("\n")); } this.pool = pool; } _collectFixturesInSetupOrder(registration, collector) { if (collector.has(registration)) return; for (const name of registration.deps) { const dep = this.pool.resolve(name, registration); this._collectFixturesInSetupOrder(dep, collector); } collector.add(registration); } async teardownScope(scope, testInfo, runnable) { const allFixtures = Array.from(this.instanceForId.values()).reverse(); const collector = /* @__PURE__ */ new Set(); for (const fixture of allFixtures) fixture._collectFixturesInTeardownOrder(scope, collector); let firstError; for (const fixture of collector) { try { await fixture.teardown(testInfo, runnable); } catch (error) { firstError = firstError ?? error; } } if (scope === "test") this.testScopeClean = true; if (firstError) throw firstError; } async resolveParametersForFunction(fn, testInfo, autoFixtures, runnable) { const collector = /* @__PURE__ */ new Set(); const auto = []; for (const registration of this.pool.autoFixtures()) { let shouldRun = true; if (autoFixtures === "all-hooks-only") shouldRun = registration.scope === "worker" || registration.auto === "all-hooks-included"; else if (autoFixtures === "worker") shouldRun = registration.scope === "worker"; if (shouldRun) auto.push(registration); } auto.sort((r1, r2) => (r1.scope === "worker" ? 0 : 1) - (r2.scope === "worker" ? 0 : 1)); for (const registration of auto) this._collectFixturesInSetupOrder(registration, collector); const names = getRequiredFixtureNames(fn); for (const name of names) this._collectFixturesInSetupOrder(this.pool.resolve(name), collector); for (const registration of collector) await this._setupFixtureForRegistration(registration, testInfo, runnable); const params = {}; for (const name of names) { const registration = this.pool.resolve(name); const fixture = this.instanceForId.get(registration.id); if (!fixture || fixture.failed) return null; params[name] = fixture.value; } return { result: params }; } async resolveParametersAndRunFunction(fn, testInfo, autoFixtures, runnable) { const params = await this.resolveParametersForFunction(fn, testInfo, autoFixtures, runnable); if (params === null) { return null; } await testInfo._runWithTimeout(runnable, () => fn(params.result, testInfo)); } async _setupFixtureForRegistration(registration, testInfo, runnable) { if (registration.scope === "test") this.testScopeClean = false; let fixture = this.instanceForId.get(registration.id); if (fixture) return fixture; fixture = new Fixture(this, registration); await fixture.setup(testInfo, runnable); return fixture; } dependsOnWorkerFixturesOnly(fn, location) { const names = getRequiredFixtureNames(fn, location); for (const name of names) { const registration = this.pool.resolve(name); if (registration.scope !== "worker") return false; } return true; } }; function getRequiredFixtureNames(fn, location) { return import_common.fixtures.fixtureParameterNames(fn, location ?? { file: "<unknown>", line: 1, column: 1 }, (e) => { throw new Error(`${formatLocation(e.location)}: ${e.message}`); }); } // packages/playwright/src/worker/testInfo.ts var import_fs3 = __toESM(require("fs")); var import_path3 = __toESM(require("path")); // packages/playwright/src/worker/timeoutManager.ts var colors = require("playwright-core/lib/utilsBundle").colors; var { ManualPromise: ManualPromise2 } = require("playwright-core/lib/coreBundle").iso; var { monotonicTime } = require("playwright-core/lib/coreBundle").iso; var kMaxDeadline = 2147483647; var TimeoutManager = class { constructor(timeout) { this._ignoreTimeouts = false; this._slow = false; this._defaultSlot = { timeout, elapsed: 0 }; } setIgnoreTimeouts(ignoreTimeouts) { if (this._ignoreTimeouts === ignoreTimeouts) return; this._ignoreTimeouts = ignoreTimeouts; if (this._running) { if (ignoreTimeouts) this._running.slot.elapsed += monotonicTime() - this._running.start; else this._running.start = monotonicTime(); this._updateTimeout(this._running); } } interrupt() { if (this._running) this._running.timeoutPromise.reject(this._createTimeoutError(this._running)); } isTimeExhaustedFor(runnable) { const slot = runnable.fixture?.slot || runnable.slot || this._defaultSlot; return slot.timeout > 0 && slot.elapsed >= slot.timeout - 1; } async withRunnable(runnable, cb) { if (this._running) throw new Error(`Internal error: duplicate runnable`); const running = this._running = { runnable, slot: runnable.fixture?.slot || runnable.slot || this._defaultSlot, start: monotonicTime(), deadline: kMaxDeadline, timer: void 0, timeoutPromise: new ManualPromise2() }; let debugTitle = ""; try { if (debugTest.enabled) { debugTitle = runnable.fixture ? `${runnable.fixture.phase} "${runnable.fixture.title}"` : runnable.type; const location = runnable.location ? ` at "${formatLocation(runnable.location)}"` : ``; debugTest(`started ${debugTitle}${location}`); } this._updateTimeout(running); return await Promise.race([ cb(), running.timeoutPromise ]); } finally { if (running.timer) clearTimeout(running.timer); running.timer = void 0; running.slot.elapsed += monotonicTime() - running.start; this._running = void 0; if (debugTest.enabled) debugTest(`finished ${debugTitle}`); } } _updateTimeout(running) { if (running.timer) clearTimeout(running.timer); running.timer = void 0; if (this._ignoreTimeouts || !running.slot.timeout) { running.deadline = kMaxDeadline; return; } running.deadline = running.start + (running.slot.timeout - running.slot.elapsed); const timeout = running.deadline - monotonicTime() + 1; if (timeout <= 0) running.timeoutPromise.reject(this._createTimeoutError(running)); else running.timer = setTimeout(() => running.timeoutPromise.reject(this._createTimeoutError(running)), timeout); } defaultSlot() { return this._defaultSlot; } slow() { if (this._slow) return; this._slow = true; const slot = this._running ? this._running.slot : this._defaultSlot; slot.timeout = slot.timeout * 3; if (this._running) this._updateTimeout(this._running); } setTimeout(timeout) { const slot = this._running ? this._running.slot : this._defaultSlot; slot.timeout = timeout; if (this._running) this._updateTimeout(this._running); } currentSlotDeadline() { return this._running ? this._running.deadline : kMaxDeadline; } currentSlotType() { return this._running ? this._running.runnable.type : "test"; } _createTimeoutError(running) { let message = ""; const timeout = running.slot.timeout; const runnable = running.runnable; switch (runnable.type) { case "test": { if (runnable.fixture) { if (runnable.fixture.phase === "setup") message = `Test timeout of ${timeout}ms exceeded while setting up "${runnable.fixture.title}".`; else message = `Tearing down "${runnable.fixture.title}" exceeded the test timeout of ${timeout}ms.`; } else { message = `Test timeout of ${timeout}ms exceeded.`; } break; } case "afterEach": case "beforeEach": message = `Test timeout of ${timeout}ms exceeded while running "${runnable.type}" hook.`; break; case "beforeAll": case "afterAll": message = `"${runnable.type}" hook timeout of ${timeout}ms exceeded.`; break; case "teardown": { if (runnable.fixture) message = `Worker teardown timeout of ${timeout}ms exceeded while ${runnable.fixture.phase === "setup" ? "setting up" : "tearing down"} "${runnable.fixture.title}".`; else message = `Worker teardown timeout of ${timeout}ms exceeded.`; break; } case "skip": case "slow": case "fixme": case "fail": message = `"${runnable.type}" modifier timeout of ${timeout}ms exceeded.`; break; } const fixtureWithSlot = runnable.fixture?.slot ? runnable.fixture : void 0; if (fixtureWithSlot) message = `Fixture "${fixtureWithSlot.title}" timeout of ${timeout}ms exceeded during ${fixtureWithSlot.phase}.`; message = colors.red(message); const location = (fixtureWithSlot || runnable).location; const error = new TimeoutManagerError(message); error.name = ""; error.stack = message + (location ? ` at ${location.file}:${location.line}:${location.column}` : ""); return error; } }; var TimeoutManagerError = class extends Error { }; // packages/playwright/src/worker/testTracing.ts var import_fs2 = __toESM(require("fs")); var import_path2 = __toESM(require("path")); var import_coreBundle = require("playwright-core/lib/coreBundle"); var yazl = require("playwright-core/lib/utilsBundle").yazl; var yauzl = require("playwright-core/lib/utilsBundle").yauzl; var { ManualPromise: ManualPromise3 } = require("playwright-core/lib/coreBundle").iso; var { monotonicTime: monotonicTime2 } = require("playwright-core/lib/coreBundle").iso; var { calculateSha1: calculateSha12, createGuid } = require("playwright-core/lib/coreBundle").utils; var { SerializedFS } = require("playwright-core/lib/coreBundle").utils; var testTraceEntryName = "test.trace"; var version = 8; var traceOrdinal = 0; var TestTracing = class { constructor(testInfo, artifactsDir) { this._traceEvents = []; this._temporaryTraceFiles = []; this._didFinishTestFunctionAndAfterEachHooks = false; this._testInfo = testInfo; this._artifactsDir = artifactsDir; this._tracesDir = import_path2.default.join(this._artifactsDir, "traces"); this._contextCreatedEvent = { version, type: "context-options", origin: "testRunner", browserName: "", playwrightVersion: (0, import_coreBundle.getPlaywrightVersion)(), options: {}, platform: process.platform, wallTime: Date.now(), monotonicTime: monotonicTime2(), sdkLanguage: "javascript" }; this._appendTraceEvent(this._contextCreatedEvent); } _shouldCaptureTrace() { if (this._options?.mode === "on") return true; if (this._options?.mode === "retain-on-failure") return true; if (this._options?.mode === "on-first-retry" && this._testInfo.retry === 1) return true; if (this._options?.mode === "on-all-retries" && this._testInfo.retry > 0) return true; if (this._options?.mode === "retain-on-first-failure" && this._testInfo.retry === 0) return true; if (this._options?.mode === "retain-on-failure-and-retries") return true; return false; } async startIfNeeded(value) { const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true, attachments: true, live: false, mode: "off" }; if (!value) { this._options = defaultTraceOptions; } else if (typeof value === "string") { this._options = { ...defaultTraceOptions, mode: value === "retry-with-trace" ? "on-first-retry" : value }; } else { const mode = value.mode || "off"; this._options = { ...defaultTraceOptions, ...value, mode: mode === "retry-with-trace" ? "on-first-retry" : mode }; } if (!this._shouldCaptureTrace()) { this._options = void 0; return; } if (!this._liveTraceFile && this._options.live) { this._liveTraceFile = { file: import_path2.default.join(this._tracesDir, `${this._testInfo.testId}-test.trace`), fs: new SerializedFS() }; this._liveTraceFile.fs.mkdir(import_path2.default.dirname(this._liveTraceFile.file)); const data = this._traceEvents.map((e) => JSON.stringify(e)).join("\n") + "\n"; this._liveTraceFile.fs.writeFile(this._liveTraceFile.file, data); } } didFinishTestFunctionAndAfterEachHooks() { this._didFinishTestFunctionAndAfterEachHooks = true; } artifactsDir() { return this._artifactsDir; } tracesDir() { return this._tracesDir; } traceTitle() { return [import_path2.default.relative(this._testInfo.project.testDir, this._testInfo.file) + ":" + this._testInfo.line, ...this._testInfo.titlePath.slice(1)].join(" \u203A "); } generateNextTraceRecordingName() { const ordinalSuffix = traceOrdinal ? `-recording${traceOrdinal}` : ""; ++traceOrdinal; const retrySuffix = this._testInfo.retry ? `-retry${this._testInfo.retry}` : ""; return `${this._testInfo.testId}${retrySuffix}${ordinalSuffix}`; } _generateNextTraceRecordingPath() { const file = import_path2.default.join(this._artifactsDir, createGuid() + ".zip"); this._temporaryTraceFiles.push(file); return file; } traceOptions() { return this._options; } maybeGenerateNextTraceRecordingPath() { if (this._didFinishTestFunctionAndAfterEachHooks && this._shouldAbandonTrace()) return; return this._generateNextTraceRecordingPath(); } _shouldAbandonTrace() { if (!this._options) return true; const testFailed = this._testInfo.status !== this._testInfo.expectedStatus; if (this._options.mode === "retain-on-failure-and-retries") return !testFailed && this._testInfo.retry === 0; return !testFailed && (this._options.mode === "retain-on-failure" || this._options.mode === "retain-on-first-failure"); } async stopIfNeeded() { this._contextCreatedEvent.testTimeout = this._testInfo.timeout; if (!this._options) return; const error = await this._liveTraceFile?.fs.syncAndGetError(); if (error) throw error; if (this._shouldAbandonTrace()) { for (const file of this._temporaryTraceFiles) await import_fs2.default.promises.unlink(file).catch(() => { }); return; } const zipFile = new yazl.ZipFile(); if (!this._options?.attachments) { for (const event of this._traceEvents) { if (event.type === "after") delete event.attachments; } } if (this._options?.sources) { const sourceFiles = /* @__PURE__ */ new Set(); for (const event of this._traceEvents) { if (event.type === "before") { for (const frame of event.stack || []) sourceFiles.add(frame.file); } } for (const sourceFile of sourceFiles) { await import_fs2.default.promises.readFile(sourceFile, "utf8").then((source) => { zipFile.addBuffer(Buffer.from(source), "resources/src@" + calculateSha12(sourceFile) + ".txt"); }).catch(() => { }); } } const sha1s = /* @__PURE__ */ new Set(); for (const event of this._traceEvents.filter((e) => e.type === "after")) { for (const attachment of event.attachments || []) { let contentPromise; if (attachment.path) contentPromise = import_fs2.default.promises.readFile(attachment.path).catch(() => void 0); else if (attachment.base64) contentPromise = Promise.resolve(Buffer.from(attachment.base64, "base64")); const content = await contentPromise; if (content === void 0) continue; const sha1 = calculateSha12(content); attachment.sha1 = sha1; delete attachment.path; delete attachment.base64; if (sha1s.has(sha1)) continue; sha1s.add(sha1); zipFile.addBuffer(content, "resources/" + sha1); } } const traceContent = Buffer.from(this._traceEvents.map((e) => JSON.stringify(e)).join("\n")); zipFile.addBuffer(traceContent, testTraceEntryName); await new Promise((resolve, reject) => { zipFile.end(void 0, () => { zipFile.outputStream.pipe(import_fs2.default.createWriteStream(this._generateNextTraceRecordingPath())).on("close", resolve).on("error", reject); }); }); const tracePath = this._testInfo.outputPath("trace.zip"); await mergeTraceFiles(tracePath, this._temporaryTraceFiles); this._testInfo.attachments.push({ name: "trace", path: tracePath, contentType: "application/zip" }); } appendForError(error) { const rawStack = error.stack?.split("\n") || []; const stack = rawStack ? filteredStackTrace(rawStack) : []; this._appendTraceEvent({ type: "error", message: this._formatError(error), stack }); } _formatError(error) { const parts = [error.message || String(error.value)]; if (error.cause) parts.push("[cause]: " + this._formatError(error.cause)); return parts.join("\n"); } appendStdioToTrace(type, chunk) { this._appendTraceEvent({ type, timestamp: monotonicTime2(), text: typeof chunk === "string" ? chunk : void 0, base64: typeof chunk === "string" ? void 0 : chunk.toString("base64") }); } appendBeforeActionForStep(options) { this._appendTraceEvent({ type: "before", callId: options.stepId, stepId: options.stepId, parentId: options.parentId, startTime: monotonicTime2(), class: "Test", method: options.category, title: options.title, params: Object.fromEntries(Object.entries(options.params || {}).map(([name, value]) => [name, generatePreview(value)])), stack: options.stack, group: options.group }); } appendAfterActionForStep(callId, error, attachments = [], annotations) { this._appendTraceEvent({ type: "after", callId, endTime: monotonicTime2(), attachments: serializeAttachments(attachments), annotations, error }); } _appendTraceEvent(event) { this._traceEvents.push(event); if (this._liveTraceFile) this._liveTraceFile.fs.appendFile(this._liveTraceFile.file, JSON.stringify(event) + "\n", true); } }; function serializeAttachments(attachments) { if (attachments.length === 0) return void 0; return attachments.filter((a) => a.name !== "trace").map((a) => { return { name: a.name, contentType: a.contentType, path: a.path, base64: a.body?.toString("base64") }; }); } function generatePreview(value, visited = /* @__PURE__ */ new Set()) { if (visited.has(value)) return ""; visited.add(value); if (typeof value === "string") return value; if (typeof value === "number") return value.toString(); if (typeof value === "boolean") return value.toString(); if (value === null) return "null"; if (value === void 0) return "undefined"; if (Array.isArray(value)) return "[" + value.map((v) => generatePreview(v, visited)).join(", ") + "]"; if (typeof value === "object") return "Object"; return String(value); } async function mergeTraceFiles(fileName, temporaryTraceFiles) { temporaryTraceFiles = temporaryTraceFiles.filter((file) => import_fs2.default.existsSync(file)); if (temporaryTraceFiles.length === 1) { await import_fs2.default.promises.rename(temporaryTraceFiles[0], fileName); return; } const mergePromise = new ManualPromise3(); const zipFile = new yazl.ZipFile(); const entryNames = /* @__PURE__ */ new Set(); zipFile.on("error", (error) => mergePromise.reject(error)); for (let i = temporaryTraceFiles.length - 1; i >= 0; --i) { const tempFile = temporaryTraceFiles[i]; const promise = new ManualPromise3(); yauzl.open(tempFile, (err, inZipFile) => { if (err) { promise.reject(err); return; } let pendingEntries = inZipFile.entryCount; inZipFile.on("entry", (entry) => { let entryName = entry.fileName; if (entry.fileName === testTraceEntryName) { } else if (entry.fileName.match(/trace\.[a-z]*$/)) { entryName = i + "-" + entry.fileName; } if (entryNames.has(entryName)) { if (--pendingEntries === 0) promise.resolve(); return; } entryNames.add(entryName); inZipFile.openReadStream(entry, (err2, readStream) => { if (err2) { promise.reject(err2); return; } zipFile.addReadStream(readStream, entryName); if (--pendingEntries === 0) promise.resolve(); }); }); }); await promise; } zipFile.end(void 0, () => { zipFile.outputStream.pipe(import_fs2.default.createWriteStream(fileName)).on("close", () => { void Promise.all(temporaryTraceFiles.map((tempFile) => import_fs2.default.promises.unlink(tempFile))).then(() => { mergePromise.resolve(); }).catch((error) => mergePromise.reject(error)); }).on("error", (error) => mergePromise.reject(error)); }); await mergePromise; } // packages/playwright/src/worker/util.ts function testInfoError(error) { const result = serializeError(error); const matcherResult = error instanceof Error ? error.matcherResult : void 0; if (matcherResult?.ariaSnapshot !== void 0) result.errorContext = matcherResult.ariaSnapshot; return result; } // packages/playwright/src/worker/testInfo.ts var import_common2 = require("../common"); var { ManualPromise: ManualPromise4 } = require("playwright-core/lib/coreBundle").iso; var { captureRawStack, stringifyStackFrames: stringifyStackFrames2 } = require("playwright-core/lib/coreBundle").iso; var { escapeWithQuotes: escapeWithQuotes2 } = require("playwright-core/lib/coreBundle").iso; var { monotonicTime: monotonicTime3 } = require("playwright-core/lib/coreBundle").iso; var { createGuid: createGuid2 } = require("playwright-core/lib/coreBundle").utils; var { sanitizeForFilePath: sanitizeForFilePath2, trimLongString } = require("playwright-core/lib/coreBundle").utils; var { currentZone } = require("playwright-core/lib/coreBundle").utils; var emtpyTestInfoCallbacks = { onStepBegin: () => { }, onStepEnd: () => { }, onAttach: () => { }, onTestPaused: () => Promise.reject(new Error("TestInfoImpl not initialized")) }; var TestInfoImpl = class { constructor(configInternal, projectInternal, workerParams, test, retry, callbacks) { this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} }; this._ariaSnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} }; this._interruptedPromise = new ManualPromise4(); this._lastStepId = 0; this._steps = []; this._stepMap = /* @__PURE__ */ new Map(); this._onDidFinishTestFunctionCallbacks = /* @__PURE__ */ new Set(); this._hasNonRetriableError = false; this._hasUnhandledError = false; this._allowSkips = false; this.duration = 0; this.annotations = []; this.attachments = []; this.status = "passed"; this.snapshotSuffix = ""; this.errors = []; this._ignoreTimeoutsCounter = 0; this.testId = test?.id ?? ""; this._callbacks = callbacks; this._startTime = monotonicTime3(); this._startWallTime = Date.now(); this._requireFile = test?._requireFile ?? ""; this._uniqueSymbol = Symbol("testInfoUniqueSymbol"); this._workerParams = workerParams; this.repeatEachIndex = workerParams.repeatEachIndex; this.retry = retry; this.workerIndex = workerParams.workerIndex; this.parallelIndex = workerParams.parallelIndex; this._projectInternal = projectInternal; this.project = projectInternal.project; this._configInternal = configInternal; this.config = configInternal.config; this.title = test?.title ?? ""; this.titlePath = test?.titlePath() ?? []; this.file = test?.location.file ?? ""; this.line = test?.location.line ?? 0; this.column = test?.location.column ?? 0; this.tags = test?.tags ?? []; this.fn = test?.fn ?? (() => { }); this.expectedStatus = test?.expectedStatus ?? "skipped"; this._timeoutManager = new TimeoutManager(this.project.timeout); if (configInternal.configCLIOverrides.debug === "inspector") this._setIgnoreTimeouts(true); this.outputDir = (() => { const relativeTestFilePath = import_path3.default.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/, "")); const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === "win32" ? new RegExp("\\\\", "g") : new RegExp("/", "g"), "-"); const fullTitleWithoutSpec = this.titlePath.slice(1).join(" "); let testOutputDir = trimLongString(sanitizedRelativePath + "-" + sanitizeForFilePath2(fullTitleWithoutSpec), windowsFilesystemFriendlyLength); if (projectInternal.id) testOutputDir += "-" + sanitizeForFilePath2(projectInternal.id); if (this.retry) testOutputDir += "-retry" + this.retry; if (this.repeatEachIndex) testOutputDir += "-repeat" + this.repeatEachIndex; return import_path3.default.join(this.project.outputDir, testOutputDir); })(); this.snapshotDir = (() => { const relativeTestFilePath = import_path3.default.relative(this.project.testDir, this._requireFile); return import_path3.default.join(this.project.snapshotDir, relativeTestFilePath + "-snapshots"); })(); this._attachmentsPush = this.attachments.push.bind(this.attachments); const attachmentsPush = (...attachments) => { for (const a of attachments) this._attach(a, this._parentStep()?.stepId); return this.attachments.length; }; Object.defineProperty(this.attachments, "push", { value: attachmentsPush, writable: true, enumerable: false, configurable: true }); this._tracing = new TestTracing(this, workerParams.artifactsDir); this.skip = import_common2.transform.wrapFunctionWithLocation((location, ...args) => this._modifier("skip", location, args)); this.fixme = import_common2.transform.wrapFunctionWithLocation((location, ...args) => this._modifier("fixme", location, args)); this.fail = import_common2.transform.wrapFunctionWithLocation((location, ...args) => this._modifier("fail", location, args)); this.slow = import_common2.transform.wrapFunctionWithLocation((location, ...args) => this._modifier("slow", location, args)); } get error() { return this.errors[0]; } set error(e) { if (e === void 0) throw new Error("Cannot assign testInfo.error undefined value!"); this.errors[0] = e; } get timeout() { return this._timeoutManager.defaultSlot().timeout; } set timeout(timeout) { } _deadline() { return { deadline: this._timeoutManager.currentSlotDeadline(), timeout: this.timeout }; } _modifier(type, location, modifierArgs) { if (typeof modifierArgs[1] === "function") { throw new Error([ "It looks like you are calling test.skip() inside the test and pass a callback.", "Pass a condition instead and optional description instead:", `test('my test', async ({ page, isMobile }) => {`, ` test.skip(isMobile, 'This test is not applicable on mobile');`, `});` ].join("\n")); } if (modifierArgs.length >= 1 && !modifierArgs[0]) return; const description = modifierArgs[1]; this.annotations.push({ type, description, location }); if (type === "slow") { this._timeoutManager.slow(); } else if (type === "skip" || type === "fixme") { this.expectedStatus = "skipped"; throw new TestSkipError("Test is skipped: " + (description || "")); } else if (type === "fail") { if (this.expectedStatus !== "skipped") this.expectedStatus = "failed"; } } _findLastPredefinedStep(steps) { for (let i = steps.length - 1; i >= 0; i--) { const child = this._findLastPredefinedStep(steps[i].steps); if (child) return child; if ((steps[i].category === "hook" || steps[i].category === "fixture") && !steps[i].endWallTime) return steps[i]; } } _parentStep() { return currentZone().data("stepZone") ?? this._findLastPredefinedStep(this._steps); } _addStep(data, parentStep) { const stepId = `${data.category}@${++this._lastStepId}`; if (data.category === "hook" || data.category === "fixture") { parentStep = this._findLastPredefinedStep(this._steps); } else { if (!parentStep) parentStep = this._parentStep(); } const filteredStack = filteredStackTrace(captureRawStack()); let boxedStack = parentStep?.boxedStack; let location = data.location; if (!boxedStack && data.box) { boxedStack = filteredStack.slice(1); location = location || boxedStack[0]; } location = location || filteredStack[0]; const step = { ...data, stepId, group: parentStep?.group ?? data.group, boxedStack, location, steps: [], attachmentIndices: [], info: new TestStepInfoImpl(this, stepId, data.title, parentStep?.info), complete: (result) => { if (step.endWallTime) return; step.endWallTime = Date.now(); if (result.attachments) { for (const attachment of result.attachments) this._attach(attachment, stepId); } if (result.error) { if (typeof result.error === "object" && !result.error?.[stepSymbol]) result.error[stepSymbol] = step; const error = testInfoError(result.error); if (step.boxedStack) error.stack = `${error.message} ${stringifyStackFrames2(step.boxedStack).join("\n")}`; step.error = error; } if (result.softError) { step.infectParentStepsWithError = true; this._failWithError(result.softError); } if (result.shouldNotRetryTest) this._hasNonRetriableError = true; if (!step.error) { for (const childStep of step.steps) { if (childStep.error && childStep.infectParentStepsWithError) { step.error = childStep.error; step.infectParentStepsWithError = true; break; } } } if (!step.group) { const payload = { testId: this.testId, stepId, wallTime: step.endWallTime, error: step.error ? import_common2.ipc.toTestInfoErrorPayload(step.error) : void 0, suggestedRebaseline: result.suggestedRebaseline, annotations: step.info.annotations }; this._callbacks.onStepEnd(payload); } if (step.group !== "internal") { const errorForTrace = step.error ? { name: "", message: step.error.message || "", stack: step.error.stack } : void 0; const attachments = step.attachmentIndices.map((i) => this.attachments[i]); this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations); } } }; const parentStepList = parentStep ? parentStep.steps : this._steps; parentStepList.push(step); this._stepMap.set(stepId, step); if (!step.group) { const payload = { testId: this.testId, stepId, parentStepId: parentStep ? parentStep.stepId : void 0, title: step.title, category: step.category, wallTime: Date.now(), location: step.location }; this._callbacks.onStepBegin(payload); } if (step.group !== "internal") { this._tracing.appendBeforeActionForStep({ stepId, parentId: parentStep?.stepId, title: step.shortTitle ?? step.title, category: step.category, params: step.params, stack: step.location ? [step.location] : [], group: step.group }); } return step; } _abort(location, message) { this.annotations.push({ type: "abort", description: message, location }); throw new TestAbortError("Test aborted" + (message ? ": " + message : "")); } _interrupt() { this._interruptedPromise.resolve(); this._timeoutManager.interrupt(); if (this.status === "passed") this.status = "interrupted"; } _failWithError(root) { if (this.status === "passed" || this.status === "skipped") this.status = root instanceof TimeoutManagerError ? "timedOut" : "failed"; const visit = (error) => { const serialized = testInfoError(error); const step = error === root && typeof error === "object" ? error?.[stepSymbol] : void 0; if (step && step.boxedStack) serialized.stack = `${error.name}: ${error.message} ${stringifyStackFrames2(step.boxedStack).join("\n")}`; this.errors.push(serialized); this._tracing.appendForError(serialized); const children = error?.errors; if (Array.isArray(children)) { for (const child of children) visit(child); } }; visit(root); } async _runAsStep(stepInfo, cb) { const step = this._addStep(stepInfo); try { await cb(); step.complete({}); } catch (error) { step.complete({ error }); throw error; } } async _runWithTimeout(runnable, cb) { try { await this._timeoutManager.withRunnable(runnable, async () => { try { await cb(); } catch (e) { if (this._allowSkips && e instanceof TestSkipError) { if (this.status === "passed") this.status = "skipped"; } else { this._failWithError(e); } throw e; } }); } catch (error) { if (!this._interruptedPromise.isDone() && error instanceof TimeoutManagerError) this._failWithError(error); throw error; } } _isFailure() { return this.status !== "skipped" && this.status !== this.expectedStatus; } _currentHookType() { const type = this._timeoutManager.currentSlotType(); return ["beforeAll", "afterAll", "beforeEach", "afterEach"].includes(type) ? type : void 0; } _setIgnoreTimeouts(ignoreTimeouts) { this._ignoreTimeoutsCounter += ignoreTimeouts ? 1 : -1; this._timeoutManager.setIgnoreTimeouts(this._ignoreTimeoutsCounter > 0); } async _didFinishTestFunction() { const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure(); if (shouldPause) { await Promise.race([ this._callbacks.onTestPaused({ testId: this.testId, errors: this._isFailure() ? this.errors.map(import_common2.ipc.toTestInfoErrorPayload) : [], status: this.status }), this._interruptedPromise ]); } for (const cb of this._onDidFinishTestFunctionCallbacks) await cb(); } // ------------ TestInfo methods ------------ async attach(name, options = {}) { const step = this._addStep({ title: `Attach ${escapeWithQuotes2(name, '"')}`, category: "test.attach" }); this._attach( await normalizeAndSaveAttachment(this.outputPath(), name, options), step.stepId ); step.complete({}); } _attach(attachment, stepId) { const index = this._attachmentsPush(attachment) - 1; let step = stepId ? this._stepMap.get(stepId) : void 0; if (!!step?.group) step = void 0; if (step) { step.attachmentIndices.push(index); } else { const stepId2 = `attach@${createGuid2()}`; this._tracing.appendBeforeActionForStep({ stepId: stepId2, title: `Attach ${escapeWithQuotes2(attachment.name, '"')}`, category: "test.attach", stack: [] }); this._tracing.appendAfterActionForStep(stepId2, void 0, [attachment]); } this._callbacks.onAttach({ testId: this.testId, name: attachment.name, contentType: attachment.contentType, path: attachment.path, body: attachment.body?.toString("base64"), stepId: step?.stepId }); } outputPath(...pathSegments) { const outputPath = this._getOutputPath(...pathSegments); import_fs3.default.mkdirSync(this.outputDir, { recursive: true }); return outputPath; } _getOutputPath(...pathSegments) { const joinedPath = import_path3.default.join(...pathSegments); const outputPath = getContainedPath(this.outputDir, joinedPath); if (outputPath) return outputPath; throw new Error(`The outputPath is not allowed outside of the parent directory. Please fix the defined path. outputPath: ${joinedPath}`); } _fsSanitizedTestName() { const fullTitleWithoutSpec = this.titlePath.slice(1).join(" "); return sanitizeForFilePath2(trimLongString(fullTitleWithoutSpec)); } _resolveSnapshotPaths(kind, name, updateSnapshotIndex, anonymousExtension) { const snapshotNames = kind === "aria" ? this._ariaSnapshotNames : this._snapshotNames; const defaultExtensions = { "aria": ".aria.yml", "screenshot": ".png", "snapshot": ".txt" }; const ariaAwareExtname = (filePath) => kind === "aria" && filePath.endsWith(".aria.yml") ? ".aria.yml" : import_path3.default.extname(filePath); let subPath; let ext; let relativeOutputPath; if (!name) { const index = snapshotNames.lastAnonymousSnapshotIndex + 1; if (updateSnapshotIndex === "updateSnapshotIndex") snapshotNames.lastAnonymousSnapshotIndex = index; const fullTitleWithoutSpec = [...this.titlePath.slice(1), index].join(" "); ext = anonymousExtension ?? defaultExtensions[kind]; subPath = sanitizeFilePathBeforeExtension(trimLongString(fullTitleWithoutSpec) + ext, ext); relativeOutputPath = sanitizeFilePathBeforeExtension(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength) + ext, ext); } else { if (Array.isArray(name)) { subPath = import_path3.default.join(...name); relativeOutputPath = import_path3.default.join(...name); ext = ariaAwareExtname(subPath); } else { ext = ariaAwareExtname(name); subPath = sanitizeFilePathBeforeExtension(name, ext); relativeOutputPath = sanitizeFilePathBeforeExtension(trimLongString(name, windowsFilesystemFriendlyLength), ext); } const index = (snapshotNames.lastNamedSnapshotIndex[relativeOutputPath] || 0) + 1; if (updateSnapshotIndex === "updateSnapshotIndex") snapshotNames.lastNamedSnapshotIndex[relativeOutputPath] = index; if (inde