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

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.
@@ -33,7 +33,7 @@ class Agent {
33
33
  const { clients, tools, callTool } = await this._initClients();
34
34
  const prompt = this.spec.description;
35
35
  try {
36
- return await this.loop.run(`${prompt}
36
+ const { result } = await this.loop.run(`${prompt}
37
37
 
38
38
  Task:
39
39
  ${task}
@@ -45,6 +45,7 @@ ${JSON.stringify(params, null, 2)}`, {
45
45
  callTool,
46
46
  resultSchema: this.resultSchema
47
47
  });
48
+ return result;
48
49
  } finally {
49
50
  await this._disconnectFromServers(clients);
50
51
  }
@@ -17,6 +17,7 @@ tools:
17
17
  - playwright-test/browser_navigate_back
18
18
  - playwright-test/browser_network_requests
19
19
  - playwright-test/browser_press_key
20
+ - playwright-test/browser_run_code
20
21
  - playwright-test/browser_select_option
21
22
  - playwright-test/browser_snapshot
22
23
  - playwright-test/browser_take_screenshot
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;
@@ -217,23 +217,21 @@ const playwrightFixtures = {
217
217
  ...options
218
218
  });
219
219
  }, { box: true }],
220
- _setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
220
+ _setupContextOptions: [async ({ playwright, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
221
221
  if (testIdAttribute)
222
222
  playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
223
223
  testInfo.snapshotSuffix = process.platform;
224
224
  if ((0, import_utils.debugMode)() === "inspector")
225
225
  testInfo._setDebugMode();
226
- playwright._defaultContextOptions = _combinedContextOptions;
227
226
  playwright._defaultContextTimeout = actionTimeout || 0;
228
227
  playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
229
228
  await use();
230
- playwright._defaultContextOptions = void 0;
231
229
  playwright._defaultContextTimeout = void 0;
232
230
  playwright._defaultContextNavigationTimeout = void 0;
233
231
  }, { auto: "all-hooks-included", title: "context configuration", box: true }],
234
- _setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
232
+ _setupArtifacts: [async ({ playwright, screenshot, _combinedContextOptions, agent }, use, testInfo) => {
235
233
  testInfo.setTimeout(testInfo.project.timeout);
236
- const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
234
+ const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, agent);
237
235
  await artifactsRecorder.willStartTest(testInfo);
238
236
  const tracingGroupSteps = [];
239
237
  const csiListener = {
@@ -279,20 +277,33 @@ const playwrightFixtures = {
279
277
  if (!keepTestTimeout)
280
278
  (0, import_globals.currentTestInfo)()?._setDebugMode();
281
279
  },
280
+ runBeforeCreateBrowserContext: async (options) => {
281
+ for (const [key, value] of Object.entries(_combinedContextOptions)) {
282
+ if (!(key in options))
283
+ options[key] = value;
284
+ }
285
+ await artifactsRecorder.willCreateBrowserContext(options);
286
+ },
287
+ runBeforeCreateRequestContext: async (options) => {
288
+ for (const [key, value] of Object.entries(_combinedContextOptions)) {
289
+ if (!(key in options))
290
+ options[key] = value;
291
+ }
292
+ },
282
293
  runAfterCreateBrowserContext: async (context) => {
283
- await artifactsRecorder?.didCreateBrowserContext(context);
294
+ await artifactsRecorder.didCreateBrowserContext(context);
284
295
  const testInfo2 = (0, import_globals.currentTestInfo)();
285
296
  if (testInfo2)
286
297
  attachConnectedHeaderIfNeeded(testInfo2, context.browser());
287
298
  },
288
299
  runAfterCreateRequestContext: async (context) => {
289
- await artifactsRecorder?.didCreateRequestContext(context);
300
+ await artifactsRecorder.didCreateRequestContext(context);
290
301
  },
291
302
  runBeforeCloseBrowserContext: async (context) => {
292
- await artifactsRecorder?.willCloseBrowserContext(context);
303
+ await artifactsRecorder.willCloseBrowserContext(context);
293
304
  },
294
305
  runBeforeCloseRequestContext: async (context) => {
295
- await artifactsRecorder?.willCloseRequestContext(context);
306
+ await artifactsRecorder.willCloseRequestContext(context);
296
307
  }
297
308
  };
298
309
  const clientInstrumentation = playwright._instrumentation;
@@ -551,9 +562,10 @@ class SnapshotRecorder {
551
562
  }
552
563
  }
553
564
  class ArtifactsRecorder {
554
- constructor(playwright, artifactsDir, screenshot) {
565
+ constructor(playwright, artifactsDir, screenshot, agent) {
555
566
  this._playwright = playwright;
556
567
  this._artifactsDir = artifactsDir;
568
+ this._agent = agent;
557
569
  const screenshotOptions = typeof screenshot === "string" ? void 0 : screenshot;
558
570
  this._startedCollectingArtifacts = Symbol("startedCollectingArtifacts");
559
571
  this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), "screenshot", "image/png", ".png", async (page, path2) => {
@@ -573,10 +585,14 @@ class ArtifactsRecorder {
573
585
  async didCreateBrowserContext(context) {
574
586
  await this._startTraceChunkOnContextCreation(context, context.tracing);
575
587
  }
588
+ async willCreateBrowserContext(options) {
589
+ await this._cloneAgentCache(options);
590
+ }
576
591
  async willCloseBrowserContext(context) {
577
592
  await this._stopTracing(context, context.tracing);
578
593
  await this._screenshotRecorder.captureTemporary(context);
579
594
  await this._takePageSnapshot(context);
595
+ await this._upstreamAgentCache(context);
580
596
  }
581
597
  async _takePageSnapshot(context) {
582
598
  if (process.env.PLAYWRIGHT_NO_COPY_PROMPT)
@@ -595,6 +611,21 @@ class ArtifactsRecorder {
595
611
  } catch {
596
612
  }
597
613
  }
614
+ async _cloneAgentCache(options) {
615
+ if (!this._agent || this._agent.cacheMode === "ignore")
616
+ return;
617
+ if (!this._agent.cacheFile && !this._agent.cachePathTemplate)
618
+ return;
619
+ const cacheFile = this._agent.cacheFile ?? this._testInfo._applyPathTemplate(this._agent.cachePathTemplate, "cache", ".json");
620
+ const workerFile = await this._testInfo._cloneStorage(cacheFile);
621
+ if (this._agent && workerFile)
622
+ options.agent = { ...this._agent, cacheFile: workerFile };
623
+ }
624
+ async _upstreamAgentCache(context) {
625
+ const agent = context._options.agent;
626
+ if (this._testInfo.status === "passed" && agent?.cacheFile)
627
+ await this._testInfo._upstreamStorage(agent.cacheFile);
628
+ }
598
629
  async didCreateRequestContext(context) {
599
630
  await this._startTraceChunkOnContextCreation(context, context._tracing);
600
631
  }
@@ -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
  });
@@ -36,7 +36,7 @@ var import_utils = require("playwright-core/lib/utils");
36
36
  var import_mcpBundle = require("playwright-core/lib/mcpBundle");
37
37
  var import_tool = require("./tool");
38
38
  const codeSchema = import_mcpBundle.z.object({
39
- code: import_mcpBundle.z.string().describe(`Playwright code snippet to run. The snippet should access the \`page\` object to interact with the page. Can make multiple statements. For example: \`await page.getByRole('button', { name: 'Submit' }).click();\``)
39
+ code: import_mcpBundle.z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``)
40
40
  });
