playwright 1.58.0-alpha-2025-12-09 → 1.58.0-alpha-2025-12-10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -164,7 +164,7 @@ const playwrightFixtures = {
164
164
  baseURL,
165
165
  contextOptions,
166
166
  serviceWorkers
167
- }, use) => {
167
+ }, use, testInfo) => {
168
168
  const options = {};
169
169
  if (acceptDownloads !== void 0)
170
170
  options.acceptDownloads = acceptDownloads;
@@ -212,10 +212,19 @@ const playwrightFixtures = {
212
212
  options.baseURL = baseURL;
213
213
  if (serviceWorkers !== void 0)
214
214
  options.serviceWorkers = serviceWorkers;
215
+ const workerFile = agent?.cacheFile && agent.cacheMode !== "ignore" ? await testInfo._cloneStorage(agent.cacheFile) : void 0;
216
+ if (agent && workerFile) {
217
+ options.agent = {
218
+ ...agent,
219
+ cacheFile: workerFile
220
+ };
221
+ }
215
222
  await use({
216
223
  ...contextOptions,
217
224
  ...options
218
225
  });
226
+ if (workerFile)
227
+ await testInfo._upstreamStorage(workerFile);
219
228
  }, { box: true }],
220
229
  _setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
221
230
  if (testIdAttribute)
@@ -58,6 +58,10 @@ class TeleReporterReceiver {
58
58
  this._onTestBegin(params.testId, params.result);
59
59
  return;
60
60
  }
