playwright 1.56.0-alpha-2025-09-23 → 1.56.0-alpha-1758747822000

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.
Files changed (37) hide show
  1. package/lib/agents/generateAgents.js +25 -10
  2. package/lib/agents/generator.md +5 -5
  3. package/lib/agents/healer.md +4 -4
  4. package/lib/agents/planner.md +5 -5
  5. package/lib/index.js +3 -50
  6. package/lib/matchers/toBeTruthy.js +0 -2
  7. package/lib/matchers/toEqual.js +0 -2
  8. package/lib/matchers/toMatchText.js +0 -3
  9. package/lib/mcp/browser/browserContextFactory.js +20 -9
  10. package/lib/mcp/browser/browserServerBackend.js +2 -0
  11. package/lib/mcp/browser/config.js +55 -18
  12. package/lib/mcp/browser/context.js +17 -1
  13. package/lib/mcp/browser/response.js +27 -20
  14. package/lib/mcp/browser/sessionLog.js +1 -1
  15. package/lib/mcp/browser/tab.js +6 -32
  16. package/lib/mcp/browser/tools/dialogs.js +6 -1
  17. package/lib/mcp/browser/tools/files.js +6 -1
  18. package/lib/mcp/browser/tools/pdf.js +1 -1
  19. package/lib/mcp/browser/tools/screenshot.js +1 -1
  20. package/lib/mcp/browser/tools/snapshot.js +1 -1
  21. package/lib/mcp/browser/tools/tracing.js +1 -1
  22. package/lib/mcp/browser/tools/utils.js +2 -2
  23. package/lib/mcp/browser/tools.js +5 -0
  24. package/lib/mcp/log.js +2 -2
  25. package/lib/mcp/program.js +1 -1
  26. package/lib/mcp/sdk/http.js +17 -5
  27. package/lib/mcp/sdk/server.js +1 -1
  28. package/lib/mcp/test/browserBackend.js +29 -33
  29. package/lib/program.js +6 -4
  30. package/lib/runner/lastRun.js +6 -16
  31. package/lib/runner/loadUtils.js +35 -2
  32. package/lib/runner/tasks.js +9 -1
  33. package/lib/runner/testRunner.js +2 -1
  34. package/lib/util.js +12 -6
  35. package/lib/worker/testInfo.js +1 -0
  36. package/lib/worker/workerMain.js +4 -1
  37. package/package.json +2 -2
@@ -18,16 +18,19 @@ 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 = [];
28
31
  this._code = [];
29
32
  this._images = [];
30
- this._includeSnapshot = "none";
33
+ this._includeSnapshot = false;
31
34
  this._includeTabs = false;
32
35
  this._context = context;
33
36
  this.toolName = toolName;
