playwright
Version:
A high-level API to automate web browsers
1,272 lines (1,262 loc) • 75.9 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __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