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.
- package/README.md +3 -3
- package/ThirdPartyNotices.txt +202 -282
- package/lib/agents/copilot-setup-steps.yml +34 -0
- package/lib/agents/generateAgents.js +292 -160
- package/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
- package/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
- package/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
- package/lib/common/config.js +6 -0
- package/lib/common/expectBundle.js +0 -9
- package/lib/common/expectBundleImpl.js +267 -249
- package/lib/common/testLoader.js +3 -2
- package/lib/common/testType.js +3 -12
- package/lib/common/validators.js +68 -0
- package/lib/index.js +9 -9
- package/lib/isomorphic/teleReceiver.js +1 -0
- package/lib/isomorphic/testServerConnection.js +14 -0
- package/lib/loader/loaderMain.js +1 -1
- package/lib/matchers/expect.js +12 -13
- package/lib/matchers/matchers.js +16 -0
- package/lib/mcp/browser/browserServerBackend.js +1 -1
- package/lib/mcp/browser/config.js +9 -24
- package/lib/mcp/browser/context.js +4 -21
- package/lib/mcp/browser/response.js +20 -11
- package/lib/mcp/browser/tab.js +25 -10
- package/lib/mcp/browser/tools/evaluate.js +2 -3
- package/lib/mcp/browser/tools/form.js +2 -3
- package/lib/mcp/browser/tools/keyboard.js +4 -5
- package/lib/mcp/browser/tools/pdf.js +1 -1
- package/lib/mcp/browser/tools/runCode.js +75 -0
- package/lib/mcp/browser/tools/screenshot.js +33 -15
- package/lib/mcp/browser/tools/snapshot.js +13 -14
- package/lib/mcp/browser/tools/tabs.js +2 -2
- package/lib/mcp/browser/tools/utils.js +0 -11
- package/lib/mcp/browser/tools/verify.js +3 -4
- package/lib/mcp/browser/tools.js +2 -0
- package/lib/mcp/program.js +21 -1
- package/lib/mcp/sdk/exports.js +1 -3
- package/lib/mcp/sdk/http.js +9 -2
- package/lib/mcp/sdk/proxyBackend.js +1 -1
- package/lib/mcp/sdk/server.js +13 -5
- package/lib/mcp/sdk/tool.js +2 -6
- package/lib/mcp/test/browserBackend.js +43 -33
- package/lib/mcp/test/generatorTools.js +3 -3
- package/lib/mcp/test/plannerTools.js +103 -5
- package/lib/mcp/test/seed.js +25 -15
- package/lib/mcp/test/streams.js +9 -4
- package/lib/mcp/test/testBackend.js +31 -29
- package/lib/mcp/test/testContext.js +143 -40
- package/lib/mcp/test/testTools.js +12 -21
- package/lib/plugins/webServerPlugin.js +37 -9
- package/lib/program.js +11 -20
- package/lib/reporters/html.js +2 -23
- package/lib/reporters/internalReporter.js +4 -2
- package/lib/reporters/junit.js +4 -2
- package/lib/reporters/list.js +1 -5
- package/lib/reporters/merge.js +12 -6
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +26 -2
- package/lib/runner/failureTracker.js +5 -5
- package/lib/runner/loadUtils.js +2 -1
- package/lib/runner/loaderHost.js +1 -1
- package/lib/runner/reporters.js +5 -4
- package/lib/runner/testRunner.js +8 -9
- package/lib/runner/testServer.js +8 -3
- package/lib/runner/workerHost.js +3 -0
- package/lib/worker/testInfo.js +28 -17
- package/lib/worker/testTracing.js +1 -0
- package/lib/worker/workerMain.js +15 -6
- package/package.json +2 -2
- package/types/test.d.ts +96 -3
- package/types/testReporter.d.ts +5 -0
- package/lib/mcp/sdk/mdb.js +0 -208
package/lib/mcp/browser/tab.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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.${
|
|
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.${
|
|
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.${
|
|
67
|
+
response.addCode(`await page.${resolved}.pressSequentially(${secret.code});`);
|
|
69
68
|
await locator.pressSequentially(secret.value);
|
|
70
69
|
} else {
|
|
71
|
-
response.addCode(`await page.${
|
|
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.${
|
|
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
|
|
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
|
|
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
|
|
72
|
-
if (
|
|
73
|
-
response.addCode(`await page.${
|
|
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 =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.${
|
|
80
|
+
response.addCode(`await page.${resolved}.dblclick(${optionsAttr});`);
|
|
82
81
|
else
|
|
83
|
-
response.addCode(`await page.${
|
|
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 [
|
|
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
|
|
112
|
+
await start.locator.dragTo(end.locator);
|
|
114
113
|
});
|
|
115
|
-
response.addCode(`await page.${
|
|
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.${
|
|
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.${
|
|
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
|
|
168
|
-
response.addResult(
|
|
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.${
|
|
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) {
|
package/lib/mcp/browser/tools.js
CHANGED
|
@@ -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,
|
package/lib/mcp/program.js
CHANGED
|
@@ -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("--
|
|
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
|
package/lib/mcp/sdk/exports.js
CHANGED
|
@@ -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
|
});
|
package/lib/mcp/sdk/http.js
CHANGED
|
@@ -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
|
-
|
|
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") {
|