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.
- package/lib/agents/generateAgents.js +25 -10
- package/lib/agents/generator.md +5 -5
- package/lib/agents/healer.md +4 -4
- package/lib/agents/planner.md +5 -5
- package/lib/index.js +3 -50
- package/lib/matchers/toBeTruthy.js +0 -2
- package/lib/matchers/toEqual.js +0 -2
- package/lib/matchers/toMatchText.js +0 -3
- package/lib/mcp/browser/browserContextFactory.js +20 -9
- package/lib/mcp/browser/browserServerBackend.js +2 -0
- package/lib/mcp/browser/config.js +55 -18
- package/lib/mcp/browser/context.js +17 -1
- package/lib/mcp/browser/response.js +27 -20
- package/lib/mcp/browser/sessionLog.js +1 -1
- package/lib/mcp/browser/tab.js +6 -32
- package/lib/mcp/browser/tools/dialogs.js +6 -1
- package/lib/mcp/browser/tools/files.js +6 -1
- package/lib/mcp/browser/tools/pdf.js +1 -1
- package/lib/mcp/browser/tools/screenshot.js +1 -1
- package/lib/mcp/browser/tools/snapshot.js +1 -1
- package/lib/mcp/browser/tools/tracing.js +1 -1
- package/lib/mcp/browser/tools/utils.js +2 -2
- package/lib/mcp/browser/tools.js +5 -0
- package/lib/mcp/log.js +2 -2
- package/lib/mcp/program.js +1 -1
- package/lib/mcp/sdk/http.js +17 -5
- package/lib/mcp/sdk/server.js +1 -1
- package/lib/mcp/test/browserBackend.js +29 -33
- package/lib/program.js +6 -4
- package/lib/runner/lastRun.js +6 -16
- package/lib/runner/loadUtils.js +35 -2
- package/lib/runner/tasks.js +9 -1
- package/lib/runner/testRunner.js +2 -1
- package/lib/util.js +12 -6
- package/lib/worker/testInfo.js +1 -0
- package/lib/worker/workerMain.js +4 -1
- 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 =
|
|
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(
|
|
62
|
-
this._includeSnapshot =
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
107
|
+
response.push(renderTabSnapshot(this._tabSnapshot, options));
|
|
97
108
|
response.push("");
|
|
98
109
|
}
|
|
99
110
|
const content = [
|
|
@@ -101,7 +112,7 @@ ${this._code.join("\n")}
|
|
|
101
112
|
];
|
|
102
113
|
if (this._context.config.imageResponses !== "omit") {
|
|
103
114
|
for (const image of this._images)
|
|
104
|
-
content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
|
|
115
|
+
content.push({ type: "image", data: options.omitBlobs ? "<blob>" : image.data.toString("base64"), mimeType: image.contentType });
|
|
105
116
|
}
|
|
106
117
|
this._redactSecrets(content);
|
|
107
118
|
return { content, isError: this._isError };
|
|
@@ -117,7 +128,7 @@ ${this._code.join("\n")}
|
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
|
-
function renderTabSnapshot(tabSnapshot,
|
|
131
|
+
function renderTabSnapshot(tabSnapshot, options = {}) {
|
|
121
132
|
const lines = [];
|
|
122
133
|
if (tabSnapshot.consoleMessages.length) {
|
|
123
134
|
lines.push(`### New console messages`);
|
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|
package/lib/mcp/browser/tab.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
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(
|
|
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 = {
|
package/lib/mcp/browser/tools.js
CHANGED
|
@@ -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
|
|
26
|
+
const errorDebug = (0, import_utilsBundle.debug)("pw:mcp:error");
|
|
27
27
|
function logUnhandledError(error) {
|
|
28
|
-
|
|
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:
|
package/lib/mcp/program.js
CHANGED
|
@@ -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 "
|
|
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");
|
package/lib/mcp/sdk/http.js
CHANGED
|
@@ -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
|
|
75
|
-
if (
|
|
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 (
|
|
82
|
-
await handleSSE(serverBackendFactory, req, res,
|
|
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
|
});
|
package/lib/mcp/sdk/server.js
CHANGED
|
@@ -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
|
|
39
|
+
async function runBrowserBackendAtEnd(context, errorMessage) {
|
|
41
40
|
const testInfo = (0, import_globals.currentTestInfo)();
|
|
42
|
-
if (!testInfo
|
|
41
|
+
if (!testInfo)
|
|
43
42
|
return;
|
|
44
|
-
const
|
|
45
|
-
|
|
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
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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)),
|
|
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(["
|
|
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 === "
|
|
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.
|
|
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)` }],
|
package/lib/runner/lastRun.js
CHANGED
|
@@ -37,27 +37,17 @@ var import_projectUtils = require("./projectUtils");
|
|
|
37
37
|
class LastRunReporter {
|
|
38
38
|
constructor(config) {
|
|
39
39
|
this._config = config;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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
|
}
|