playwright 1.56.0-alpha-2025-09-22 → 1.56.0-alpha-2025-09-24

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.
@@ -189,21 +189,35 @@ const vscodeToolMap = /* @__PURE__ */ new Map([
189
189
  ["edit", ["editFiles"]],
190
190
  ["write", ["createFile", "createDirectory"]]
191
191
  ]);
192
+ const vscodeToolsOrder = ["createFile", "createDirectory", "editFiles", "fileSearch", "textSearch", "listDirectory", "readFile"];
193
+ const vscodeToolPrefix = "test_";
192
194
  function saveAsVSCodeChatmode(agent) {
193
195
  function asVscodeTool(tool) {
194
196
  const [first, second] = tool.split("/");
195
197
  if (second)
196
- return second;
198
+ return second.startsWith("browser_") ? vscodeToolPrefix + second : second;
197
199
  return vscodeToolMap.get(first) || first;
198
200
  }
199
- const tools = agent.header.tools.map(asVscodeTool).flat().map((tool) => `'${tool}'`).join(", ");
201
+ const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
202
+ const indexA = vscodeToolsOrder.indexOf(a);
203
+ const indexB = vscodeToolsOrder.indexOf(b);
204
+ if (indexA === -1 && indexB === -1)
205
+ return a.localeCompare(b);
206
+ if (indexA === -1)
207
+ return 1;
208
+ if (indexB === -1)
209
+ return -1;
210
+ return indexA - indexB;
211
+ }).map((tool) => `'${tool}'`).join(", ");
200
212
  const lines = [];
201
213
  lines.push(`---`);