61
+ if (method === "onTestPaused") {
62
+ this._onTestPaused(params.testId, params.resultId, params.stepId, params.errors);
63
+ return;
64
+ }
61
65
  if (method === "onTestEnd") {
62
66
  this._onTestEnd(params.test, params.result);
63
67
  return;
@@ -116,6 +120,14 @@ class TeleReporterReceiver {
116
120
  testResult.setStartTimeNumber(payload.startTime);
117
121
  this._reporter.onTestBegin?.(test, testResult);
118
122
  }
123
+ _onTestPaused(testId, resultId, stepId, errors) {
124
+ const test = this._tests.get(testId);
125
+ const result = test.results.find((r) => r._id === resultId);
126
+ const step = result._stepMap.get(stepId);
127
+ result.errors.push(...errors);
128
+ result.error = result.errors[0];
129
+ void this._reporter.onTestPaused?.(test, result, step);
130
+ }
119
131
  _onTestEnd(testEndPayload, payload) {
120
132
  const test = this._tests.get(testEndPayload.testId);
121
133
  test.timeout = testEndPayload.timeout;
@@ -123,8 +135,8 @@ class TeleReporterReceiver {
123
135
  const result = test.results.find((r) => r._id === payload.id);
124
136
  result.duration = payload.duration;
125
137
  result.status = payload.status;
126
- result.errors = payload.errors;
127
- result.error = result.errors?.[0];
138
+ result.errors.push(...payload.errors ?? []);
139
+ result.error = result.errors[0];
128
140
  if (!!payload.attachments)
129
141
  result.attachments = this._parseAttachments(payload.attachments);
130
142
  if (payload.annotations) {
@@ -272,6 +272,7 @@ function outputDir(config, clientInfo) {
272
272
  }
273
273
  async function outputFile(config, clientInfo, fileName, options) {
274
274
  const file = await resolveFile(config, clientInfo, fileName, options);
275
+ await import_fs.default.promises.mkdir(import_path.default.dirname(file), { recursive: true });
275
276
  (0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
276
277
  return file;
277
278
  }
@@ -33,7 +33,6 @@ __export(context_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(context_exports);
35
35
  var import_fs = __toESM(require("fs"));
36
- var import_path = __toESM(require("path"));
37
36
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
38
37
  var import_playwright_core = require("playwright-core");
39
38
  var import_log = require("../log");
@@ -143,7 +142,6 @@ class Context {
143
142
  await close(async () => {
144
143
  for (const video of videos) {
145
144
  const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
146
- await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
147
145
  const p = await video.path();
148
146
  if (import_fs.default.existsSync(p)) {
149
147
  try {
@@ -32,8 +32,10 @@ class Response {
32
32
  this._result = [];
33
33
  this._code = [];
34
34
  this._images = [];
35
+ this._files = [];
35
36
  this._includeSnapshot = "none";
36
37
  this._includeTabs = false;
38
+ this._includeMetaOnly = false;
37
39
  this._context = context;
38
40
  this.toolName = toolName;
39
41
  this.toolArgs = toolArgs;
@@ -63,6 +65,11 @@ class Response {
63
65
  images() {
64
66
  return this._images;
65
67
  }
68
+ async addFile(fileName, options) {
69
+ const resolvedFile = await this._context.outputFile(fileName, options);
70
+ this._files.push({ fileName: resolvedFile, title: options.reason });
71
+ return resolvedFile;
72
+ }
66
73
  setIncludeSnapshot() {
67
74
  this._includeSnapshot = this._context.config.snapshot.mode;
68
75
  }
@@ -75,6 +82,9 @@ class Response {
75
82
  setIncludeModalStates(modalStates) {
76
83
  this._includeModalStates = modalStates;
77
84
  }
85
+ setIncludeMetaOnly() {
86
+ this._includeMetaOnly = true;
87
+ }
78
88
  async finish() {
79
89
  if (this._includeSnapshot !== "none" && this._context.currentTab())
80
90
  this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
@@ -90,9 +100,9 @@ class Response {
90
100
  }
91
101
  logEnd() {
92
102
  if (requestDebug.enabled)
93
- requestDebug(this.serialize({ omitSnapshot: true, omitBlobs: true }));
103
+ requestDebug(this.serialize());
94
104
  }
95
- serialize(options = {}) {
105
+ render() {
96
106
  const renderedResponse = new RenderedResponse();
97
107
  if (this._result.length)
98
108
  renderedResponse.results.push(...this._result);
@@ -107,21 +117,34 @@ class Response {
107
117
  const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._tabSnapshot.modalStates);
108
118
  renderedResponse.states.modal = modalStatesMarkdown.join("\n");
109
119
  } else if (this._tabSnapshot) {
110
- const includeSnapshot = options.omitSnapshot ? "none" : this._includeSnapshot;
111
- renderTabSnapshot(this._tabSnapshot, includeSnapshot, renderedResponse);
120
+ renderTabSnapshot(this._tabSnapshot, this._includeSnapshot, renderedResponse);
112
121
  } else if (this._includeModalStates) {
113
122
  const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._includeModalStates);
114
123
  renderedResponse.states.modal = modalStatesMarkdown.join("\n");
115
124
  }
116
- const redactedResponse = this._context.config.secrets ? renderedResponse.redact(this._context.config.secrets) : renderedResponse;
125
+ if (this._files.length) {
126
+ const lines = [];
127
+ for (const file of this._files)
128
+ lines.push(`- [${file.title}](${file.fileName})`);
129
+ renderedResponse.updates.push({ category: "files", content: lines.join("\n") });
130
+ }
131
+ return this._context.config.secrets ? renderedResponse.redact(this._context.config.secrets) : renderedResponse;
132
+ }
133
+ serialize(options = {}) {
134
+ const renderedResponse = this.render();
117
135
  const includeMeta = options._meta && "dev.lowire/history" in options._meta && "dev.lowire/state" in options._meta;
118
- const _meta = includeMeta ? redactedResponse.asMeta() : void 0;
136
+ const _meta = includeMeta ? renderedResponse.asMeta() : void 0;
119
137
  const content = [
120
- { type: "text", text: redactedResponse.asText() }
138
+ {
139
+ type: "text",
140
+ text: renderedResponse.asText(this._includeMetaOnly ? { categories: ["files"] } : void 0)
141
+ }
121
142
  ];
143
+ if (this._includeMetaOnly)
144
+ return { _meta, content, isError: this._isError };
122
145
  if (this._context.config.imageResponses !== "omit") {
123
146
  for (const image of this._images)
124
- content.push({ type: "image", data: options.omitBlobs ? "<blob>" : image.data.toString("base64"), mimeType: image.contentType });
147
+ content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
125
148
  }
126
149
  return {
127
150
  _meta,
@@ -195,7 +218,7 @@ class RenderedResponse {
195
218
  this.code = copy.code;
196
219
  }
197
220
  }
198
- asText() {
221
+ asText(filter) {
199
222
  const text = [];
200
223
  if (this.results.length)
201
224
  text.push(`### Result
@@ -206,6 +229,8 @@ ${this.results.join("\n")}
206
229
  ${this.code.join("\n")}
207
230
  `);
208
231
  for (const { category, content } of this.updates) {
232
+ if (filter && !filter.categories.includes(category))
233
+ continue;
209
234
  if (!content.trim())
210
235
  continue;
211
236
  switch (category) {
@@ -217,11 +242,18 @@ ${content}
217
242
  case "downloads":
218
243
  text.push(`### Downloads
219
244
  ${content}
245
+ `);
246
+ break;
247
+ case "files":
248
+ text.push(`### Files
249
+ ${content}
220
250
  `);
221
251
  break;
222
252
  }
223
253
  }
224
254
  for (const [category, value] of Object.entries(this.states)) {
255
+ if (filter && !filter.categories.includes(category))
256
+ continue;
225
257
  if (!value.trim())
226
258
  continue;
227
259
  switch (category) {
@@ -291,6 +323,7 @@ function parseResponse(response) {
291
323
  const consoleMessages = sections.get("New console messages");
292
324
  const modalState = sections.get("Modal state");
293
325
  const downloads = sections.get("Downloads");
326
+ const files = sections.get("Files");
294
327
  const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
295
328
  const isError = response.isError;
296
329
  const attachments = response.content.slice(1);
@@ -302,6 +335,7 @@ function parseResponse(response) {
302
335
  consoleMessages,
303
336
  modalState,
304
337
  downloads,
338
+ files,
305
339
  isError,
306
340
  attachments,
307
341
  _meta: response._meta
@@ -48,9 +48,8 @@ const pdf = (0, import_tool.defineTabTool)({
48
48
  type: "readOnly"
49
49
  },
50
50
  handle: async (tab, params, response) => {
51
- const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)("pdf"), { origin: "llm", reason: "Saving PDF" });
51
+ const fileName = await response.addFile(params.filename ?? (0, import_utils.dateAsFileName)("pdf"), { origin: "llm", reason: "Page saved as PDF" });
52
52
  response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
53
- response.addResult(`Saved page as ${fileName}`);
54
53
  await tab.page.pdf({ path: fileName });
55
54
  }
56
55
  });
@@ -61,7 +61,6 @@ const screenshot = (0, import_tool.defineTabTool)({
61
61
  if (params.fullPage && params.ref)
62
62
  throw new Error("fullPage cannot be used with element screenshots.");
63
63
  const fileType = params.type || "png";
64
- const fileName = await tab.context.outputFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
65
64
  const options = {
66
65
  type: fileType,
67
66
  quality: fileType === "png" ? void 0 : 90,
@@ -70,6 +69,7 @@ const screenshot = (0, import_tool.defineTabTool)({
70
69
  };
71
70
  const isElementScreenshot = params.element && params.ref;
72
71
  const screenshotTarget = isElementScreenshot ? params.element : params.fullPage ? "full page" : "viewport";
72
+ const fileName = await response.addFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: `Screenshot of ${screenshotTarget}` });
73
73
  response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
74
74
  const ref = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
75
75
  if (ref)
@@ -79,7 +79,6 @@ const screenshot = (0, import_tool.defineTabTool)({
79
79
  const buffer = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
80
80
  await (0, import_utils.mkdirIfNeeded)(fileName);
81
81
  await import_fs.default.promises.writeFile(fileName, buffer);
82
- response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
83
82
  response.addImage({
84
83
  contentType: fileType === "png" ? "image/png" : "image/jpeg",
85
84
  data: scaleImageToFitMessage(buffer, fileType)
@@ -32,6 +32,7 @@ __export(snapshot_exports, {
32
32
  elementSchema: () => elementSchema
33
33
  });
34
34
  module.exports = __toCommonJS(snapshot_exports);
35
+ var import_fs = __toESM(require("fs"));
35
36
  var import_mcpBundle = require("playwright-core/lib/mcpBundle");
36
37
  var import_tool = require("./tool");
37
38
  var javascript = __toESM(require("../codegen"));
@@ -41,12 +42,20 @@ const snapshot = (0, import_tool.defineTool)({
41
42
  name: "browser_snapshot",
42
43
  title: "Page snapshot",
43
44
  description: "Capture accessibility snapshot of the current page, this is better than screenshot",
44
- inputSchema: import_mcpBundle.z.object({}),
45
+ inputSchema: import_mcpBundle.z.object({
46
+ filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
47
+ }),
45
48
  type: "readOnly"
46
49
  },
47
50
  handle: async (context, params, response) => {
48
51
  await context.ensureTab();
49
52
  response.setIncludeFullSnapshot();
53
+ if (params.filename) {
54
+ const renderedResponse = response.render();
55
+ const fileName = await response.addFile(params.filename, { origin: "llm", reason: "Saved snapshot" });
56
+ await import_fs.default.promises.writeFile(fileName, renderedResponse.asText());
57
+ response.setIncludeMetaOnly();
58
+ }
50
59
  }
51
60
  });
52
61
  const elementSchema = import_mcpBundle.z.object({
@@ -24,55 +24,39 @@ __export(utils_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(utils_exports);
26
26
  async function waitForCompletion(tab, callback) {
27
- const requests = /* @__PURE__ */ new Set();
28
- let frameNavigated = false;
29
- let waitCallback = () => {
30
- };
31
- const waitBarrier = new Promise((f) => {
32
- waitCallback = f;
33
- });
34
- const responseListener = (request) => {
35
- requests.delete(request);
36
- if (!requests.size)
37
- waitCallback();
38
- };
39
- const requestListener = (request) => {
40
- requests.add(request);
41
- void request.response().then(() => responseListener(request)).catch(() => {
42
- });
43
- };
44
- const frameNavigateListener = (frame) => {
45
- if (frame.parentFrame())
46
- return;
47
- frameNavigated = true;
48
- dispose();
49
- clearTimeout(timeout);
50
- void tab.waitForLoadState("load").then(waitCallback);
51
- };
52
- const onTimeout = () => {
53
- dispose();
54
- waitCallback();
55
- };
56
- tab.page.on("request", requestListener);
57
- tab.page.on("requestfailed", responseListener);
58
- tab.page.on("framenavigated", frameNavigateListener);
59
- const timeout = setTimeout(onTimeout, 1e4);
60
- const dispose = () => {
27
+ const requests = [];
28
+ const requestListener = (request) => requests.push(request);
29
+ const disposeListeners = () => {
61
30
  tab.page.off("request", requestListener);
62
- tab.page.off("requestfailed", responseListener);
63
- tab.page.off("framenavigated", frameNavigateListener);
64
- clearTimeout(timeout);
65
31
  };
32
+ tab.page.on("request", requestListener);
33
+ let result;
66
34
  try {
67
- const result = await callback();
68
- if (!requests.size && !frameNavigated)
69
- waitCallback();
70
- await waitBarrier;
71
- await tab.waitForTimeout(1e3);
72
- return result;
35
+ result = await callback();
36
+ await tab.waitForTimeout(500);
73
37
  } finally {
74
- dispose();
38
+ disposeListeners();
39
+ }
40
+ const requestedNavigation = requests.some((request) => request.isNavigationRequest());
41
+ if (requestedNavigation) {
42
+ await tab.page.mainFrame().waitForLoadState("load", { timeout: 1e4 }).catch(() => {
43
+ });
44
+ return result;
45
+ }
46
+ const promises = [];
47
+ for (const request of requests) {
48
+ if (["document", "stylesheet", "script", "xhr", "fetch"].includes(request.resourceType()))
49
+ promises.push(request.response().then((r) => r?.finished()).catch(() => {
50
+ }));
51
+ else
52
+ promises.push(request.response().catch(() => {
53
+ }));
75
54
  }
55
+ const timeout = new Promise((resolve) => setTimeout(resolve, 5e3));
56
+ await Promise.race([Promise.all(promises), timeout]);
57
+ if (requests.length)
58
+ await tab.waitForTimeout(500);
59
+ return result;
76
60
  }
77
61
  async function callOnPageNoTrace(page, callback) {
78
62
  return await page._wrapApiCall(() => callback(page), { internal: true });
@@ -68,6 +68,9 @@ class BlobReporter extends import_teleEmitter.TeleReporterEmitter {
68
68
  this._config = config;
69
69
  super.onConfigure(config);
70
70
  }
71
+ async onTestPaused(test, result, step) {
72
+ return { action: void 0 };
73
+ }
71
74
  async onEnd(result) {
72
75
  await super.onEnd(result);
73
76
  const zipFileName = await this._prepareOutputFile();
@@ -66,6 +66,12 @@ class InternalReporter {
66
66
  onStdErr(chunk, test, result) {
67
67
  this._reporter.onStdErr?.(chunk, test, result);
68
68
  }
69
+ async onTestPaused(test, result, step) {
70
+ this._addSnippetToTestErrors(test, result);
71
+ if (step)
72
+ this._addSnippetToStepError(test, step);
73
+ return await this._reporter.onTestPaused?.(test, result, step) ?? { action: void 0 };
74
+ }
69
75
  onTestEnd(test, result) {
70
76
  this._addSnippetToTestErrors(test, result);
71
77
  this._reporter.onTestEnd?.(test, result);
@@ -112,6 +118,8 @@ function addLocationAndSnippetToError(config, error, file) {
112
118
  const location = error.location;
113
119
  if (!location)
114
120
  return;
121
+ if (!!error.snippet)
122
+ return;
115
123
  try {
116
124
  const tokens = [];
117
125
  const source = import_fs.default.readFileSync(location.file, "utf8");
@@ -48,6 +48,14 @@ class Multiplexer {
48
48
  for (const reporter of this._reporters)
49
49
  wrap(() => reporter.onStdErr?.(chunk, test, result));
50
50
  }
51
+ async onTestPaused(test, result, step) {
52
+ for (const reporter of this._reporters) {
53
+ const disposition = await wrapAsync(() => reporter.onTestPaused?.(test, result, step));
54
+ if (disposition?.action)
55
+ return disposition;
56
+ }
57
+ return { action: void 0 };
58
+ }
51
59
  onTestEnd(test, result) {
52
60
  for (const reporter of this._reporters)
53
61
  wrap(() => reporter.onTestEnd?.(test, result));
@@ -37,7 +37,8 @@ var import_teleReceiver = require("../isomorphic/teleReceiver");
37
37
  class TeleReporterEmitter {
38
38
  constructor(messageSink, options = {}) {
39
39
  this._resultKnownAttachmentCounts = /* @__PURE__ */ new Map();
40
- // In case there is blob reporter and UI mode, make sure one does override
40
+ this._resultKnownErrorCounts = /* @__PURE__ */ new Map();
41
+ // In case there is blob reporter and UI mode, make sure one doesn't override
41
42
  // the id assigned by the other.
42
43
  this._idSymbol = Symbol("id");
43
44
  this._messageSink = messageSink;
@@ -66,6 +67,21 @@ class TeleReporterEmitter {
66
67
  }
67
68
  });
68
69
  }
70
+ async onTestPaused(test, result, step) {
71
+ const resultId = result[this._idSymbol];
72
+ const stepId = step[this._idSymbol];
73
+ this._resultKnownErrorCounts.set(resultId, result.errors.length);
74
+ this._messageSink({
75
+ method: "onTestPaused",
76
+ params: {
77
+ testId: test.id,
78
+ resultId,
79
+ stepId,
80
+ errors: result.errors
81
+ }
82
+ });
83
+ return { action: void 0 };
84
+ }
69
85
  onTestEnd(test, result) {
70
86
  const testEnd = {
71
87
  testId: test.id,
@@ -81,7 +97,9 @@ class TeleReporterEmitter {
81
97
  result: this._serializeResultEnd(result)
82
98
  }
83
99
  });
84
- this._resultKnownAttachmentCounts.delete(result[this._idSymbol]);
100
+ const resultId = result[this._idSymbol];
101
+ this._resultKnownAttachmentCounts.delete(resultId);
102
+ this._resultKnownErrorCounts.delete(resultId);
85
103
  }
86
104
  onStepBegin(test, result, step) {
87
105
  step[this._idSymbol] = (0, import_utils.createGuid)();
@@ -221,11 +239,12 @@ class TeleReporterEmitter {
221
239
  };
222
240
  }
223
241
  _serializeResultEnd(result) {
242
+ const id = result[this._idSymbol];
224
243
  return {
225
- id: result[this._idSymbol],
244
+ id,
226
245
  duration: result.duration,
227
246
  status: result.status,
228
- errors: result.errors,
247
+ errors: this._resultKnownErrorCounts.has(id) ? result.errors.slice(this._resultKnownAttachmentCounts.get(id)) : result.errors,
229
248
  annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0
230
249
  };
231
250
  }
@@ -28,6 +28,7 @@ var import_workerHost = require("./workerHost");
28
28
  var import_ipc = require("../common/ipc");
29
29
  var import_internalReporter = require("../reporters/internalReporter");
30
30
  var import_util = require("../util");
31
+ var import_storage = require("./storage");
31
32
  class Dispatcher {
32
33
  constructor(config, reporter, failureTracker) {
33
34
  this._workerSlots = [];
@@ -197,11 +198,11 @@ class Dispatcher {
197
198
  const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
198
199
  this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() });
199
200
  });
200
- worker.onRequest("setStorageValue", async (params) => {
201
- this._setStorageValue(params.fileName, params.key, params.value);
201
+ worker.onRequest("cloneStorage", async (params) => {
202
+ return await import_storage.Storage.clone(params.storageFile, worker.artifactsDir());
202
203
  });
203
- worker.onRequest("getStorageValue", async (params) => {
204
- return this._getStorageValue(params.fileName, params.key);
204
+ worker.onRequest("upstreamStorage", async (params) => {
205
+ await import_storage.Storage.upstream(params.workerFile);
205
206
  });
206
207
  return worker;
207
208
  }
@@ -215,11 +216,6 @@ class Dispatcher {
215
216
  await Promise.all(this._workerSlots.map(({ worker }) => worker?.stop()));
216
217
  this._checkFinished();
217
218
  }
218
- _setStorageValue(fileName, key, value) {
219
- }
220
- _getStorageValue(fileName, key) {
221
- return {};
222
- }
223
219
  }
224
220
  class JobDispatcher {
225
221
  constructor(job, config, reporter, failureTracker, stopCallback) {
@@ -469,11 +465,18 @@ class JobDispatcher {
469
465
  ];
470
466
  }
471
467
  _onTestPaused(worker, params) {
468
+ const data = this._dataByTestId.get(params.testId);
469
+ if (!data)
470
+ return;
471
+ const { result, test, steps } = data;
472
+ const step = steps.get(params.stepId);
473
+ if (!step)
474
+ return;
472
475
  const sendMessage = async (message) => {
473
476
  try {
474
477
  if (this.jobResult.isDone())
475
478
  throw new Error("Test has already stopped");
476
- const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
479
+ const response = await worker.sendCustomMessage({ testId: test.id, request: message.request });
477
480
  if (response.error)
478
481
  (0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
479
482
  return response;
@@ -483,8 +486,11 @@ class JobDispatcher {
483
486
  return { response: void 0, error };
484
487
  }
485
488
  };
486
- for (const error of params.errors)
487
- (0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
489
+ result.errors = params.errors;
490
+ result.error = result.errors[0];
491
+ void this._reporter.onTestPaused?.(test, result, step).then((params2) => {
492
+ worker.sendResume(params2);
493
+ });
488
494
  this._failureTracker.onTestPaused?.({ ...params, sendMessage });
489
495
  }
490
496
  skipWholeJob() {
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var storage_exports = {};
30
+ __export(storage_exports, {
31
+ Storage: () => Storage
32
+ });
33
+ module.exports = __toCommonJS(storage_exports);
34
+ var import_fs = __toESM(require("fs"));
35
+ var import_path = __toESM(require("path"));
36
+ var import_utils = require("playwright-core/lib/utils");
37
+ class Storage {
38
+ constructor(fileName) {
39
+ this._writeChain = Promise.resolve();
40
+ this._fileName = fileName;
41
+ }
42
+ static {
43
+ this._storages = /* @__PURE__ */ new Map();
44
+ }
45
+ static {
46
+ this._workerFiles = /* @__PURE__ */ new Map();
47
+ }
48
+ static async clone(storageFile, artifactsDir) {
49
+ const workerFile = await this._storage(storageFile)._clone(artifactsDir);
50
+ const stat = await import_fs.default.promises.stat(workerFile);
51
+ const lastModified = stat.mtime.getTime();
52
+ Storage._workerFiles.set(workerFile, { storageFile, lastModified });
53
+ return workerFile;
54
+ }
55
+ static async upstream(workerFile) {
56
+ const entry = Storage._workerFiles.get(workerFile);
57
+ if (!entry)
58
+ return;
59
+ const { storageFile, lastModified } = entry;
60
+ const stat = await import_fs.default.promises.stat(workerFile);
61
+ const newLastModified = stat.mtime.getTime();
62
+ if (lastModified !== newLastModified)
63
+ await this._storage(storageFile)._upstream(workerFile);
64
+ Storage._workerFiles.delete(workerFile);
65
+ }
66
+ static _storage(fileName) {
67
+ if (!Storage._storages.has(fileName))
68
+ Storage._storages.set(fileName, new Storage(fileName));
69
+ return Storage._storages.get(fileName);
70
+ }
71
+ async _clone(artifactsDir) {
72
+ const entries = await this._load();
73
+ const workerFile = import_path.default.join(artifactsDir, `pw-storage-${(0, import_utils.createGuid)()}.json`);
74
+ await import_fs.default.promises.writeFile(workerFile, JSON.stringify(entries, null, 2)).catch(() => {
75
+ });
76
+ return workerFile;
77
+ }
78
+ async _upstream(workerFile) {
79
+ const entries = await this._load();
80
+ const newEntries = await import_fs.default.promises.readFile(workerFile, "utf8").then(JSON.parse).catch(() => ({}));
81
+ for (const [key, newValue] of Object.entries(newEntries)) {
82
+ const existing = entries[key];
83
+ if (!existing || existing.timestamp < newValue.timestamp)
84
+ entries[key] = newValue;
85
+ }
86
+ await this._writeFile(entries);
87
+ }
88
+ async _load() {
89
+ if (!this._entriesPromise)
90
+ this._entriesPromise = import_fs.default.promises.readFile(this._fileName, "utf8").then(JSON.parse).catch(() => ({}));
91
+ return this._entriesPromise;
92
+ }
93
+ _writeFile(entries) {
94
+ this._writeChain = this._writeChain.then(() => import_fs.default.promises.writeFile(this._fileName, JSON.stringify(entries, null, 2))).catch(() => {
95
+ });
96
+ return this._writeChain;
97
+ }
98
+ async flush() {
99
+ await this._writeChain;
100
+ }
101
+ }
102
+ // Annotate the CommonJS export names for ESM import in node:
103
+ 0 && (module.exports = {
104
+ Storage
105
+ });
@@ -383,7 +383,8 @@ async function runAllTestsWithConfig(config) {
383
383
  (0, import_tasks.createLoadTask)("in-process", { filterOnly: true, failOnLoadErrors: true }),
384
384
  ...(0, import_tasks.createRunTestsTasks)(config)
385
385
  ];
386
- const status = await (0, import_tasks.runTasks)(new import_tasks.TestRun(config, reporter), tasks, config.config.globalTimeout);
386
+ const testRun = new import_tasks.TestRun(config, reporter, { pauseAtEnd: config.configCLIOverrides.debug, pauseOnError: config.configCLIOverrides.debug });
387
+ const status = await (0, import_tasks.runTasks)(testRun, tasks, config.config.globalTimeout);
387
388
  await new Promise((resolve) => process.stdout.write("", () => resolve()));
388
389
  await new Promise((resolve) => process.stderr.write("", () => resolve()));
389
390
  return status;
@@ -61,6 +61,9 @@ class WorkerHost extends import_processHost.ProcessHost {
61
61
  pauseAtEnd: options.pauseAtEnd
62
62
  };
63
63
  }
64
+ artifactsDir() {
65
+ return this._params.artifactsDir;
66
+ }
64
67
  async start() {
65
68
  await import_fs.default.promises.mkdir(this._params.artifactsDir, { recursive: true });
66
69
  return await this.startRunner(this._params, {
@@ -82,6 +85,9 @@ class WorkerHost extends import_processHost.ProcessHost {
82
85
  async sendCustomMessage(payload) {
83
86
  return await this.sendMessage({ method: "customMessage", params: payload });
84
87
  }
88
+ sendResume(payload) {
89
+ this.sendMessageNoReply({ method: "resume", params: payload });
90
+ }
85
91
  hash() {
86
92
  return this._hash;
87
93
  }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var babelHighlightUtils_exports = {};
30
+ __export(babelHighlightUtils_exports, {
31
+ findTestEndLocation: () => findTestEndLocation
32
+ });
33
+ module.exports = __toCommonJS(babelHighlightUtils_exports);
34
+ var import_path = __toESM(require("path"));
35
+ var import_babelBundle = require("./babelBundle");
36
+ function containsLocation(range, location) {
37
+ if (location.line < range.start.line || location.line > range.end.line)
38
+ return false;
39
+ if (location.line === range.start.line && location.column < range.start.column)
40
+ return false;
41
+ if (location.line === range.end.line && location.column > range.end.column)
42
+ return false;
43
+ return true;
44
+ }
45
+ function findTestEndLocation(text, testStartLocation) {
46
+ const ast = (0, import_babelBundle.babelParse)(text, import_path.default.basename(testStartLocation.file), false);
47
+ let result;
48
+ (0, import_babelBundle.traverse)(ast, {
49
+ enter(path2) {
50
+ if (import_babelBundle.types.isCallExpression(path2.node) && path2.node.loc && containsLocation(path2.node.loc, testStartLocation)) {
51
+ const callNode = path2.node;
52
+ const funcNode = callNode.arguments[callNode.arguments.length - 1];
53
+ if (callNode.arguments.length >= 2 && import_babelBundle.types.isFunction(funcNode) && funcNode.body.loc)
54
+ result = { file: testStartLocation.file, ...funcNode.body.loc.end };
55
+ }
56
+ }
57
+ });
58
+ return result;
59
+ }
60
+ // Annotate the CommonJS export names for ESM import in node:
61
+ 0 && (module.exports = {
62
+ findTestEndLocation
63
+ });
@@ -42,6 +42,7 @@ var import_util = require("../util");
42
42
  var import_testTracing = require("./testTracing");
43
43
  var import_util2 = require("./util");
44
44
  var import_transform = require("../transform/transform");
45
+ var import_babelHighlightUtils = require("../transform/babelHighlightUtils");
45
46
  class TestInfoImpl {
46
47
  constructor(configInternal, projectInternal, workerParams, test, retry, callbacks) {
47
48
  this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
@@ -335,11 +336,33 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
335
336
  async _didFinishTestFunction() {
336
337
  const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
337
338
  if (shouldPause) {
338
- this._callbacks.onTestPaused?.({ testId: this.testId, errors: this._isFailure() ? this.errors : [] });
339
- await this._interruptedPromise;
339
+ const location = (this._isFailure() ? this._errorLocation() : await this._testEndLocation()) ?? { file: this.file, line: this.line, column: this.column };
340
+ const step = this._addStep({ category: "hook", title: "Paused", location });
341
+ const result = await Promise.race([
342
+ this._callbacks.onTestPaused({ testId: this.testId, stepId: step.stepId, errors: this._isFailure() ? this.errors : [] }),
343
+ this._interruptedPromise.then(() => "interrupted")
344
+ ]);
345
+ step.complete({});
346
+ if (result !== "interrupted") {
347
+ if (result.action === "abort")
348
+ this._interrupt();
349
+ if (result.action === void 0)
350
+ await this._interruptedPromise;
351
+ }
340
352
  }
341
353
  await this._onDidFinishTestFunctionCallback?.();
342
354
  }
355
+ _errorLocation() {
356
+ if (this.error?.stack)
357
+ return (0, import_util.filteredStackTrace)(this.error.stack.split("\n"))[0];
358
+ }
359
+ async _testEndLocation() {
360
+ try {
361
+ const source = await import_fs.default.promises.readFile(this.file, "utf-8");
362
+ return (0, import_babelHighlightUtils.findTestEndLocation)(source, { file: this.file, line: this.line, column: this.column });
363
+ } catch {
364
+ }
365
+ }
343
366
  // ------------ TestInfo methods ------------
344
367
  async attach(name, options = {}) {
345
368
  const step = this._addStep({
@@ -459,11 +482,11 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
459
482
  setTimeout(timeout) {
460
483
  this._timeoutManager.setTimeout(timeout);
461
484
  }
462
- async _getStorageValue(fileName, key) {
463
- return await this._callbacks.onGetStorageValue?.({ fileName, key }) ?? Promise.resolve(void 0);
485
+ async _cloneStorage(storageFile) {
486
+ return await this._callbacks.onCloneStorage?.({ storageFile });
464
487
  }
465
- _setStorageValue(fileName, key, value) {
466
- this._callbacks.onSetStorageValue?.({ fileName, key, value });
488
+ async _upstreamStorage(workerFile) {
489
+ await this._callbacks.onUpstreamStorage?.({ workerFile });
467
490
  }
468
491
  }
469
492
  class TestStepInfoImpl {
@@ -229,14 +229,21 @@ class WorkerMain extends import_process.ProcessRunner {
229
229
  return { response: {}, error: (0, import_util2.testInfoError)(error) };
230
230
  }
231
231
  }
232
+ resume(payload) {
233
+ this._resumePromise?.resolve(payload);
234
+ }
232
235
  async _runTest(test, retry, nextTest) {
233
236
  const testInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, test, retry, {
234
237
  onStepBegin: (payload) => this.dispatchEvent("stepBegin", payload),
235
238
  onStepEnd: (payload) => this.dispatchEvent("stepEnd", payload),
236
239
  onAttach: (payload) => this.dispatchEvent("attach", payload),
237
- onTestPaused: (payload) => this.dispatchEvent("testPaused", payload),
238
- onGetStorageValue: (payload) => this.sendRequest("getStorageValue", payload),
239
- onSetStorageValue: (payload) => this.sendMessageNoReply("setStorageValue", payload)
240
+ onTestPaused: (payload) => {
241
+ this._resumePromise = new import_utils.ManualPromise();
242
+ this.dispatchEvent("testPaused", payload);
243
+ return this._resumePromise;
244
+ },
245
+ onCloneStorage: async (payload) => this.sendRequest("cloneStorage", payload),
246
+ onUpstreamStorage: (payload) => this.sendRequest("upstreamStorage", payload)
240
247
  });
241
248
  const processAnnotation = (annotation) => {
242
249
  testInfo.annotations.push(annotation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright",
3
- "version": "1.58.0-alpha-2025-12-09",
3
+ "version": "1.58.0-alpha-2025-12-10",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "license": "Apache-2.0",
66
66
  "dependencies": {
67
- "playwright-core": "1.58.0-alpha-2025-12-09"
67
+ "playwright-core": "1.58.0-alpha-2025-12-10"
68
68
  },
69
69
  "optionalDependencies": {
70
70
  "fsevents": "2.3.2"