playwright 1.56.1 → 1.57.0

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 (76) hide show
  1. package/README.md +3 -3
  2. package/ThirdPartyNotices.txt +202 -282
  3. package/lib/agents/copilot-setup-steps.yml +34 -0
  4. package/lib/agents/generateAgents.js +292 -160
  5. package/lib/agents/playwright-test-coverage.prompt.md +31 -0
  6. package/lib/agents/playwright-test-generate.prompt.md +8 -0
  7. package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
  8. package/lib/agents/playwright-test-heal.prompt.md +6 -0
  9. package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
  10. package/lib/agents/playwright-test-plan.prompt.md +9 -0
  11. package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
  12. package/lib/common/config.js +6 -0
  13. package/lib/common/expectBundle.js +0 -9
  14. package/lib/common/expectBundleImpl.js +267 -249
  15. package/lib/common/testLoader.js +3 -2
  16. package/lib/common/testType.js +3 -12
  17. package/lib/common/validators.js +68 -0
  18. package/lib/index.js +9 -9
  19. package/lib/isomorphic/teleReceiver.js +1 -0
  20. package/lib/isomorphic/testServerConnection.js +14 -0
  21. package/lib/loader/loaderMain.js +1 -1
  22. package/lib/matchers/expect.js +12 -13
  23. package/lib/matchers/matchers.js +16 -0
  24. package/lib/mcp/browser/browserServerBackend.js +1 -1
  25. package/lib/mcp/browser/config.js +9 -24
  26. package/lib/mcp/browser/context.js +4 -21
  27. package/lib/mcp/browser/response.js +20 -11
  28. package/lib/mcp/browser/tab.js +25 -10
  29. package/lib/mcp/browser/tools/evaluate.js +2 -3
  30. package/lib/mcp/browser/tools/form.js +2 -3
  31. package/lib/mcp/browser/tools/keyboard.js +4 -5
  32. package/lib/mcp/browser/tools/pdf.js +1 -1
  33. package/lib/mcp/browser/tools/runCode.js +75 -0
  34. package/lib/mcp/browser/tools/screenshot.js +33 -15
  35. package/lib/mcp/browser/tools/snapshot.js +13 -14
  36. package/lib/mcp/browser/tools/tabs.js +2 -2
  37. package/lib/mcp/browser/tools/utils.js +0 -11
  38. package/lib/mcp/browser/tools/verify.js +3 -4
  39. package/lib/mcp/browser/tools.js +2 -0
  40. package/lib/mcp/program.js +21 -1
  41. package/lib/mcp/sdk/exports.js +1 -3
  42. package/lib/mcp/sdk/http.js +9 -2
  43. package/lib/mcp/sdk/proxyBackend.js +1 -1
  44. package/lib/mcp/sdk/server.js +13 -5
  45. package/lib/mcp/sdk/tool.js +2 -6
  46. package/lib/mcp/test/browserBackend.js +43 -33
  47. package/lib/mcp/test/generatorTools.js +3 -3
  48. package/lib/mcp/test/plannerTools.js +103 -5
  49. package/lib/mcp/test/seed.js +25 -15
  50. package/lib/mcp/test/streams.js +9 -4
  51. package/lib/mcp/test/testBackend.js +31 -29
  52. package/lib/mcp/test/testContext.js +143 -40
  53. package/lib/mcp/test/testTools.js +12 -21
  54. package/lib/plugins/webServerPlugin.js +37 -9
  55. package/lib/program.js +11 -20
  56. package/lib/reporters/html.js +2 -23
  57. package/lib/reporters/internalReporter.js +4 -2
  58. package/lib/reporters/junit.js +4 -2
  59. package/lib/reporters/list.js +1 -5
  60. package/lib/reporters/merge.js +12 -6
  61. package/lib/reporters/teleEmitter.js +3 -1
  62. package/lib/runner/dispatcher.js +26 -2
  63. package/lib/runner/failureTracker.js +5 -5
  64. package/lib/runner/loadUtils.js +2 -1
  65. package/lib/runner/loaderHost.js +1 -1
  66. package/lib/runner/reporters.js +5 -4
  67. package/lib/runner/testRunner.js +8 -9
  68. package/lib/runner/testServer.js +8 -3
  69. package/lib/runner/workerHost.js +3 -0
  70. package/lib/worker/testInfo.js +28 -17
  71. package/lib/worker/testTracing.js +1 -0
  72. package/lib/worker/workerMain.js +15 -6
  73. package/package.json +2 -2
  74. package/types/test.d.ts +96 -3
  75. package/types/testReporter.d.ts +5 -0
  76. package/lib/mcp/sdk/mdb.js +0 -208