202
- lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}`);
214
+ lines.push(`description: ${agent.header.description}.`);
203
215
  lines.push(`tools: [${tools}]`);
204
216
  lines.push(`---`);
205
217
  lines.push("");
206
218
  lines.push(agent.instructions);
219
+ for (const example of agent.examples)
220
+ lines.push(`<example>${example}</example>`);
207
221
  return lines.join("\n");
208
222
  }
209
223
  async function initVSCodeRepo() {
@@ -226,7 +240,8 @@ async function initVSCodeRepo() {
226
240
  mcpJson.servers["playwright-test"] = {
227
241
  type: "stdio",
228
242
  command: commonMcpServers.playwrightTest.command,
229
- args: commonMcpServers.playwrightTest.args
243
+ args: commonMcpServers.playwrightTest.args,
244
+ env: { "PLAYWRIGHT_MCP_TOOL_PREFIX": vscodeToolPrefix }
230
245
  };
231
246
  await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2));
232
247
  }
package/lib/index.js CHANGED
@@ -43,8 +43,6 @@ var import_utils = require("playwright-core/lib/utils");
43
43
  var import_globals = require("./common/globals");
44
44
  var import_testType = require("./common/testType");
45
45
  var import_browserBackend = require("./mcp/test/browserBackend");
46
- var import_babelBundle = require("./transform/babelBundle");
47
- var import_util = require("./util");
48
46
  var import_expect = require("./matchers/expect");
49
47
  var import_configLoader = require("./common/configLoader");
50
48
  var import_testType2 = require("./common/testType");
@@ -262,22 +260,6 @@ const playwrightFixtures = {
262
260
  if (data.apiName === "tracing.group")
263
261
  tracingGroupSteps.push(step);
264
262
  },
265
- onApiCallRecovery: (data, error, channelOwner, recoveryHandlers) => {
266
- const testInfo2 = (0, import_globals.currentTestInfo)();
267
- if (!testInfo2 || !testInfo2._pauseOnError())
268
- return;
269
- const step = data.userData;
270
- if (!step)
271
- return;
272
- const page = channelToPage(channelOwner);
273
- if (!page)
274
- return;
275
- recoveryHandlers.push(async () => {
276
- await (0, import_browserBackend.runBrowserBackendOnError)(page, () => {
277
- return (0, import_util.stripAnsiEscapes)(createErrorCodeframe(error.message, step.location));
278
- });
279
- });
280
- },
281
263
  onApiCallEnd: (data) => {
282
264
  if (data.apiName === "tracing.group")
283
265
  return;
@@ -398,12 +380,13 @@ const playwrightFixtures = {
398
380
  attachConnectedHeaderIfNeeded(testInfo, browserImpl);
399
381
  if (!_reuseContext) {
400
382
  const { context: context2, close } = await _contextFactory();
383
+ testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context2, testInfo.errors[0]?.message));
401
384
  await use(context2);
402
- await (0, import_browserBackend.runBrowserBackendAtEnd)(context2);
403
385
  await close();
404
386
  return;
405
387
  }
406
388
  const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
389
+ testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context, testInfo.errors[0]?.message));
407
390
  await use(context);
408
391
  const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
409
392
  await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
@@ -577,7 +560,7 @@ class ArtifactsRecorder {
577
560
  }
578
561
  async willStartTest(testInfo) {
579
562
  this._testInfo = testInfo;
580
- testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
563
+ testInfo._onDidFinishTestFunctions.push(() => this.didFinishTestFunction());
581
564
  this._screenshotRecorder.fixOrdinal();
582
565
  await Promise.all(this._playwright._allContexts().map((context) => this.didCreateBrowserContext(context)));
583
566
  const existingApiRequests = Array.from(this._playwright.request._contexts);
@@ -688,36 +671,6 @@ function tracing() {
688
671
  return test.info()._tracing;
689
672
  }
690
673
  const test = _baseTest.extend(playwrightFixtures);
691
- function channelToPage(channelOwner) {
692
- if (channelOwner._type === "Page")
693
- return channelOwner;
694
- if (channelOwner._type === "Frame")
695
- return channelOwner.page();
696
- return void 0;
697
- }
698
- function createErrorCodeframe(message, location) {
699
- let source;
700
- try {
701
- source = import_fs.default.readFileSync(location.file, "utf-8") + "\n//";
702
- } catch (e) {
703
- return "";
704
- }
705
- return (0, import_babelBundle.codeFrameColumns)(
706
- source,
707
- {
708
- start: {
709
- line: location.line,
710
- column: location.column
711
- }
712
- },
713
- {
714
- highlightCode: true,
715
- linesAbove: 5,
716
- linesBelow: 5,
717
- message: message.split("\n")[0] || void 0
718
- }
719
- );
720
- }
721
674
  // Annotate the CommonJS export names for ESM import in node:
722
675
  0 && (module.exports = {
723
676
  _baseTest,
@@ -23,7 +23,6 @@ __export(toBeTruthy_exports, {
23
23
  module.exports = __toCommonJS(toBeTruthy_exports);
24
24
  var import_util = require("../util");
25
25
  var import_matcherHint = require("./matcherHint");
26
- var import_browserBackend = require("../mcp/test/browserBackend");
27
26
  async function toBeTruthy(matcherName, locator, receiverType, expected, arg, query, options = {}) {
28
27
  (0, import_util.expectTypes)(locator, [receiverType], matcherName);
29
28
  const timeout = options.timeout ?? this.timeout;
@@ -58,7 +57,6 @@ async function toBeTruthy(matcherName, locator, receiverType, expected, arg, que
58
57
  log
59
58
  });
60
59
  };
61
- await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
62
60
  return {
63
61
  message,
64
62
  pass,
@@ -24,7 +24,6 @@ module.exports = __toCommonJS(toEqual_exports);
24
24
  var import_utils = require("playwright-core/lib/utils");
25
25
  var import_util = require("../util");
26
26
  var import_matcherHint = require("./matcherHint");
27
- var import_browserBackend = require("../mcp/test/browserBackend");
28
27
  const EXPECTED_LABEL = "Expected";
29
28
  const RECEIVED_LABEL = "Received";
30
29
  async function toEqual(matcherName, locator, receiverType, query, expected, options = {}) {
@@ -84,7 +83,6 @@ async function toEqual(matcherName, locator, receiverType, query, expected, opti
84
83
  log
85
84
  });
86
85
  };
87
- await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
88
86
  return {
89
87
  actual: received,
90
88
  expected,
@@ -25,7 +25,6 @@ var import_util = require("../util");
25
25
  var import_expect = require("./expect");
26
26
  var import_matcherHint = require("./matcherHint");
27
27
  var import_expectBundle = require("../common/expectBundle");
28
- var import_browserBackend = require("../mcp/test/browserBackend");
29
28
  async function toMatchText(matcherName, receiver, receiverType, query, expected, options = {}) {
30
29
  (0, import_util.expectTypes)(receiver, [receiverType], matcherName);
31
30
  const locator = receiverType === "Locator" ? receiver : void 0;
@@ -84,8 +83,6 @@ ${this.utils.printWithType("Expected", expected, this.utils.printExpected)}`;
84
83
  errorMessage
85
84
  });
86
85
  };
