playwright-core
Version:
A high-level API to automate web browsers
605 lines (604 loc) • 22.4 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 __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var tracing_exports = {};
__export(tracing_exports, {
Tracing: () => Tracing
});
module.exports = __toCommonJS(tracing_exports);
var import_fs = __toESM(require("fs"));
var import_os = __toESM(require("os"));
var import_path = __toESM(require("path"));
var import_snapshotter = require("./snapshotter");
var import_protocolMetainfo = require("../../../utils/isomorphic/protocolMetainfo");
var import_assert = require("../../../utils/isomorphic/assert");
var import_time = require("../../../utils/isomorphic/time");
var import_eventsHelper = require("../../utils/eventsHelper");
var import_crypto = require("../../utils/crypto");
var import_userAgent = require("../../utils/userAgent");
var import_artifact = require("../../artifact");
var import_browserContext = require("../../browserContext");
var import_dispatcher = require("../../dispatchers/dispatcher");
var import_errors = require("../../errors");
var import_fileUtils = require("../../utils/fileUtils");
var import_harTracer = require("../../har/harTracer");
var import_instrumentation = require("../../instrumentation");
var import_page = require("../../page");
var import_progress = require("../../progress");
const version = 8;
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
class Tracing extends import_instrumentation.SdkObject {
constructor(context, tracesDir) {
super(context, "tracing");
this._fs = new import_fileUtils.SerializedFS();
this._screencastListeners = [];
this._eventListeners = [];
this._isStopping = false;
this._allResources = /* @__PURE__ */ new Set();
this._pendingHarEntries = /* @__PURE__ */ new Set();
this._context = context;
this._precreatedTracesDir = tracesDir;
this._harTracer = new import_harTracer.HarTracer(context, null, this, {
content: "attach",
includeTraceInfo: true,
recordRequestOverrides: false,
waitForContentOnStop: false
});
const testIdAttributeName = "selectors" in context ? context.selectors().testIdAttributeName() : void 0;
this._contextCreatedEvent = {
version,
type: "context-options",
origin: "library",
browserName: "",
playwrightVersion: (0, import_userAgent.getPlaywrightVersion)(),
options: {},
platform: process.platform,
wallTime: 0,
monotonicTime: 0,
sdkLanguage: this._sdkLanguage(),
testIdAttributeName,
contextId: context.guid
};
if (context instanceof import_browserContext.BrowserContext) {
this._snapshotter = new import_snapshotter.Snapshotter(context, this);
(0, import_assert.assert)(tracesDir, "tracesDir must be specified for BrowserContext");
this._contextCreatedEvent.browserName = context._browser.options.name;
this._contextCreatedEvent.channel = context._browser.options.channel;
this._contextCreatedEvent.options = context._options;
}
}
_sdkLanguage() {
return this._context instanceof import_browserContext.BrowserContext ? this._context._browser.sdkLanguage() : this._context.attribution.playwright.options.sdkLanguage;
}
async resetForReuse(progress) {
await this.stopChunk(progress, { mode: "discard" }).catch(() => {
});
await this.stop(progress);
if (this._snapshotter)
await progress.race(this._snapshotter.resetForReuse());
}
start(options) {
if (this._isStopping)
throw new Error("Cannot start tracing while stopping");
if (this._state)
throw new Error("Tracing has been already started");
this._contextCreatedEvent.sdkLanguage = this._sdkLanguage();
const traceName = options.name || (0, import_crypto.createGuid)();
const tracesDir = this._createTracesDirIfNeeded();
this._state = {
options,
traceName,
tracesDir,
traceFile: import_path.default.join(tracesDir, traceName + ".trace"),
networkFile: import_path.default.join(tracesDir, traceName + ".network"),
resourcesDir: import_path.default.join(tracesDir, "resources"),
chunkOrdinal: 0,
traceSha1s: /* @__PURE__ */ new Set(),
networkSha1s: /* @__PURE__ */ new Set(),
recording: false,
callIds: /* @__PURE__ */ new Set(),
groupStack: []
};
this._fs.mkdir(this._state.resourcesDir);
this._fs.writeFile(this._state.networkFile, "");
if (options.snapshots)
this._harTracer.start({ omitScripts: !options.live });
}
async startChunk(progress, options = {}) {
if (this._state && this._state.recording)
await this.stopChunk(progress, { mode: "discard" });
if (!this._state)
throw new Error("Must start tracing before starting a new chunk");
if (this._isStopping)
throw new Error("Cannot start a trace chunk while stopping");
this._state.recording = true;
this._state.callIds.clear();
const preserveNetworkResources = this._context instanceof import_browserContext.BrowserContext;
if (options.name && options.name !== this._state.traceName)
this._changeTraceName(this._state, options.name, preserveNetworkResources);
else
this._allocateNewTraceFile(this._state);
if (!preserveNetworkResources)
this._fs.writeFile(this._state.networkFile, "");
this._fs.mkdir(import_path.default.dirname(this._state.traceFile));
const event = {
...this._contextCreatedEvent,
title: options.title,
wallTime: Date.now(),
monotonicTime: (0, import_time.monotonicTime)()
};
this._appendTraceEvent(event);
this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push(
import_eventsHelper.eventsHelper.addEventListener(this._context, import_browserContext.BrowserContext.Events.Console, this._onConsoleMessage.bind(this)),
import_eventsHelper.eventsHelper.addEventListener(this._context, import_browserContext.BrowserContext.Events.PageError, this._onPageError.bind(this))
);
if (this._state.options.screenshots)
this._startScreencast();
if (this._state.options.snapshots)
await this._snapshotter?.start();
return { traceName: this._state.traceName };
}
_currentGroupId() {
return this._state?.groupStack.length ? this._state.groupStack[this._state.groupStack.length - 1] : void 0;
}
group(name, location, metadata) {
if (!this._state)
return;
const stackFrames = [];
const { file, line, column } = location ?? metadata.location ?? {};
if (file) {
stackFrames.push({
file,
line: line ?? 0,
column: column ?? 0
});
}
const event = {
type: "before",
callId: metadata.id,
startTime: metadata.startTime,
title: name,
class: "Tracing",
method: "tracingGroup",
params: {},
stepId: metadata.stepId,
stack: stackFrames
};
if (this._currentGroupId())
event.parentId = this._currentGroupId();
this._state.groupStack.push(event.callId);
this._appendTraceEvent(event);
}
groupEnd() {
if (!this._state)
return;
const callId = this._state.groupStack.pop();
if (!callId)
return;
const event = {
type: "after",
callId,
endTime: (0, import_time.monotonicTime)()
};
this._appendTraceEvent(event);
}
_startScreencast() {
if (!(this._context instanceof import_browserContext.BrowserContext))
return;
for (const page of this._context.pages())
this._startScreencastInPage(page);
this._screencastListeners.push(
import_eventsHelper.eventsHelper.addEventListener(this._context, import_browserContext.BrowserContext.Events.Page, this._startScreencastInPage.bind(this))
);
}
_stopScreencast() {
import_eventsHelper.eventsHelper.removeEventListeners(this._screencastListeners);
if (!(this._context instanceof import_browserContext.BrowserContext))
return;
for (const page of this._context.pages())
page.screencast.setOptions(null);
}
_allocateNewTraceFile(state) {
const suffix = state.chunkOrdinal ? `-chunk${state.chunkOrdinal}` : ``;
state.chunkOrdinal++;
state.traceFile = import_path.default.join(state.tracesDir, `${state.traceName}${suffix}.trace`);
}
_changeTraceName(state, name, preserveNetworkResources) {
state.traceName = name;
state.chunkOrdinal = 0;
this._allocateNewTraceFile(state);
const newNetworkFile = import_path.default.join(state.tracesDir, name + ".network");
if (preserveNetworkResources)
this._fs.copyFile(state.networkFile, newNetworkFile);
state.networkFile = newNetworkFile;
}
async stop(progress) {
if (!this._state)
return;
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
if (this._state.recording)
throw new Error(`Must stop trace file before stopping tracing`);
this._closeAllGroups();
this._harTracer.stop();
this.flushHarEntries();
const promise = progress ? progress.race(this._fs.syncAndGetError()) : this._fs.syncAndGetError();
await promise.finally(() => {
this._state = void 0;
});
}
async deleteTmpTracesDir() {
if (this._tracesTmpDir)
await (0, import_fileUtils.removeFolders)([this._tracesTmpDir]);
}
_createTracesDirIfNeeded() {
if (this._precreatedTracesDir)
return this._precreatedTracesDir;
this._tracesTmpDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "playwright-tracing-"));
return this._tracesTmpDir;
}
abort() {
this._snapshotter?.dispose();
this._harTracer.stop();
}
async flush() {
this.abort();
await this._fs.syncAndGetError();
}
_closeAllGroups() {
while (this._currentGroupId())
this.groupEnd();
}
async stopChunk(progress, params) {
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
this._isStopping = true;
if (!this._state || !this._state.recording) {
this._isStopping = false;
if (params.mode !== "discard")
throw new Error(`Must start tracing before stopping`);
return {};
}
this._closeAllGroups();
this._context.instrumentation.removeListener(this);
import_eventsHelper.eventsHelper.removeEventListeners(this._eventListeners);
if (this._state.options.screenshots)
this._stopScreencast();
if (this._state.options.snapshots)
this._snapshotter?.stop();
this.flushHarEntries();
const newNetworkFile = import_path.default.join(this._state.tracesDir, this._state.traceName + `-pwnetcopy-${this._state.chunkOrdinal}.network`);
const entries = [];
entries.push({ name: "trace.trace", value: this._state.traceFile });
entries.push({ name: "trace.network", value: newNetworkFile });
for (const sha1 of /* @__PURE__ */ new Set([...this._state.traceSha1s, ...this._state.networkSha1s]))
entries.push({ name: import_path.default.join("resources", sha1), value: import_path.default.join(this._state.resourcesDir, sha1) });
this._state.traceSha1s = /* @__PURE__ */ new Set();
if (params.mode === "discard") {
this._isStopping = false;
this._state.recording = false;
return {};
}
this._fs.copyFile(this._state.networkFile, newNetworkFile);
const zipFileName = this._state.traceFile + ".zip";
if (params.mode === "archive")
this._fs.zip(entries, zipFileName);
const promise = progress ? progress.race(this._fs.syncAndGetError()) : this._fs.syncAndGetError();
const error = await promise.catch((e) => e);
this._isStopping = false;
if (this._state)
this._state.recording = false;
if (error) {
if (!(0, import_progress.isAbortError)(error) && this._context instanceof import_browserContext.BrowserContext && !this._context._browser.isConnected())
return {};
throw error;
}
if (params.mode === "entries")
return { entries };
const artifact = new import_artifact.Artifact(this._context, zipFileName);
artifact.reportFinished();
return { artifact };
}
async _captureSnapshot(snapshotName, sdkObject, metadata) {
if (!snapshotName || !sdkObject.attribution.page)
return;
await this._snapshotter?.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {
});
}
_shouldCaptureSnapshot(sdkObject, metadata) {
return !!this._snapshotter?.started() && shouldCaptureSnapshot(metadata) && !!sdkObject.attribution.page;
}
onBeforeCall(sdkObject, metadata, parentId) {
const event = createBeforeActionTraceEvent(metadata, parentId ?? this._currentGroupId());
if (!event)
return Promise.resolve();
sdkObject.attribution.page?.screencast.temporarilyDisableThrottling();
if (this._shouldCaptureSnapshot(sdkObject, metadata))
event.beforeSnapshot = `before@${metadata.id}`;
this._state?.callIds.add(metadata.id);
this._appendTraceEvent(event);
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
}
onBeforeInputAction(sdkObject, metadata) {
if (!this._state?.callIds.has(metadata.id))
return Promise.resolve();
const event = createInputActionTraceEvent(metadata);
if (!event)
return Promise.resolve();
sdkObject.attribution.page?.screencast.temporarilyDisableThrottling();
if (this._shouldCaptureSnapshot(sdkObject, metadata))
event.inputSnapshot = `input@${metadata.id}`;
this._appendTraceEvent(event);
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata);
}
onCallLog(sdkObject, metadata, logName, message) {
if (!this._state?.callIds.has(metadata.id))
return;
if (metadata.internal)
return;
if (logName !== "api")
return;
const event = createActionLogTraceEvent(metadata, message);
if (event)
this._appendTraceEvent(event);
}
onAfterCall(sdkObject, metadata) {
if (!this._state?.callIds.has(metadata.id))
return Promise.resolve();
this._state?.callIds.delete(metadata.id);
const event = createAfterActionTraceEvent(metadata);
if (!event)
return Promise.resolve();
sdkObject.attribution.page?.screencast.temporarilyDisableThrottling();
if (this._shouldCaptureSnapshot(sdkObject, metadata))
event.afterSnapshot = `after@${metadata.id}`;
this._appendTraceEvent(event);
return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata);
}
onEntryStarted(entry) {
this._pendingHarEntries.add(entry);
}
onEntryFinished(entry) {
this._pendingHarEntries.delete(entry);
const event = { type: "resource-snapshot", snapshot: entry };
const visited = visitTraceEvent(event, this._state.networkSha1s);
this._fs.appendFile(
this._state.networkFile,
JSON.stringify(visited) + "\n",
true
/* flush */
);
}
flushHarEntries() {
const harLines = [];
for (const entry of this._pendingHarEntries) {
const event = { type: "resource-snapshot", snapshot: entry };
const visited = visitTraceEvent(event, this._state.networkSha1s);
harLines.push(JSON.stringify(visited));
}
this._pendingHarEntries.clear();
if (harLines.length)
this._fs.appendFile(
this._state.networkFile,
harLines.join("\n") + "\n",
true
/* flush */
);
}
onContentBlob(sha1, buffer) {
this._appendResource(sha1, buffer);
}
onSnapshotterBlob(blob) {
this._appendResource(blob.sha1, blob.buffer);
}
onFrameSnapshot(snapshot) {
this._appendTraceEvent({ type: "frame-snapshot", snapshot });
}
_onConsoleMessage(message) {
const event = {
type: "console",
messageType: message.type(),
text: message.text(),
args: message.args().map((a) => ({ preview: a.toString(), value: a.rawValue() })),
location: message.location(),
time: (0, import_time.monotonicTime)(),
pageId: message.page()?.guid
};
this._appendTraceEvent(event);
}
onDialog(dialog) {
const event = {
type: "event",
time: (0, import_time.monotonicTime)(),
class: "BrowserContext",
method: "dialog",
params: { pageId: dialog.page().guid, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue() }
};
this._appendTraceEvent(event);
}
onDownload(page, download) {
const event = {
type: "event",
time: (0, import_time.monotonicTime)(),
class: "BrowserContext",
method: "download",
params: { pageId: page.guid, url: download.url, suggestedFilename: download.suggestedFilename() }
};
this._appendTraceEvent(event);
}
onPageOpen(page) {
const event = {
type: "event",
time: (0, import_time.monotonicTime)(),
class: "BrowserContext",
method: "page",
params: { pageId: page.guid, openerPageId: page.opener()?.guid }
};
this._appendTraceEvent(event);
}
onPageClose(page) {
const event = {
type: "event",
time: (0, import_time.monotonicTime)(),
class: "BrowserContext",
method: "pageClosed",
params: { pageId: page.guid }
};
this._appendTraceEvent(event);
}
_onPageError(error, page) {
const event = {
type: "event",
time: (0, import_time.monotonicTime)(),
class: "BrowserContext",
method: "pageError",
params: { error: (0, import_errors.serializeError)(error) },
pageId: page.guid
};
this._appendTraceEvent(event);
}
_startScreencastInPage(page) {
page.screencast.setOptions(kScreencastOptions);
const prefix = page.guid;
this._screencastListeners.push(
import_eventsHelper.eventsHelper.addEventListener(page, import_page.Page.Events.ScreencastFrame, (params) => {
const suffix = params.timestamp || Date.now();
const sha1 = `${prefix}-${suffix}.jpeg`;
const event = {
type: "screencast-frame",
pageId: page.guid,
sha1,
width: params.width,
height: params.height,
timestamp: (0, import_time.monotonicTime)(),
frameSwapWallTime: params.frameSwapWallTime
};
this._appendResource(sha1, params.buffer);
this._appendTraceEvent(event);
})
);
}
_appendTraceEvent(event) {
const visited = visitTraceEvent(event, this._state.traceSha1s);
const flush = this._state.options.live || event.type !== "event" && event.type !== "console" && event.type !== "log";
this._fs.appendFile(this._state.traceFile, JSON.stringify(visited) + "\n", flush);
}
_appendResource(sha1, buffer) {
if (this._allResources.has(sha1))
return;
this._allResources.add(sha1);
const resourcePath = import_path.default.join(this._state.resourcesDir, sha1);
this._fs.writeFile(
resourcePath,
buffer,
true
/* skipIfExists */
);
}
}
function visitTraceEvent(object, sha1s) {
if (Array.isArray(object))
return object.map((o) => visitTraceEvent(o, sha1s));
if (object instanceof import_dispatcher.Dispatcher)
return `<${object._type}>`;
if (object instanceof Buffer)
return `<Buffer>`;
if (object instanceof Date)
return object;
if (typeof object === "object") {
const result = {};
for (const key in object) {
if (key === "sha1" || key === "_sha1" || key.endsWith("Sha1")) {
const sha1 = object[key];
if (sha1)
sha1s.add(sha1);
}
result[key] = visitTraceEvent(object[key], sha1s);
}
return result;
}
return object;
}
function shouldCaptureSnapshot(metadata) {
const metainfo = import_protocolMetainfo.methodMetainfo.get(metadata.type + "." + metadata.method);
return !!metainfo?.snapshot;
}
function createBeforeActionTraceEvent(metadata, parentId) {
if (metadata.internal || metadata.method.startsWith("tracing"))
return null;
const event = {
type: "before",
callId: metadata.id,
startTime: metadata.startTime,
title: metadata.title,
class: metadata.type,
method: metadata.method,
params: metadata.params,
stepId: metadata.stepId,
pageId: metadata.pageId
};
if (parentId)
event.parentId = parentId;
return event;
}
function createInputActionTraceEvent(metadata) {
if (metadata.internal || metadata.method.startsWith("tracing"))
return null;
return {
type: "input",
callId: metadata.id,
point: metadata.point
};
}
function createActionLogTraceEvent(metadata, message) {
if (metadata.internal || metadata.method.startsWith("tracing"))
return null;
return {
type: "log",
callId: metadata.id,
time: (0, import_time.monotonicTime)(),
message
};
}
function createAfterActionTraceEvent(metadata) {
if (metadata.internal || metadata.method.startsWith("tracing"))
return null;
return {
type: "after",
callId: metadata.id,
endTime: metadata.endTime,
error: metadata.error?.error,
result: metadata.result,
point: metadata.point
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Tracing
});