@@ -29,6 +29,7 @@ var import_utils2 = require("./tools/utils");
29
29
  var import_log = require("../log");
30
30
  var import_dialogs = require("./tools/dialogs");
31
31
  var import_files = require("./tools/files");
32
+ var import_transform = require("../../transform/transform");
32
33
  const TabEvents = {
33
34
  modalState: "modalState"
34
35
  };
@@ -41,6 +42,7 @@ class Tab extends import_events.EventEmitter {
41
42
  this._requests = /* @__PURE__ */ new Set();
42
43
  this._modalStates = [];
43
44
  this._downloads = [];
45
+ this._needsFullSnapshot = false;
44
46
  this.context = context;
45
47
  this.page = page;
46
48
  this._onPageClose = onPageClose;
@@ -63,7 +65,7 @@ class Tab extends import_events.EventEmitter {
63
65
  page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
64
66
  page.setDefaultTimeout(this.context.config.timeouts.action);
65
67
  page[tabSymbol] = this;
66
- this._initializedPromise = this._initialize();
68
+ this.initializedPromise = this._initialize();
67
69
  }
68
70
  static forPage(page) {
69
71
  return page[tabSymbol];
@@ -84,6 +86,14 @@ class Tab extends import_events.EventEmitter {
84
86
  const requests = await this.page.requests().catch(() => []);
85
87
  for (const request of requests)
86
88
  this._requests.add(request);
89
+ for (const initPage of this.context.config.browser.initPage || []) {
90
+ try {
91
+ const { default: func } = await (0, import_transform.requireOrImport)(initPage);
92
+ await func({ page: this.page });
93
+ } catch (e) {
94
+ (0, import_log.logUnhandledError)(e);
95
+ }
96
+ }
87
97
  }
88
98
  modalStates() {
89
99
  return this._modalStates;
@@ -165,21 +175,22 @@ class Tab extends import_events.EventEmitter {
165
175
  await this.waitForLoadState("load", { timeout: 5e3 });
166
176
  }
167
177
  async consoleMessages(type) {
168
- await this._initializedPromise;
178
+ await this.initializedPromise;
169
179
  return this._consoleMessages.filter((message) => type ? message.type === type : true);
170
180
  }
171
181
  async requests() {
172
- await this._initializedPromise;
182
+ await this.initializedPromise;
173
183
  return this._requests;
174
184
  }
175
185
  async captureSnapshot() {
176
186
  let tabSnapshot;
177
187
  const modalStates = await this._raceAgainstModalStates(async () => {
178
- const snapshot = await this.page._snapshotForAI();
188
+ const snapshot = await this.page._snapshotForAI({ track: "response" });
179
189
  tabSnapshot = {
180
190
  url: this.page.url(),
181
191
  title: await this.page.title(),
182
- ariaSnapshot: snapshot,
192
+ ariaSnapshot: snapshot.full,
193
+ ariaSnapshotDiff: this._needsFullSnapshot ? void 0 : snapshot.incremental,
183
194
  modalStates: [],
184
195
  consoleMessages: [],
185
196
  downloads: this._downloads
@@ -189,6 +200,7 @@ class Tab extends import_events.EventEmitter {
189
200
  tabSnapshot.consoleMessages = this._recentConsoleMessages;
190
201
  this._recentConsoleMessages = [];
191
202
  }
203
+ this._needsFullSnapshot = !tabSnapshot;
192
204
  return tabSnapshot ?? {
193
205
  url: this.page.url(),
194
206
  title: "",
@@ -222,12 +234,15 @@ class Tab extends import_events.EventEmitter {
222
234
  return (await this.refLocators([params]))[0];
223
235
  }
224
236
  async refLocators(params) {
225
- const snapshot = await this.page._snapshotForAI();
226
- return params.map((param) => {
227
- if (!snapshot.includes(`[ref=${param.ref}]`))
237
+ return Promise.all(params.map(async (param) => {
238
+ try {
239
+ const locator = this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
240
+ const { resolvedSelector } = await locator._resolveSelector();
241
+ return { locator, resolved: (0, import_utils.asLocator)("javascript", resolvedSelector) };
242
+ } catch (e) {
228
243
  throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
229
- return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
230
- });
244
+ }
245
+ }));
231
246
  }
232
247
  async waitForTimeout(time) {
233
248
  if (this._javaScriptBlocked()) {
@@ -34,7 +34,6 @@ module.exports = __toCommonJS(evaluate_exports);
34
34
  var import_bundle = require("../../sdk/bundle");
35
35
  var import_tool = require("./tool");
36
36
  var javascript = __toESM(require("../codegen"));
37
- var import_utils = require("./utils");
38
37
  const evaluateSchema = import_bundle.z.object({
39
38
  function: import_bundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
40
39
  element: import_bundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
@@ -54,12 +53,12 @@ const evaluate = (0, import_tool.defineTabTool)({
54
53
  let locator;
55
54
  if (params.ref && params.element) {
56
55
  locator = await tab.refLocator({ ref: params.ref, element: params.element });
57
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.evaluate(${javascript.quote(params.function)});`);
56
+ response.addCode(`await page.${locator.resolved}.evaluate(${javascript.quote(params.function)});`);
58
57
  } else {
59
58
  response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
60
59
  }
61
60
  await tab.waitForCompletion(async () => {
62
- const receiver = locator ?? tab.page;
61
+ const receiver = locator?.locator ?? tab.page;
63
62
  const result = await receiver._evaluateFunction(params.function);
64
63
  response.addResult(JSON.stringify(result, null, 2) || "undefined");
65
64
  });
@@ -33,7 +33,6 @@ __export(form_exports, {
33
33
  module.exports = __toCommonJS(form_exports);
34
34
  var import_bundle = require("../../sdk/bundle");
35
35
  var import_tool = require("./tool");
36
- var import_utils = require("./utils");
37
36
  var codegen = __toESM(require("../codegen"));
38
37
  const fillForm = (0, import_tool.defineTabTool)({
39
38
  capability: "core",
@@ -53,8 +52,8 @@ const fillForm = (0, import_tool.defineTabTool)({
53
52
  },
54
53
  handle: async (tab, params, response) => {
55
54
  for (const field of params.fields) {
56
- const locator = await tab.refLocator({ element: field.name, ref: field.ref });
57
- const locatorSource = `await page.${await (0, import_utils.generateLocator)(locator)}`;
55
+ const { locator, resolved } = await tab.refLocator({ element: field.name, ref: field.ref });
56
+ const locatorSource = `await page.${resolved}`;
58
57
  if (field.type === "textbox" || field.type === "slider") {
59
58
  const secret = tab.context.lookupSecret(field.value);
60
59
  await locator.fill(secret.value);
@@ -24,7 +24,6 @@ module.exports = __toCommonJS(keyboard_exports);
24
24
  var import_bundle = require("../../sdk/bundle");
25
25
  var import_tool = require("./tool");
26
26
  var import_snapshot = require("./snapshot");
27
- var import_utils = require("./utils");
28
27
  const pressKey = (0, import_tool.defineTabTool)({
29
28
  capability: "core",
30
29
  schema: {
@@ -60,20 +59,20 @@ const type = (0, import_tool.defineTabTool)({
60
59
  type: "input"
61
60
  },
62
61
  handle: async (tab, params, response) => {
63
- const locator = await tab.refLocator(params);
62
+ const { locator, resolved } = await tab.refLocator(params);
64
63
  const secret = tab.context.lookupSecret(params.text);
65
64
  await tab.waitForCompletion(async () => {
66
65
  if (params.slowly) {
67
66
  response.setIncludeSnapshot();
68
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.pressSequentially(${secret.code});`);
67
+ response.addCode(`await page.${resolved}.pressSequentially(${secret.code});`);
69
68
  await locator.pressSequentially(secret.value);
70
69
  } else {
71
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.fill(${secret.code});`);
70
+ response.addCode(`await page.${resolved}.fill(${secret.code});`);
72
71
  await locator.fill(secret.value);
73
72
  }
74
73
  if (params.submit) {
75
74
  response.setIncludeSnapshot();
76
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.press('Enter');`);
75
+ response.addCode(`await page.${resolved}.press('Enter');`);
77
76
  await locator.press("Enter");
78
77
  }
79
78
  });
@@ -36,7 +36,7 @@ var import_tool = require("./tool");
36
36
  var javascript = __toESM(require("../codegen"));
37
37
  var import_utils = require("./utils");
38
38
  const pdfSchema = import_bundle.z.object({
39
- filename: import_bundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.")
39
+ filename: import_bundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.")
40
40
  });
41
41
  const pdf = (0, import_tool.defineTabTool)({
42
42
  capability: "pdf",
@@ -0,0 +1,75 @@
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 runCode_exports = {};
30
+ __export(runCode_exports, {
31
+ default: () => runCode_default
32
+ });
33
+ module.exports = __toCommonJS(runCode_exports);
34
+ var import_vm = __toESM(require("vm"));
35
+ var import_utils = require("playwright-core/lib/utils");
36
+ var import_bundle = require("../../sdk/bundle");
37
+ var import_tool = require("./tool");
38
+ const codeSchema = import_bundle.z.object({
39
+ code: import_bundle.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();\``)
40
+ });
41
+ const runCode = (0, import_tool.defineTabTool)({
42
+ capability: "core",
43
+ schema: {
44
+ name: "browser_run_code",
45
+ title: "Run Playwright code",
46
+ description: "Run Playwright code snippet",
47
+ inputSchema: codeSchema,
48
+ type: "action"
49
+ },
50
+ handle: async (tab, params, response) => {
51
+ response.setIncludeSnapshot();
52
+ response.addCode(params.code);
53
+ const __end__ = new import_utils.ManualPromise();
54
+ const context = {
55
+ page: tab.page,
56
+ __end__
57
+ };
58
+ import_vm.default.createContext(context);
59
+ await tab.waitForCompletion(async () => {
60
+ const snippet = `(async () => {
61
+ try {
62
+ ${params.code};
63
+ __end__.resolve();
64
+ } catch (e) {
65
+ __end__.reject(e);
66
+ }
67
+ })()`;
68
+ import_vm.default.runInContext(snippet, context);
69
+ await __end__;
70
+ });
71
+ }
72
+ });
73
+ var runCode_default = [
74
+ runCode
75
+ ];
@@ -28,16 +28,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var screenshot_exports = {};
30
30
  __export(screenshot_exports, {
31
- default: () => screenshot_default
31
+ default: () => screenshot_default,
32
+ scaleImageToFitMessage: () => scaleImageToFitMessage
32
33
  });
33
34
  module.exports = __toCommonJS(screenshot_exports);
35
+ var import_fs = __toESM(require("fs"));
36
+ var import_utils = require("playwright-core/lib/utils");
37
+ var import_utilsBundle = require("playwright-core/lib/utilsBundle");
34
38
  var import_bundle = require("../../sdk/bundle");
35
39
  var import_tool = require("./tool");
36
40
  var javascript = __toESM(require("../codegen"));
37
- var import_utils = require("./utils");
41
+ var import_utils2 = require("./utils");
38
42
  const screenshotSchema = import_bundle.z.object({
39
43
  type: import_bundle.z.enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
40
- filename: import_bundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."),
44
+ filename: import_bundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory."),
41
45
  element: import_bundle.z.string().optional().describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
42
46
  ref: import_bundle.z.string().optional().describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
43
47
  fullPage: import_bundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
@@ -57,32 +61,46 @@ const screenshot = (0, import_tool.defineTabTool)({
57
61
  if (params.fullPage && params.ref)
58
62
  throw new Error("fullPage cannot be used with element screenshots.");
59
63
  const fileType = params.type || "png";
60
- const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
64
+ const fileName = await tab.context.outputFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
61
65
  const options = {
62
66
  type: fileType,
63
67
  quality: fileType === "png" ? void 0 : 90,
64
68
  scale: "css",
65
- path: fileName,
66
69
  ...params.fullPage !== void 0 && { fullPage: params.fullPage }
67
70
  };
68
71
  const isElementScreenshot = params.element && params.ref;
69
72
  const screenshotTarget = isElementScreenshot ? params.element : params.fullPage ? "full page" : "viewport";
70
73
  response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
71
- const locator = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
72
- if (locator)
73
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.screenshot(${javascript.formatObject(options)});`);
74
+ const ref = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
75
+ if (ref)
76
+ response.addCode(`await page.${ref.resolved}.screenshot(${javascript.formatObject(options)});`);
74
77
  else
75
78
  response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
76
- const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
79
+ const buffer = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
80
+ await (0, import_utils.mkdirIfNeeded)(fileName);
81
+ await import_fs.default.promises.writeFile(fileName, buffer);
77
82
  response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
78
- if (!params.fullPage) {
79
- response.addImage({
80
- contentType: fileType === "png" ? "image/png" : "image/jpeg",
81
- data: buffer
82
- });
83
- }
83
+ response.addImage({
84
+ contentType: fileType === "png" ? "image/png" : "image/jpeg",
85
+ data: scaleImageToFitMessage(buffer, fileType)
86
+ });
84
87
  }
85
88
  });
89
+ function scaleImageToFitMessage(buffer, imageType) {
90
+ const image = imageType === "png" ? import_utilsBundle.PNG.sync.read(buffer) : import_utilsBundle.jpegjs.decode(buffer, { maxMemoryUsageInMB: 512 });
91
+ const pixels = image.width * image.height;
92
+ const shrink = Math.min(1568 / image.width, 1568 / image.height, Math.sqrt(1.15 * 1024 * 1024 / pixels));
93
+ if (shrink > 1)
94
+ return buffer;
95
+ const width = image.width * shrink | 0;
96
+ const height = image.height * shrink | 0;
97
+ const scaledImage = (0, import_utils.scaleImageToSize)(image, { width, height });
98
+ return imageType === "png" ? import_utilsBundle.PNG.sync.write(scaledImage) : import_utilsBundle.jpegjs.encode(scaledImage, 80).data;
99
+ }
86
100
  var screenshot_default = [
87
101
  screenshot
88
102
  ];
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ scaleImageToFitMessage
106
+ });
@@ -35,7 +35,6 @@ module.exports = __toCommonJS(snapshot_exports);
35
35
  var import_bundle = require("../../sdk/bundle");
36
36
  var import_tool = require("./tool");
37
37
  var javascript = __toESM(require("../codegen"));
38
- var import_utils = require("./utils");
39
38
  const snapshot = (0, import_tool.defineTool)({
40
39
  capability: "core",
41
40
  schema: {
@@ -47,7 +46,7 @@ const snapshot = (0, import_tool.defineTool)({
47
46
  },
48
47
  handle: async (context, params, response) => {
49
48
  await context.ensureTab();
50
- response.setIncludeSnapshot();
49
+ response.setIncludeSnapshot("full");
51
50
  }
52
51
  });
53
52
  const elementSchema = import_bundle.z.object({
@@ -70,7 +69,7 @@ const click = (0, import_tool.defineTabTool)({
70
69
  },
71
70
  handle: async (tab, params, response) => {
72
71
  response.setIncludeSnapshot();
73
- const locator = await tab.refLocator(params);
72
+ const { locator, resolved } = await tab.refLocator(params);
74
73
  const options = {
75
74
  button: params.button,
76
75
  modifiers: params.modifiers
@@ -78,9 +77,9 @@ const click = (0, import_tool.defineTabTool)({
78
77
  const formatted = javascript.formatObject(options, " ", "oneline");
79
78
  const optionsAttr = formatted !== "{}" ? formatted : "";
80
79
  if (params.doubleClick)
81
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.dblclick(${optionsAttr});`);
80
+ response.addCode(`await page.${resolved}.dblclick(${optionsAttr});`);
82
81
  else
83
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.click(${optionsAttr});`);
82
+ response.addCode(`await page.${resolved}.click(${optionsAttr});`);
84
83
  await tab.waitForCompletion(async () => {
85
84
  if (params.doubleClick)
86
85
  await locator.dblclick(options);
@@ -105,14 +104,14 @@ const drag = (0, import_tool.defineTabTool)({
105
104
  },
106
105
  handle: async (tab, params, response) => {
107
106
  response.setIncludeSnapshot();
108
- const [startLocator, endLocator] = await tab.refLocators([
107
+ const [start, end] = await tab.refLocators([
109
108
  { ref: params.startRef, element: params.startElement },
110
109
  { ref: params.endRef, element: params.endElement }
111
110
  ]);
112
111
  await tab.waitForCompletion(async () => {
113
- await startLocator.dragTo(endLocator);
112
+ await start.locator.dragTo(end.locator);
114
113
  });
115
- response.addCode(`await page.${await (0, import_utils.generateLocator)(startLocator)}.dragTo(page.${await (0, import_utils.generateLocator)(endLocator)});`);
114
+ response.addCode(`await page.${start.resolved}.dragTo(page.${end.resolved});`);
116
115
  }
117
116
  });
118
117
  const hover = (0, import_tool.defineTabTool)({
@@ -126,8 +125,8 @@ const hover = (0, import_tool.defineTabTool)({
126
125
  },
127
126
  handle: async (tab, params, response) => {
128
127
  response.setIncludeSnapshot();
129
- const locator = await tab.refLocator(params);
130
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.hover();`);
128
+ const { locator, resolved } = await tab.refLocator(params);
129
+ response.addCode(`await page.${resolved}.hover();`);
131
130
  await tab.waitForCompletion(async () => {
132
131
  await locator.hover();
133
132
  });
@@ -147,8 +146,8 @@ const selectOption = (0, import_tool.defineTabTool)({
147
146
  },
148
147
  handle: async (tab, params, response) => {
149
148
  response.setIncludeSnapshot();
150
- const locator = await tab.refLocator(params);
151
- response.addCode(`await page.${await (0, import_utils.generateLocator)(locator)}.selectOption(${javascript.formatObject(params.values)});`);
149
+ const { locator, resolved } = await tab.refLocator(params);
150
+ response.addCode(`await page.${resolved}.selectOption(${javascript.formatObject(params.values)});`);
152
151
  await tab.waitForCompletion(async () => {
153
152
  await locator.selectOption(params.values);
154
153
  });
@@ -164,8 +163,8 @@ const pickLocator = (0, import_tool.defineTabTool)({
164
163
  type: "readOnly"
165
164
  },
166
165
  handle: async (tab, params, response) => {
167
- const locator = await tab.refLocator(params);
168
- response.addResult(await (0, import_utils.generateLocator)(locator));
166
+ const { resolved } = await tab.refLocator(params);
167
+ response.addResult(resolved);
169
168
  }
170
169
  });
171
170
  var snapshot_default = [
@@ -49,14 +49,14 @@ const browserTabs = (0, import_tool.defineTool)({
49
49
  }
50
50
  case "close": {
51
51
  await context.closeTab(params.index);
52
- response.setIncludeSnapshot();
52
+ response.setIncludeSnapshot("full");
53
53
  return;
54
54
  }
55
55
  case "select": {
56
56
  if (params.index === void 0)
57
57
  throw new Error("Tab index is required");
58
58
  await context.selectTab(params.index);
59
- response.setIncludeSnapshot();
59
+ response.setIncludeSnapshot("full");
60
60
  return;
61
61
  }
62
62
  }
@@ -20,11 +20,9 @@ var utils_exports = {};
20
20
  __export(utils_exports, {
21
21
  callOnPageNoTrace: () => callOnPageNoTrace,
22
22
  dateAsFileName: () => dateAsFileName,
23
- generateLocator: () => generateLocator,
24
23
  waitForCompletion: () => waitForCompletion
25
24
  });
26
25
  module.exports = __toCommonJS(utils_exports);
27
- var import_utils = require("playwright-core/lib/utils");
28
26
  async function waitForCompletion(tab, callback) {
29
27
  const requests = /* @__PURE__ */ new Set();
30
28
  let frameNavigated = false;
@@ -76,14 +74,6 @@ async function waitForCompletion(tab, callback) {
76
74
  dispose();
77
75
  }
78
76
  }
79
- async function generateLocator(locator) {
80
- try {
81
- const { resolvedSelector } = await locator._resolveSelector();
82
- return (0, import_utils.asLocator)("javascript", resolvedSelector);
83
- } catch (e) {
84
- throw new Error("Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.");
85
- }
86
- }
87
77
  async function callOnPageNoTrace(page, callback) {
88
78
  return await page._wrapApiCall(() => callback(page), { internal: true });
89
79
  }
@@ -95,6 +85,5 @@ function dateAsFileName(extension) {
95
85
  0 && (module.exports = {
96
86
  callOnPageNoTrace,
97
87
  dateAsFileName,
98
- generateLocator,
99
88
  waitForCompletion
100
89
  });
@@ -34,7 +34,6 @@ module.exports = __toCommonJS(verify_exports);
34
34
  var import_bundle = require("../../sdk/bundle");
35
35
  var import_tool = require("./tool");
36
36
  var javascript = __toESM(require("../codegen"));
37
- var import_utils = require("./utils");
38
37
  const verifyElement = (0, import_tool.defineTabTool)({
39
38
  capability: "testing",
40
39
  schema: {
@@ -92,7 +91,7 @@ const verifyList = (0, import_tool.defineTabTool)({
92
91
  type: "assertion"
93
92
  },
94
93
  handle: async (tab, params, response) => {
95
- const locator = await tab.refLocator({ ref: params.ref, element: params.element });
94
+ const { locator } = await tab.refLocator({ ref: params.ref, element: params.element });
96
95
  const itemTexts = [];
97
96
  for (const item of params.items) {
98
97
  const itemLocator = locator.getByText(item);
@@ -125,8 +124,8 @@ const verifyValue = (0, import_tool.defineTabTool)({
125
124
  type: "assertion"
126
125
  },
127
126
  handle: async (tab, params, response) => {
128
- const locator = await tab.refLocator({ ref: params.ref, element: params.element });
129
- const locatorSource = `page.${await (0, import_utils.generateLocator)(locator)}`;
127
+ const { locator, resolved } = await tab.refLocator({ ref: params.ref, element: params.element });
128
+ const locatorSource = `page.${resolved}`;
130
129
  if (params.type === "textbox" || params.type === "slider" || params.type === "combobox") {
131
130
  const value = await locator.inputValue();
132
131
  if (value !== params.value) {
@@ -44,6 +44,7 @@ var import_mouse = __toESM(require("./tools/mouse"));
44
44
  var import_navigate = __toESM(require("./tools/navigate"));
45
45
  var import_network = __toESM(require("./tools/network"));
46
46
  var import_pdf = __toESM(require("./tools/pdf"));
47
+ var import_runCode = __toESM(require("./tools/runCode"));
47
48
  var import_snapshot = __toESM(require("./tools/snapshot"));
48
49
  var import_screenshot = __toESM(require("./tools/screenshot"));
49
50
  var import_tabs = __toESM(require("./tools/tabs"));
@@ -63,6 +64,7 @@ const browserTools = [
63
64
  ...import_network.default,
64
65
  ...import_mouse.default,
65
66
  ...import_pdf.default,
67
+ ...import_runCode.default,
66
68
  ...import_screenshot.default,
67
69
  ...import_snapshot.default,
68
70
  ...import_tabs.default,
@@ -31,7 +31,9 @@ __export(program_exports, {
31
31
  decorateCommand: () => decorateCommand
32
32
  });
33
33
  module.exports = __toCommonJS(program_exports);
34
+ var import_fs = __toESM(require("fs"));
34
35
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
36
+ var import_server = require("playwright-core/lib/server");
35
37
  var mcpServer = __toESM(require("./sdk/server"));
36
38
  var import_config = require("./browser/config");
37
39
  var import_watchdog = require("./browser/watchdog");
@@ -40,13 +42,23 @@ var import_proxyBackend = require("./sdk/proxyBackend");
40
42
  var import_browserServerBackend = require("./browser/browserServerBackend");
41
43
  var import_extensionContextFactory = require("./extension/extensionContextFactory");
42
44
  function decorateCommand(command, version) {
43
- 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. Pass '*' to disable the host check.", 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("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
45
+ 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. Pass '*' to disable the host check.", import_config.commaSeparatedList).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-page <path...>", "path to TypeScript file to evaluate on Playwright page object").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("--test-id-attribute <attribute>", 'specify the attribute to use for test ids, defaults to "data-testid"').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("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
44
46
  (0, import_watchdog.setupExitWatchdog)();
45
47
  if (options.vision) {
46
48
  console.error("The --vision option is deprecated, use --caps=vision instead");
47
49
  options.caps = "vision";
48
50
  }
49
51
  const config = await (0, import_config.resolveCLIConfig)(options);
52
+ if (config.saveVideo && !checkFfmpeg()) {
53
+ console.error(import_utilsBundle.colors.red(`
54
+ Error: ffmpeg required to save the video is not installed.`));
55
+ console.error(`
56
+ Please run the command below. It will install a local copy of ffmpeg and will not change any system-wide settings.`);
57
+ console.error(`
58
+ npx playwright install ffmpeg
59
+ `);
60
+ process.exit(1);
61
+ }
50
62
  const browserContextFactory = (0, import_browserContextFactory.contextFactory)(config);
51
63
  const extensionContextFactory = new import_extensionContextFactory.ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir, config.browser.launchOptions.executablePath);
52
64
  if (options.extension) {
@@ -90,6 +102,14 @@ function decorateCommand(command, version) {
90
102
  await mcpServer.start(factory, config.server);
91
103
  });
92
104
  }
105
+ function checkFfmpeg() {
106
+ try {
107
+ const executable = import_server.registry.findExecutable("ffmpeg");
108
+ return import_fs.default.existsSync(executable.executablePath("javascript"));
109
+ } catch (error) {
110
+ return false;
111
+ }
112
+ }
93
113
  // Annotate the CommonJS export names for ESM import in node:
94
114
  0 && (module.exports = {
95
115
  decorateCommand
@@ -20,13 +20,11 @@ __reExport(exports_exports, require("./proxyBackend"), module.exports);
20
20
  __reExport(exports_exports, require("./server"), module.exports);
21
21
  __reExport(exports_exports, require("./tool"), module.exports);
22
22
  __reExport(exports_exports, require("./http"), module.exports);
23
- __reExport(exports_exports, require("./mdb"), module.exports);
24
23
  // Annotate the CommonJS export names for ESM import in node:
25
24
  0 && (module.exports = {
26
25
  ...require("./inProcessTransport"),
27
26
  ...require("./proxyBackend"),
28
27
  ...require("./server"),
29
28
  ...require("./tool"),
30
- ...require("./http"),
31
- ...require("./mdb")
29
+ ...require("./http")
32
30
  });
@@ -67,11 +67,12 @@ function httpAddressToString(address) {
67
67
  resolvedHost = "localhost";
68
68
  return `http://${resolvedHost}:${resolvedPort}`;
69
69
  }
70
- async function installHttpTransport(httpServer, serverBackendFactory, allowedHosts) {
70
+ async function installHttpTransport(httpServer, serverBackendFactory, unguessableUrl, allowedHosts) {
71
71
  const url = httpAddressToString(httpServer.address());
72
72
  const host = new URL(url).host;
73
73
  allowedHosts = (allowedHosts || [host]).map((h) => h.toLowerCase());
74
74
  const allowAnyHost = allowedHosts.includes("*");
75
+ const pathPrefix = unguessableUrl ? `/${import_crypto.default.randomUUID()}` : "";
75
76
  const sseSessions = /* @__PURE__ */ new Map();
76
77
  const streamableSessions = /* @__PURE__ */ new Map();
77
78
  httpServer.on("request", async (req, res) => {
@@ -86,7 +87,12 @@ async function installHttpTransport(httpServer, serverBackendFactory, allowedHos
86
87
  return res.end("Access is only allowed at " + allowedHosts.join(", "));
87
88
  }
88
89
  }
89
- const url2 = new URL(`http://localhost${req.url}`);
90
+ if (!req.url?.startsWith(pathPrefix)) {
91
+ res.statusCode = 404;
92
+ return res.end("Not found");
93
+ }
94
+ const path = req.url?.slice(pathPrefix.length);
95
+ const url2 = new URL(`http://localhost${path}`);
90
96
  if (url2.pathname === "/killkillkill" && req.method === "GET") {
91
97
  res.statusCode = 200;
92
98
  res.end("Killing process");
@@ -98,6 +104,7 @@ async function installHttpTransport(httpServer, serverBackendFactory, allowedHos
98
104
  else
99
105
  await handleStreamable(serverBackendFactory, req, res, streamableSessions);
100
106
  });
107
+ return `${url}${pathPrefix}`;
101
108
  }
102
109
  async function handleSSE(serverBackendFactory, req, res, url, sessions) {
103
110
  if (req.method === "POST") {