@@ -58,14 +61,14 @@ class Response {
58
61
  images() {
59
62
  return this._images;
60
63
  }
61
- setIncludeSnapshot(full) {
62
- this._includeSnapshot = full ?? "partial";
64
+ setIncludeSnapshot() {
65
+ this._includeSnapshot = true;
63
66
  }
64
67
  setIncludeTabs() {
65
68
  this._includeTabs = true;
66
69
  }
67
70
  async finish() {
68
- if (this._includeSnapshot !== "none" && this._context.currentTab())
71
+ if (this._includeSnapshot && this._context.currentTab())
69
72
  this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
70
73
  for (const tab of this._context.tabs())
71
74
  await tab.updateTitle();
@@ -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");
@@ -87,13 +98,13 @@ ${this._code.join("\n")}
87
98
  \`\`\``);
88
99
  response.push("");
89
100
  }
90
- if (this._includeSnapshot !== "none" || this._includeTabs)
101
+ if (this._includeSnapshot || this._includeTabs)
91
102
  response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
92
103
  if (this._tabSnapshot?.modalStates.length) {
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, this._includeSnapshot === "full"));
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, fullSnapshot) {
131
+ function renderTabSnapshot(tabSnapshot, options = {}) {
121
132
  const lines = [];
122
133
  if (tabSnapshot.consoleMessages.length) {
123
134
  lines.push(`### New console messages`);
@@ -138,15 +149,10 @@ function renderTabSnapshot(tabSnapshot, fullSnapshot) {
138
149
  lines.push(`### Page state`);
139
150
  lines.push(`- Page URL: ${tabSnapshot.url}`);
140
151
  lines.push(`- Page Title: ${tabSnapshot.title}`);
141
- if (!fullSnapshot && tabSnapshot.formattedAriaSnapshotDiff) {
142
- lines.push(`- Page Snapshot Diff:`);
143
- lines.push(tabSnapshot.formattedAriaSnapshotDiff);
144
- } else {
145
- lines.push(`- Page Snapshot:`);
146
- lines.push("```yaml");
147
- lines.push(tabSnapshot.ariaSnapshot);
148
- lines.push("```");
149
- }
152
+ lines.push(`- Page Snapshot:`);
153
+ lines.push("```yaml");
154
+ lines.push(options.omitSnapshot ? "<snapshot>" : tabSnapshot.ariaSnapshot);
155
+ lines.push("```");
150
156
  return lines.join("\n");
151
157
  }
152
158
  function renderTabsMarkdown(tabs, force = false) {
@@ -175,5 +181,6 @@ function trim(text, maxLength) {
175
181
  }
176
182
  // Annotate the CommonJS export names for ESM import in node:
177
183
  0 && (module.exports = {
178
- Response
184
+ Response,
185
+ requestDebug
179
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);
@@ -25,9 +25,10 @@ __export(tab_exports, {
25
25
  module.exports = __toCommonJS(tab_exports);
26
26
  var import_events = require("events");
27
27
  var import_utils = require("playwright-core/lib/utils");
28
- var import_utilsBundle = require("playwright-core/lib/utilsBundle");
29
28
  var import_utils2 = require("./tools/utils");
30
29
  var import_log = require("../log");
30
+ var import_dialogs = require("./tools/dialogs");
31
+ var import_files = require("./tools/files");
31
32
  const TabEvents = {
32
33
  modalState: "modalState"
33
34
  };
@@ -45,11 +46,7 @@ class Tab extends import_events.EventEmitter {
45
46
  this._onPageClose = onPageClose;
46
47
  page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
47
48
  page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
48
- page.on("request", (request) => {
49
- this._requests.set(request, null);
50
- if (request.frame() === page.mainFrame() && request.isNavigationRequest())
51
- this._willNavigateMainFrameToNewDocument();
52
- });
49
+ page.on("request", (request) => this._requests.set(request, null));
53
50
  page.on("response", (response) => this._requests.set(response.request(), response));
54
51
  page.on("close", () => this._onClose());
55
52
  page.on("filechooser", (chooser) => {
@@ -57,7 +54,7 @@ class Tab extends import_events.EventEmitter {
57
54
  type: "fileChooser",
58
55
  description: "File chooser",
59
56
  fileChooser: chooser,
60
- clearedBy: "browser_file_upload"
57
+ clearedBy: import_files.uploadFile.schema.name
61
58
  });
62
59
  });
63
60
  page.on("dialog", (dialog) => this._dialogShown(dialog));
@@ -106,14 +103,14 @@ class Tab extends import_events.EventEmitter {
106
103
  type: "dialog",
107
104
  description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
108
105
  dialog,
109
- clearedBy: "browser_handle_dialog"
106
+ clearedBy: import_dialogs.handleDialog.schema.name
110
107
  });
111
108
  }
112
109
  async _downloadStarted(download) {
113
110
  const entry = {
114
111
  download,
115
112
  finished: false,
116
- outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web" })
113
+ outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", reason: "Saving download" })
117
114
  };
118
115
  this._downloads.push(entry);
119
116
  await download.saveAs(entry.outputFile);
@@ -132,9 +129,6 @@ class Tab extends import_events.EventEmitter {
132
129
  this._clearCollectedArtifacts();
133
130
  this._onPageClose(this);
134
131
  }
135
- _willNavigateMainFrameToNewDocument() {
136
- this._lastAriaSnapshot = void 0;
137
- }
138
132
  async updateTitle() {
139
133
  await this._raceAgainstModalStates(async () => {
140
134
  this._lastTitle = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
@@ -151,7 +145,6 @@ class Tab extends import_events.EventEmitter {
151
145
  }
152
146
  async navigate(url) {
153
147
  this._clearCollectedArtifacts();
154
- this._willNavigateMainFrameToNewDocument();
155
148
  const downloadEvent = (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForEvent("download").catch(import_log.logUnhandledError));
156
149
  try {
157
150
  await this.page.goto(url, { waitUntil: "domcontentloaded" });
@@ -185,7 +178,6 @@ class Tab extends import_events.EventEmitter {
185
178
  url: this.page.url(),
186
179
  title: await this.page.title(),
187
180
  ariaSnapshot: snapshot,
188
- formattedAriaSnapshotDiff: this._lastAriaSnapshot ? generateAriaSnapshotDiff(this._lastAriaSnapshot, snapshot) : void 0,
189
181
  modalStates: [],
190
182
  consoleMessages: [],
191
183
  downloads: this._downloads
@@ -194,7 +186,6 @@ class Tab extends import_events.EventEmitter {
194
186
  if (tabSnapshot) {
195
187
  tabSnapshot.consoleMessages = this._recentConsoleMessages;
196
188
  this._recentConsoleMessages = [];
197
- this._lastAriaSnapshot = tabSnapshot.ariaSnapshot;
198
189
  }
199
190
  return tabSnapshot ?? {
200
191
  url: this.page.url(),
@@ -276,23 +267,6 @@ function renderModalStates(context, modalStates) {
276
267
  return result;
277
268
  }
278
269
  const tabSymbol = Symbol("tabSymbol");
279
- function generateAriaSnapshotDiff(oldSnapshot, newSnapshot) {
280
- const diffs = (0, import_utils.diffAriaSnapshots)(import_utilsBundle.yaml, oldSnapshot, newSnapshot);
281
- if (diffs === "equal")
282
- return "<no changes>";
283
- if (diffs === "different")
284
- return;
285
- if (diffs.length > 3 || diffs.some((diff) => diff.newSource.split("\n").length > 100)) {
286
- return;
287
- }
288
- const lines = [`The following refs have changed`];
289
- for (const diff of diffs)
290
- lines.push("", "```yaml", diff.newSource.trimEnd(), "```");
291
- const combined = lines.join("\n");
292
- if (combined.length >= newSnapshot.length)
293
- return;
294
- return combined;
295
- }
296
270
  // Annotate the CommonJS export names for ESM import in node:
297
271
  0 && (module.exports = {
298
272
  Tab,
@@ -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,
@@ -47,7 +47,7 @@ const snapshot = (0, import_tool.defineTool)({
47
47
  },
48
48
  handle: async (context, params, response) => {
49
49
  await context.ensureTab();
50
- response.setIncludeSnapshot("full");
50
+ response.setIncludeSnapshot();
51
51
  }
52
52
  });
53
53
  const elementSchema = import_bundle.z.object({
@@ -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
  });
package/lib/program.js CHANGED
@@ -174,12 +174,12 @@ function addInitAgentsCommand(program3) {
174
174
  const command = program3.command("init-agents", { hidden: true });
175
175
  command.description("Initialize repository agents for the Claude Code");
176
176
  const option = command.createOption("--loop <loop>", "Agentic loop provider");
177
- option.choices(["claude", "opencode", "vscode"]);
177
+ option.choices(["code", "claude", "opencode"]);
178
178
  command.addOption(option);
179
179
  command.action(async (opts) => {
180
180
  if (opts.loop === "opencode")
181
181
  await (0, import_generateAgents.initOpencodeRepo)();
182
- else if (opts.loop === "vscode")
182
+ else if (opts.loop === "code")
183
183
  await (0, import_generateAgents.initVSCodeRepo)();
184
184
  else if (opts.loop === "claude")
185
185
  await (0, import_generateAgents.initClaudeCodeRepo)();
@@ -197,7 +197,8 @@ async function runTests(args, opts) {
197
197
  config.cliProjectFilter = opts.project || void 0;
198
198
  config.cliPassWithNoTests = !!opts.passWithNoTests;
199
199
  config.cliLastFailed = !!opts.lastFailed;
200
- config.cliLastRunFile = opts.lastRunFile ? import_path.default.resolve(process.cwd(), opts.lastRunFile) : void 0;
200
+ config.cliTestList = opts.testList ? import_path.default.resolve(process.cwd(), opts.testList) : void 0;
201
+ config.cliTestListInvert = opts.testListInvert ? import_path.default.resolve(process.cwd(), opts.testListInvert) : void 0;
201
202
  (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
202
203
  if (opts.ui || opts.uiHost || opts.uiPort) {
203
204
  if (opts.onlyChanged)
@@ -356,7 +357,6 @@ const testOptions = [
356
357
  ["--headed", { description: `Run tests in headed browsers (default: headless)` }],
357
358
  ["--ignore-snapshots", { description: `Ignore screenshot and snapshot expectations` }],
358
359
  ["--last-failed", { description: `Only re-run the failures` }],
359
- ["--last-run-file <file>", { description: `Path to the last-run file (default: "test-results/.last-run.json")` }],
360
360
  ["--list", { description: `Collect all the tests and report them, but do not run` }],
361
361
  ["--max-failures <N>", { description: `Stop after the first N failures` }],
362
362
  ["--no-deps", { description: `Do not run project dependencies` }],
@@ -369,6 +369,8 @@ const testOptions = [
369
369
  ["--reporter <reporter>", { description: `Reporter to use, comma-separated, can be ${import_config.builtInReporters.map((name) => `"${name}"`).join(", ")} (default: "${import_config.defaultReporter}")` }],
370
370
  ["--retries <retries>", { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
371
371
  ["--shard <shard>", { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }],
372
+ ["--test-list <file>", { description: `Path to a file containing a list of tests to run. See https://playwright.dev/docs/test-cli for more details.` }],
373
+ ["--test-list-invert <file>", { description: `Path to a file containing a list of tests to skip. See https://playwright.dev/docs/test-cli for more details.` }],
372
374
  ["--timeout <timeout>", { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${import_config.defaultTimeout})` }],
373
375
  ["--trace <mode>", { description: `Force tracing mode`, choices: kTraceModes }],
374
376
  ["--tsconfig <path>", { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
@@ -37,27 +37,17 @@ var import_projectUtils = require("./projectUtils");
37
37
  class LastRunReporter {
38
38
  constructor(config) {
39
39
  this._config = config;
40
- if (config.cliLastRunFile) {
41
- this._lastRunFile = config.cliLastRunFile;
42
- } else {
43
- const [project] = (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
44
- if (project)
45
- this._lastRunFile = import_path.default.join(project.project.outputDir, ".last-run.json");
46
- }
40
+ const [project] = (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
41
+ if (project)
42
+ this._lastRunFile = import_path.default.join(project.project.outputDir, ".last-run.json");
47
43
  }
48
- async applyFilter() {
44
+ async filterLastFailed() {
49
45
  if (!this._lastRunFile)
50
46
  return;
51
47
  try {
52
48
  const lastRunInfo = JSON.parse(await import_fs.default.promises.readFile(this._lastRunFile, "utf8"));
53
- if (lastRunInfo.filterTests) {
54
- const filterTestIds = new Set(lastRunInfo.filterTests);
55
- this._config.preOnlyTestFilters.push((test) => filterTestIds.has(test.id));
56
- }
57
- if (this._config.cliLastFailed) {
58
- const failedTestIds = new Set(lastRunInfo.failedTests ?? []);
59
- this._config.postShardTestFilters.push((test) => failedTestIds.has(test.id));
60
- }
49
+ const failedTestIds = new Set(lastRunInfo.failedTests);
50
+ this._config.postShardTestFilters.push((test) => failedTestIds.has(test.id));
61
51
  } catch {
62
52
  }
63
53
  }