87
- if (locator)
88
- await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
89
86
  return {
90
87
  name: matcherName,
91
88
  expected,
@@ -80,16 +80,20 @@ class BaseContextFactory {
80
80
  const browser = await this._obtainBrowser(clientInfo);
81
81
  const browserContext = await this._doCreateContext(browser);
82
82
  await addInitScript(browserContext, this.config.browser.initScript);
83
- return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
83
+ return {
84
+ browserContext,
85
+ close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
86
+ };
84
87
  }
85
88
  async _doCreateContext(browser) {
86
89
  throw new Error("Not implemented");
87
90
  }
88
- async _closeBrowserContext(browserContext, browser) {
91
+ async _closeBrowserContext(browserContext, browser, afterClose) {
89
92
  (0, import_log.testDebug)(`close browser context (${this._logName})`);
90
93
  if (browser.contexts().length === 1)
91
94
  this._browserPromise = void 0;
92
95
  await browserContext.close().catch(import_log.logUnhandledError);
96
+ await afterClose();
93
97
  if (browser.contexts().length === 0) {
94
98
  (0, import_log.testDebug)(`close browser (${this._logName})`);
95
99
  await browser.close().catch(import_log.logUnhandledError);
@@ -103,8 +107,8 @@ class IsolatedContextFactory extends BaseContextFactory {
103
107
  async _doObtainBrowser(clientInfo) {
104
108
  await injectCdpPort(this.config.browser);
105
109
  const browserType = playwright[this.config.browser.browserName];
106
- const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
107
- if (this.config.saveTrace)
110
+ const tracesDir = await computeTracesDir(this.config, clientInfo);
111
+ if (tracesDir && this.config.saveTrace)
108
112
  await startTraceServer(this.config, tracesDir);
109
113
  return browserType.launch({
110
114
  tracesDir,
@@ -158,8 +162,8 @@ class PersistentContextFactory {
158
162
  await injectCdpPort(this.config.browser);
159
163
  (0, import_log.testDebug)("create browser context (persistent)");
160
164
  const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
161
- const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
162
- if (this.config.saveTrace)
165
+ const tracesDir = await computeTracesDir(this.config, clientInfo);
166
+ if (tracesDir && this.config.saveTrace)
163
167
  await startTraceServer(this.config, tracesDir);
164
168
  this._userDataDirs.add(userDataDir);
165
169
  (0, import_log.testDebug)("lock user data dir", userDataDir);
@@ -179,7 +183,7 @@ class PersistentContextFactory {
179
183
  try {
180
184
  const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
181
185
  await addInitScript(browserContext, this.config.browser.initScript);
182
- const close = () => this._closeBrowserContext(browserContext, userDataDir);
186
+ const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
183
187
  return { browserContext, close };
184
188
  } catch (error) {
185
189
  if (error.message.includes("Executable doesn't exist"))
@@ -193,11 +197,12 @@ class PersistentContextFactory {
193
197
  }
194
198
  throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
195
199
  }
196
- async _closeBrowserContext(browserContext, userDataDir) {
200
+ async _closeBrowserContext(browserContext, userDataDir, afterClose) {
197
201
  (0, import_log.testDebug)("close browser context (persistent)");
198
202
  (0, import_log.testDebug)("release user data dir", userDataDir);
199
203
  await browserContext.close().catch(() => {
200
204
  });
205
+ await afterClose();
201
206
  this._userDataDirs.delete(userDataDir);
202
207
  (0, import_log.testDebug)("close browser context complete (persistent)");
203
208
  }
@@ -275,9 +280,15 @@ class SharedContextFactory {
275
280
  if (!contextPromise)
276
281
  return;
277
282
  const { close } = await contextPromise;
278
- await close();
283
+ await close(async () => {
284
+ });
279
285
  }
280
286
  }
287
+ async function computeTracesDir(config, clientInfo) {
288
+ if (!config.saveTrace && !config.capabilities?.includes("tracing"))
289
+ return;
290
+ return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
291
+ }
281
292
  // Annotate the CommonJS export names for ESM import in node:
282
293
  0 && (module.exports = {
283
294
  SharedContextFactory,
@@ -52,6 +52,7 @@ class BrowserServerBackend {
52
52
  const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
53
53
  const context = this._context;
54
54
  const response = new import_response.Response(context, name, parsedArguments);
55
+ response.logBegin();
55
56
  context.setRunningTool(name);
56
57
  try {
57
58
  await tool.handle(context, parsedArguments, response);
@@ -62,6 +63,7 @@ class BrowserServerBackend {
62
63
  } finally {
63
64
  context.setRunningTool(void 0);
64
65
  }
66
+ response.logEnd();
65
67
  return response.serialize();
66
68
  }
67
69
  serverClosed() {
@@ -34,7 +34,9 @@ __export(config_exports, {
34
34
  dotenvFileLoader: () => dotenvFileLoader,
35
35
  headerParser: () => headerParser,
36
36
  numberParser: () => numberParser,
37
+ outputDir: () => outputDir,
37
38
  outputFile: () => outputFile,
39
+ resolutionParser: () => resolutionParser,
38
40
  resolveCLIConfig: () => resolveCLIConfig,
39
41
  resolveConfig: () => resolveConfig,
40
42
  semicolonSeparatedList: () => semicolonSeparatedList
@@ -91,6 +93,8 @@ async function validateConfig(config) {
91
93
  throw new Error(`Init script file does not exist: ${script}`);
92
94
  }
93
95
  }
96
+ if (config.sharedBrowserContext && config.saveVideo)
97
+ throw new Error("saveVideo is not supported when sharedBrowserContext is true");
94
98
  }
95
99
  function configFromCLIOptions(cliOptions) {
96
100
  let browserName;
@@ -136,22 +140,21 @@ function configFromCLIOptions(cliOptions) {
136
140
  contextOptions.storageState = cliOptions.storageState;
137
141
  if (cliOptions.userAgent)
138
142
  contextOptions.userAgent = cliOptions.userAgent;
139
- if (cliOptions.viewportSize) {
140
- try {
141
- const [width, height] = cliOptions.viewportSize.split(",").map((n) => +n);
142
- if (isNaN(width) || isNaN(height))
143
- throw new Error("bad values");
144
- contextOptions.viewport = { width, height };
145
- } catch (e) {
146
- throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
147
- }
148
- }
143
+ if (cliOptions.viewportSize)
144
+ contextOptions.viewport = cliOptions.viewportSize;
149
145
  if (cliOptions.ignoreHttpsErrors)
150
146
  contextOptions.ignoreHTTPSErrors = true;
151
147
  if (cliOptions.blockServiceWorkers)
152
148
  contextOptions.serviceWorkers = "block";
153
149
  if (cliOptions.grantPermissions)
154
150
  contextOptions.permissions = cliOptions.grantPermissions;
151
+ if (cliOptions.saveVideo) {
152
+ contextOptions.recordVideo = {
153
+ // Videos are moved to output directory on saveAs.
154
+ dir: tmpDir(),
155
+ size: cliOptions.saveVideo
156
+ };
157
+ }
155
158
  const result = {
156
159
  browser: {
157
160
  browserName,
@@ -165,7 +168,8 @@ function configFromCLIOptions(cliOptions) {
165
168
  },
166
169
  server: {
167
170
  port: cliOptions.port,
168
- host: cliOptions.host
171
+ host: cliOptions.host,
172
+ allowedHosts: cliOptions.allowedHosts
169
173
  },
170
174
  capabilities: cliOptions.caps,
171
175
  network: {
@@ -174,6 +178,7 @@ function configFromCLIOptions(cliOptions) {
174
178
  },
175
179
  saveSession: cliOptions.saveSession,
176
180
  saveTrace: cliOptions.saveTrace,
181
+ saveVideo: cliOptions.saveVideo,
177
182
  secrets: cliOptions.secrets,
178
183
  sharedBrowserContext: cliOptions.sharedBrowserContext,
179
184
  outputDir: cliOptions.outputDir,
@@ -187,6 +192,7 @@ function configFromCLIOptions(cliOptions) {
187
192
  }
188
193
  function configFromEnv() {
189
194
  const options = {};
195
+ options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
190
196
  options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
191
197
  options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
192
198
  options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
@@ -213,13 +219,14 @@ function configFromEnv() {
213
219
  options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
214
220
  options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
215
221
  options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
222
+ options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
216
223
  options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
217
224
  options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
218
225
  options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
219
226
  options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
220
227
  options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
221
228
  options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
222
- options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
229
+ options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
223
230
  return configFromCLIOptions(options);
224
231
  }
225
232
  async function loadConfig(configFile) {
@@ -231,19 +238,30 @@ async function loadConfig(configFile) {
231
238
  throw new Error(`Failed to load config file: ${configFile}, ${error}`);
232
239
  }
233
240
  }
234
- async function outputFile(config, clientInfo, fileName, options) {
241
+ function tmpDir() {
242
+ return import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output");
243
+ }
244
+ function outputDir(config, clientInfo) {
235
245
  const rootPath = (0, import_server.firstRootPath)(clientInfo);
236
- const outputDir = config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output", String(clientInfo.timestamp));
246
+ return config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(tmpDir(), String(clientInfo.timestamp));
247
+ }
248
+ async function outputFile(config, clientInfo, fileName, options) {
249
+ const file = await resolveFile(config, clientInfo, fileName, options);
250
+ (0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
251
+ return file;
252
+ }
253
+ async function resolveFile(config, clientInfo, fileName, options) {
254
+ const dir = outputDir(config, clientInfo);
237
255
  if (options.origin === "code")
238
- return import_path.default.resolve(outputDir, fileName);
256
+ return import_path.default.resolve(dir, fileName);
239
257
  if (options.origin === "llm") {
240
258
  fileName = fileName.split("\\").join("/");
241
- const resolvedFile = import_path.default.resolve(outputDir, fileName);
242
- if (!resolvedFile.startsWith(import_path.default.resolve(outputDir) + import_path.default.sep))
259
+ const resolvedFile = import_path.default.resolve(dir, fileName);
260
+ if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
243
261
  throw new Error(`Resolved file path for ${fileName} is outside of the output directory`);
244
262
  return resolvedFile;
245
263
  }
246
- return import_path.default.join(outputDir, sanitizeForFilePath(fileName));
264
+ return import_path.default.join(dir, sanitizeForFilePath(fileName));
247
265
  }
248
266
  function pickDefined(obj) {
249
267
  return Object.fromEntries(
@@ -306,6 +324,23 @@ function numberParser(value) {
306
324
  return void 0;
307
325
  return +value;
308
326
  }
327
+ function resolutionParser(name, value) {
328
+ if (!value)
329
+ return void 0;
330
+ if (value.includes("x")) {
331
+ const [width, height] = value.split("x").map((v) => +v);
332
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
333
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
334
+ return { width, height };
335
+ }
336
+ if (value.includes(",")) {
337
+ const [width, height] = value.split(",").map((v) => +v);
338
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
339
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
340
+ return { width, height };
341
+ }
342
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
343
+ }
309
344
  function headerParser(arg, previous) {
310
345
  if (!arg)
311
346
  return previous || {};
@@ -339,7 +374,9 @@ function sanitizeForFilePath(s) {
339
374
  dotenvFileLoader,
340
375
  headerParser,
341
376
  numberParser,
377
+ outputDir,
342
378
  outputFile,
379
+ resolutionParser,
343
380
  resolveCLIConfig,
344
381
  resolveConfig,
345
382
  semicolonSeparatedList
@@ -32,11 +32,14 @@ __export(context_exports, {
32
32
  InputRecorder: () => InputRecorder
33
33
  });
34
34
  module.exports = __toCommonJS(context_exports);
35
+ var import_fs = __toESM(require("fs"));
36
+ var import_path = __toESM(require("path"));
35
37
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
36
38
  var import_log = require("../log");
37
39
  var import_tab = require("./tab");
38
40
  var import_config = require("./config");
39
41
  var codegen = __toESM(require("./codegen"));
42
+ var import_utils = require("./tools/utils");
40
43
  const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
41
44
  class Context {
42
45
  constructor(options) {
@@ -135,7 +138,20 @@ class Context {
135
138
  await promise.then(async ({ browserContext, close }) => {
136
139
  if (this.config.saveTrace)
137
140
  await browserContext.tracing.stop();
138
- await close();
141
+ const videos = browserContext.pages().map((page) => page.video()).filter((video) => !!video);
142
+ await close(async () => {
143
+ for (const video of videos) {
144
+ const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
145
+ await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
146
+ const p = await video.path();
147
+ try {
148
+ if (import_fs.default.existsSync(p))
149
+ await import_fs.default.promises.rename(p, name);
150
+ } catch (e) {
151
+ (0, import_log.logUnhandledError)(e);
152
+ }
153
+ }
154
+ });
139
155
  });
140
156
  }
141
157
  async dispose() {
@@ -18,10 +18,13 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var response_exports = {};
20
20
  __export(response_exports, {
21
- Response: () => Response
21
+ Response: () => Response,
22
+ requestDebug: () => requestDebug
22
23
  });
23
24
  module.exports = __toCommonJS(response_exports);
25
+ var import_utilsBundle = require("playwright-core/lib/utilsBundle");
24
26
  var import_tab = require("./tab");
27
+ const requestDebug = (0, import_utilsBundle.debug)("pw:mcp:request");
25
28
  class Response {
26
29
  constructor(context, toolName, toolArgs) {
27
30
  this._result = [];
@@ -73,7 +76,15 @@ class Response {
73
76
  tabSnapshot() {
74
77
  return this._tabSnapshot;
75
78
  }
76
- serialize() {
79
+ logBegin() {
80
+ if (requestDebug.enabled)
81
+ requestDebug(this.toolName, this.toolArgs);
82
+ }
83
+ logEnd() {
84
+ if (requestDebug.enabled)
85
+ requestDebug(this.serialize({ omitSnapshot: true, omitBlobs: true }));
86
+ }
87
+ serialize(options = {}) {
77
88
  const response = [];
78
89
  if (this._result.length) {
79
90
  response.push("### Result");
@@ -93,7 +104,7 @@ ${this._code.join("\n")}
93
104
  response.push(...(0, import_tab.renderModalStates)(this._context, this._tabSnapshot.modalStates));
94
105
  response.push("");
95
106
  } else if (this._tabSnapshot) {
96
- response.push(renderTabSnapshot(this._tabSnapshot));
107
+ response.push(renderTabSnapshot(this._tabSnapshot, options));
97
108
  response.push("");
98
109
  }
99
110
  const content = [
@@ -101,7 +112,7 @@ ${this._code.join("\n")}
101
112
  ];
102
113
  if (this._context.config.imageResponses !== "omit") {
103
114
  for (const image of this._images)
104
- content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
115
+ content.push({ type: "image", data: options.omitBlobs ? "<blob>" : image.data.toString("base64"), mimeType: image.contentType });
105
116
  }
106
117
  this._redactSecrets(content);
107
118
  return { content, isError: this._isError };
@@ -117,7 +128,7 @@ ${this._code.join("\n")}
117
128
  }
118
129
  }
119
130
  }
120
- function renderTabSnapshot(tabSnapshot) {
131
+ function renderTabSnapshot(tabSnapshot, options = {}) {
121
132
  const lines = [];
122
133
  if (tabSnapshot.consoleMessages.length) {
123
134
  lines.push(`### New console messages`);
@@ -140,7 +151,7 @@ function renderTabSnapshot(tabSnapshot) {
140
151
  lines.push(`- Page Title: ${tabSnapshot.title}`);
141
152
  lines.push(`- Page Snapshot:`);
142
153
  lines.push("```yaml");
143
- lines.push(tabSnapshot.ariaSnapshot);
154
+ lines.push(options.omitSnapshot ? "<snapshot>" : tabSnapshot.ariaSnapshot);
144
155
  lines.push("```");
145
156
  return lines.join("\n");
146
157
  }
@@ -170,5 +181,6 @@ function trim(text, maxLength) {
170
181
  }
171
182
  // Annotate the CommonJS export names for ESM import in node:
172
183
  0 && (module.exports = {
173
- Response
184
+ Response,
185
+ requestDebug
174
186
  });
@@ -44,7 +44,7 @@ class SessionLog {
44
44
  this._file = import_path.default.join(this._folder, "session.md");
45
45
  }
46
46
  static async create(config, clientInfo) {
47
- const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code" });
47
+ const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code", reason: "Saving session" });
48
48
  await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
49
49
  console.error(`Session: ${sessionFolder}`);
50
50
  return new SessionLog(sessionFolder);
@@ -67,7 +67,7 @@ class SessionLog {
67
67
  code = code.trim();
68
68
  if (isUpdate) {
69
69
  const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
70
- if (lastEntry.userAction?.name === action.name) {
70
+ if (lastEntry?.userAction?.name === action.name) {
71
71
  lastEntry.userAction = action;
72
72
  lastEntry.code = code;
73
73
  return;
@@ -27,6 +27,8 @@ var import_events = require("events");
27
27
  var import_utils = require("playwright-core/lib/utils");
28
28
  var import_utils2 = require("./tools/utils");
29
29
  var import_log = require("../log");
30
+ var import_dialogs = require("./tools/dialogs");
31
+ var import_files = require("./tools/files");
30
32
  const TabEvents = {
31
33
  modalState: "modalState"
32
34
  };
@@ -52,7 +54,7 @@ class Tab extends import_events.EventEmitter {
52
54
  type: "fileChooser",
53
55
  description: "File chooser",
54
56
  fileChooser: chooser,
55
- clearedBy: "browser_file_upload"
57
+ clearedBy: import_files.uploadFile.schema.name
56
58
  });
57
59
  });
58
60
  page.on("dialog", (dialog) => this._dialogShown(dialog));
@@ -62,10 +64,27 @@ class Tab extends import_events.EventEmitter {
62
64
  page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
63
65
  page.setDefaultTimeout(this.context.config.timeouts.action);
64
66
  page[tabSymbol] = this;
67
+ void this._initialize();
65
68
  }
66
69
  static forPage(page) {
67
70
  return page[tabSymbol];
68
71
  }
72
+ async _initialize() {
73
+ const messages = await this.page.consoleMessages().catch(() => []);
74
+ for (const message of messages)
75
+ this._handleConsoleMessage(messageToConsoleMessage(message));
76
+ const errors = await this.page.pageErrors().catch(() => []);
77
+ for (const error of errors)
78
+ this._handleConsoleMessage(pageErrorToConsoleMessage(error));
79
+ const requests = await this.page.requests().catch(() => []);
80
+ for (const request of requests) {
81
+ this._requests.set(request, null);
82
+ void request.response().catch(() => null).then((response) => {
83
+ if (response)
84
+ this._requests.set(request, response);
85
+ });
86
+ }
87
+ }
69
88
  modalStates() {
70
89
  return this._modalStates;
71
90
  }
@@ -84,14 +103,14 @@ class Tab extends import_events.EventEmitter {
84
103
  type: "dialog",
85
104
  description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
86
105
  dialog,
87
- clearedBy: "browser_handle_dialog"
106
+ clearedBy: import_dialogs.handleDialog.schema.name
88
107
  });
89
108
  }
90
109
  async _downloadStarted(download) {
91
110
  const entry = {
92
111
  download,
93
112
  finished: false,
94
- outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web" })
113
+ outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", reason: "Saving download" })
95
114
  };
96
115
  this._downloads.push(entry);
97
116
  await download.saveAs(entry.outputFile);
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var dialogs_exports = {};
20
20
  __export(dialogs_exports, {
21
- default: () => dialogs_default
21
+ default: () => dialogs_default,
22
+ handleDialog: () => handleDialog
22
23
  });
23
24
  module.exports = __toCommonJS(dialogs_exports);
24
25
  var import_bundle = require("../../sdk/bundle");
@@ -53,3 +54,7 @@ const handleDialog = (0, import_tool.defineTabTool)({
53
54
  var dialogs_default = [
54
55
  handleDialog
55
56
  ];
57
+ // Annotate the CommonJS export names for ESM import in node:
58
+ 0 && (module.exports = {
59
+ handleDialog
60
+ });
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var files_exports = {};
20
20
  __export(files_exports, {
21
- default: () => files_default
21
+ default: () => files_default,
22
+ uploadFile: () => uploadFile
22
23
  });
23
24
  module.exports = __toCommonJS(files_exports);
24
25
  var import_bundle = require("../../sdk/bundle");
@@ -51,3 +52,7 @@ const uploadFile = (0, import_tool.defineTabTool)({
51
52
  var files_default = [
52
53
  uploadFile
53
54
  ];
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ uploadFile
58
+ });
@@ -48,7 +48,7 @@ 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 ?? `page-${(0, import_utils.dateAsFileName)()}.pdf`, { origin: "llm" });
51
+ const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)("pdf"), { origin: "llm", reason: "Saving PDF" });
52
52
  response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
53
53
  response.addResult(`Saved page as ${fileName}`);
54
54
  await tab.page.pdf({ path: fileName });
@@ -63,7 +63,7 @@ const screenshot = (0, import_tool.defineTabTool)({
63
63
  },
64
64
  handle: async (tab, params, response) => {
65
65
  const fileType = params.type || "png";
66
- const fileName = await tab.context.outputFile(params.filename ?? `page-${(0, import_utils.dateAsFileName)()}.${fileType}`, { origin: "llm" });
66
+ const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
67
67
  const options = {
68
68
  type: fileType,
69
69
  quality: fileType === "png" ? void 0 : 90,
@@ -34,7 +34,7 @@ const tracingStart = (0, import_tool.defineTool)({
34
34
  },
35
35
  handle: async (context, params, response) => {
36
36
  const browserContext = await context.ensureBrowserContext();
37
- const tracesDir = await context.outputFile(`traces`, { origin: "code" });
37
+ const tracesDir = await context.outputFile(`traces`, { origin: "code", reason: "Collecting trace" });
38
38
  const name = "trace-" + Date.now();
39
39
  await browserContext.tracing.start({
40
40
  name,
@@ -83,9 +83,9 @@ async function generateLocator(locator) {
83
83
  async function callOnPageNoTrace(page, callback) {
84
84
  return await page._wrapApiCall(() => callback(page), { internal: true });
85
85
  }
86
- function dateAsFileName() {
86
+ function dateAsFileName(extension) {
87
87
  const date = /* @__PURE__ */ new Date();
88
- return date.toISOString().replace(/[:.]/g, "-");
88
+ return `page-${date.toISOString().replace(/[:.]/g, "-")}.${extension}`;
89
89
  }
90
90
  // Annotate the CommonJS export names for ESM import in node:
91
91
  0 && (module.exports = {
@@ -70,6 +70,11 @@ const browserTools = [
70
70
  ...import_wait.default,
71
71
  ...import_verify.default
72
72
  ];
73
+ const customPrefix = process.env.PLAYWRIGHT_MCP_TOOL_PREFIX;
74
+ if (customPrefix) {
75
+ for (const tool of browserTools)
76
+ tool.schema.name = customPrefix + tool.schema.name;
77
+ }
73
78
  function filteredTools(config) {
74
79
  return browserTools.filter((tool) => tool.capability.startsWith("core") || config.capabilities?.includes(tool.capability));
75
80
  }
package/lib/mcp/log.js CHANGED
@@ -23,9 +23,9 @@ __export(log_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(log_exports);
25
25
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
26
- const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
26
+ const errorDebug = (0, import_utilsBundle.debug)("pw:mcp:error");
27
27
  function logUnhandledError(error) {
28
- errorsDebug(error);
28
+ errorDebug(error);
29
29
  }
30
30
  const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
31
31
  // Annotate the CommonJS export names for ESM import in node:
@@ -41,7 +41,7 @@ var import_browserServerBackend = require("./browser/browserServerBackend");
41
41
  var import_extensionContextFactory = require("./extension/extensionContextFactory");
42
42
  var import_host = require("./vscode/host");
43
43
  function decorateCommand(command, version) {
44
- command.option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"').addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
44
+ command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--save-video <size>", 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', import_config.resolutionParser.bind(null, "--save-video")).option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
45
45
  (0, import_watchdog.setupExitWatchdog)();
46
46
  if (options.vision) {
47
47
  console.error("The --vision option is deprecated, use --caps=vision instead");
@@ -67,19 +67,31 @@ function httpAddressToString(address) {
67
67
  resolvedHost = "localhost";
68
68
  return `http://${resolvedHost}:${resolvedPort}`;
69
69
  }
70
- async function installHttpTransport(httpServer, serverBackendFactory) {
70
+ async function installHttpTransport(httpServer, serverBackendFactory, allowedHosts) {
71
+ const url = httpAddressToString(httpServer.address());
72
+ const host = new URL(url).host;
73
+ allowedHosts = (allowedHosts || [host]).map((h) => h.toLowerCase());
71
74
  const sseSessions = /* @__PURE__ */ new Map();
72
75
  const streamableSessions = /* @__PURE__ */ new Map();
73
76
  httpServer.on("request", async (req, res) => {
74
- const url = new URL(`http://localhost${req.url}`);
75
- if (url.pathname === "/killkillkill" && req.method === "GET") {
77
+ const host2 = req.headers.host?.toLowerCase();
78
+ if (!host2) {
79
+ res.statusCode = 400;
80
+ return res.end("Missing host");
81
+ }
82
+ if (!allowedHosts.includes(host2)) {
83
+ res.statusCode = 403;
84
+ return res.end("Access is only allowed at " + allowedHosts.join(", "));
85
+ }
86
+ const url2 = new URL(`http://localhost${req.url}`);
87
+ if (url2.pathname === "/killkillkill" && req.method === "GET") {
76
88
  res.statusCode = 200;
77
89
  res.end("Killing process");
78
90
  process.emit("SIGINT");
79
91
  return;
80
92
  }
81
- if (url.pathname.startsWith("/sse"))
82
- await handleSSE(serverBackendFactory, req, res, url, sseSessions);
93
+ if (url2.pathname.startsWith("/sse"))
94
+ await handleSSE(serverBackendFactory, req, res, url2, sseSessions);
83
95
  else
84
96
  await handleStreamable(serverBackendFactory, req, res, streamableSessions);
85
97
  });
@@ -124,8 +124,8 @@ async function start(serverBackendFactory, options) {
124
124
  return;
125
125
  }
126
126
  const httpServer = await (0, import_http.startHttpServer)(options);
127
- await (0, import_http.installHttpTransport)(httpServer, serverBackendFactory);
128
127
  const url = (0, import_http.httpAddressToString)(httpServer.address());
128
+ await (0, import_http.installHttpTransport)(httpServer, serverBackendFactory, options.allowedHosts);
129
129
  const mcpConfig = { mcpServers: {} };
130
130
  mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = {
131
131
  url: `${url}/mcp`
@@ -28,8 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var browserBackend_exports = {};
30
30
  __export(browserBackend_exports, {
31
- runBrowserBackendAtEnd: () => runBrowserBackendAtEnd,
32
- runBrowserBackendOnError: () => runBrowserBackendOnError
31
+ runBrowserBackendAtEnd: () => runBrowserBackendAtEnd
33
32
  });
34
33
  module.exports = __toCommonJS(browserBackend_exports);
35
34
  var mcp = __toESM(require("../sdk/exports"));
@@ -37,42 +36,40 @@ var import_globals = require("../../common/globals");
37
36
  var import_util = require("../../util");
38
37
  var import_config = require("../browser/config");
39
38
  var import_browserServerBackend = require("../browser/browserServerBackend");
40
- async function runBrowserBackendOnError(page, message) {
39
+ async function runBrowserBackendAtEnd(context, errorMessage) {
41
40
  const testInfo = (0, import_globals.currentTestInfo)();
42
- if (!testInfo || !testInfo._pauseOnError())
41
+ if (!testInfo)
43
42
  return;
44
- const config = {
45
- ...import_config.defaultConfig,
46
- capabilities: ["testing"]
47
- };
48
- const snapshot = await page._snapshotForAI();
49
- const introMessage = `### Paused on error:
50
- ${(0, import_util.stripAnsiEscapes)(message())}
51
-
52
- ### Current page snapshot:
53
- ${snapshot}
54
-
55
- ### Task
56
- Try recovering from the error prior to continuing`;
57
- await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config, identityFactory(page.context())), introMessage);
58
- }
59
- async function runBrowserBackendAtEnd(context) {
60
- const testInfo = (0, import_globals.currentTestInfo)();
61
- if (!testInfo || !testInfo._pauseAtEnd())
43
+ const shouldPause = errorMessage ? testInfo?._pauseOnError() : testInfo?._pauseAtEnd();
44
+ if (!shouldPause)
62
45
  return;
63
- const page = context.pages()[0];
64
- if (!page)
65
- return;
66
- const snapshot = await page._snapshotForAI();
67
- const introMessage = `### Paused at end of test. ready for interaction
68
-
69
- ### Current page snapshot:
70
- ${snapshot}`;
46
+ const lines = [];
47
+ if (errorMessage)
48
+ lines.push(`### Paused on error:`, (0, import_util.stripAnsiEscapes)(errorMessage));
49
+ else
50
+ lines.push(`### Paused at end of test. ready for interaction`);
51
+ for (let i = 0; i < context.pages().length; i++) {
52
+ const page = context.pages()[i];
53
+ const stateSuffix = context.pages().length > 1 ? i + 1 + " of " + context.pages().length : "state";
54
+ lines.push(
55
+ "",
56
+ `### Page ${stateSuffix}`,
57
+ `- Page URL: ${page.url()}`,
58
+ `- Page Title: ${await page.title()}`.trim(),
59
+ `- Page Snapshot:`,
60
+ "```yaml",
61
+ await page._snapshotForAI(),
62
+ "```"
63
+ );
64
+ }
65
+ lines.push("");
66
+ if (errorMessage)
67
+ lines.push(`### Task`, `Try recovering from the error prior to continuing`);
71
68
  const config = {
72
69
  ...import_config.defaultConfig,
73
70
  capabilities: ["testing"]
74
71
  };
75
- await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config, identityFactory(context)), introMessage);
72
+ await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config, identityFactory(context)), lines.join("\n"));
76
73
  }
77
74
  function identityFactory(browserContext) {
78
75
  return {
@@ -87,6 +84,5 @@ function identityFactory(browserContext) {
87
84
  }
88
85
  // Annotate the CommonJS export names for ESM import in node:
89
86
  0 && (module.exports = {
90
- runBrowserBackendAtEnd,
91
- runBrowserBackendOnError
87
+ runBrowserBackendAtEnd
92
88
  });
@@ -50,6 +50,7 @@ class TestInfoImpl {
50
50
  this._lastStepId = 0;
51
51
  this._steps = [];
52
52
  this._stepMap = /* @__PURE__ */ new Map();
53
+ this._onDidFinishTestFunctions = [];
53
54
  this._hasNonRetriableError = false;
54
55
  this._hasUnhandledError = false;
55
56
  this._allowSkips = false;
@@ -318,7 +318,10 @@ class WorkerMain extends import_process.ProcessRunner {
318
318
  await testInfo._runAsStep({ title: "After Hooks", category: "hook" }, async () => {
319
319
  let firstAfterHooksError;
320
320
  try {
321
- await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, async () => testInfo._onDidFinishTestFunction?.());
321
+ await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, async () => {
322
+ for (const fn of testInfo._onDidFinishTestFunctions)
323
+ await fn();
324
+ });
322
325
  } catch (error) {
323
326
  firstAfterHooksError = firstAfterHooksError ?? error;
324
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright",
3
- "version": "1.56.0-alpha-2025-09-22",
3
+ "version": "1.56.0-alpha-2025-09-24",
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.56.0-alpha-2025-09-22"
67
+ "playwright-core": "1.56.0-alpha-2025-09-24"
68
68
  },
69
69
  "optionalDependencies": {
70
70
  "fsevents": "2.3.2"