41
41
  const runCode = (0, import_tool.defineTabTool)({
42
42
  capability: "core",
@@ -49,7 +49,7 @@ const runCode = (0, import_tool.defineTabTool)({
49
49
  },
50
50
  handle: async (tab, params, response) => {
51
51
  response.setIncludeSnapshot();
52
- response.addCode(params.code);
52
+ response.addCode(`await (${params.code})(page);`);
53
53
  const __end__ = new import_utils.ManualPromise();
54
54
  const context = {
55
55
  page: tab.page,
@@ -59,14 +59,16 @@ const runCode = (0, import_tool.defineTabTool)({
59
59
  await tab.waitForCompletion(async () => {
60
60
  const snippet = `(async () => {
61
61
  try {
62
- ${params.code};
63
- __end__.resolve();
62
+ const result = await (${params.code})(page);
63
+ __end__.resolve(JSON.stringify(result));
64
64
  } catch (e) {
65
65
  __end__.reject(e);
66
66
  }
67
67
  })()`;
68
- import_vm.default.runInContext(snippet, context);
69
- await __end__;
68
+ await import_vm.default.runInContext(snippet, context);
69
+ const result = await __end__;
70
+ if (typeof result === "string")
71
+ response.addResult(result);
70
72
  });
71
73
  }
72
74
  });
@@ -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({
@@ -422,10 +445,6 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
422
445
  if (index > 1)
423
446
  relativeOutputPath = (0, import_util.addSuffixToFilePath)(relativeOutputPath, `-${index - 1}`);
424
447
  }
425
- const absoluteSnapshotPath = this._applyPathTemplate(kind, subPath, ext);
426
- return { absoluteSnapshotPath, relativeOutputPath };
427
- }
428
- _applyPathTemplate(kind, relativePath, ext) {
429
448
  const legacyTemplate = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
430
449
  let template;
431
450
  if (kind === "screenshot") {
@@ -436,12 +455,15 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
436
455
  } else {
437
456
  template = this._projectInternal.snapshotPathTemplate || legacyTemplate;
438
457
  }
439
- const dir = import_path.default.dirname(relativePath);
440
- const name = import_path.default.basename(relativePath, ext);
458
+ const nameArgument = import_path.default.join(import_path.default.dirname(subPath), import_path.default.basename(subPath, ext));
459
+ const absoluteSnapshotPath = this._applyPathTemplate(template, nameArgument, ext);
460
+ return { absoluteSnapshotPath, relativeOutputPath };
461
+ }
462
+ _applyPathTemplate(template, nameArgument, ext) {
441
463
  const relativeTestFilePath = import_path.default.relative(this.project.testDir, this._requireFile);
442
464
  const parsedRelativeTestFilePath = import_path.default.parse(relativeTestFilePath);
443
465
  const projectNamePathSegment = (0, import_utils.sanitizeForFilePath)(this.project.name);
444
- const snapshotPath = template.replace(/\{(.)?testDir\}/g, "$1" + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, "$1" + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? "$1" + this.snapshotSuffix : "").replace(/\{(.)?testFileDir\}/g, "$1" + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, "$1" + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? "$1" + projectNamePathSegment : "").replace(/\{(.)?testName\}/g, "$1" + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, "$1" + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, "$1" + relativeTestFilePath).replace(/\{(.)?arg\}/g, "$1" + import_path.default.join(dir, name)).replace(/\{(.)?ext\}/g, ext ? "$1" + ext : "");
466
+ const snapshotPath = template.replace(/\{(.)?testDir\}/g, "$1" + this.project.testDir).replace(/\{(.)?snapshotDir\}/g, "$1" + this.project.snapshotDir).replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? "$1" + this.snapshotSuffix : "").replace(/\{(.)?testFileDir\}/g, "$1" + parsedRelativeTestFilePath.dir).replace(/\{(.)?platform\}/g, "$1" + process.platform).replace(/\{(.)?projectName\}/g, projectNamePathSegment ? "$1" + projectNamePathSegment : "").replace(/\{(.)?testName\}/g, "$1" + this._fsSanitizedTestName()).replace(/\{(.)?testFileName\}/g, "$1" + parsedRelativeTestFilePath.base).replace(/\{(.)?testFilePath\}/g, "$1" + relativeTestFilePath).replace(/\{(.)?arg\}/g, "$1" + nameArgument).replace(/\{(.)?ext\}/g, ext ? "$1" + ext : "");
445
467
  return import_path.default.normalize(import_path.default.resolve(this._configInternal.configDir, snapshotPath));
446
468
  }
447
469
  snapshotPath(...args) {
@@ -459,11 +481,12 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
459
481
  setTimeout(timeout) {
460
482
  this._timeoutManager.setTimeout(timeout);
461
483
  }
462
- async _getStorageValue(fileName, key) {
463
- return await this._callbacks.onGetStorageValue?.({ fileName, key }) ?? Promise.resolve(void 0);
484
+ async _cloneStorage(cacheFileTemplate) {
485
+ const storageFile = this._applyPathTemplate(cacheFileTemplate, "cache", ".json");
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-11",
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-11"
68
68
  },
69
69
  "optionalDependencies": {
70
70
  "fsevents": "2.3.2"
package/types/test.d.ts CHANGED
@@ -6644,7 +6644,7 @@ export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, P
6644
6644
  [K in Exclude<keyof T, keyof PW | keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }];
6645
6645
  };
6646
6646
 
6647
- type Agent = Exclude<BrowserContextOptions['agent'], undefined> | undefined;
6647
+ type Agent = Exclude<BrowserContextOptions['agent'], undefined> & { cachePathTemplate?: string } | undefined;
6648
6648
  type BrowserName = 'chromium' | 'firefox' | 'webkit';
6649
6649
  type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
6650
6650
  type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;