playwright 1.56.0-alpha-2025-09-23 → 1.56.0-alpha-2025-09-24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agents/generateAgents.js +19 -4
- 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/worker/testInfo.js +1 -0
- package/lib/worker/workerMain.js +4 -1
- package/package.json +2 -2
|
@@ -189,21 +189,35 @@ const vscodeToolMap = /* @__PURE__ */ new Map([
|
|
|
189
189
|
["edit", ["editFiles"]],
|
|
190
190
|
["write", ["createFile", "createDirectory"]]
|
|
191
191
|
]);
|
|
192
|
+
const vscodeToolsOrder = ["createFile", "createDirectory", "editFiles", "fileSearch", "textSearch", "listDirectory", "readFile"];
|
|
193
|
+
const vscodeToolPrefix = "test_";
|
|
192
194
|
function saveAsVSCodeChatmode(agent) {
|
|
193
195
|
function asVscodeTool(tool) {
|
|
194
196
|
const [first, second] = tool.split("/");
|
|
195
197
|
if (second)
|
|
196
|
-
return second;
|
|
198
|
+
return second.startsWith("browser_") ? vscodeToolPrefix + second : second;
|
|
197
199
|
return vscodeToolMap.get(first) || first;
|
|
198
200
|
}
|
|
199
|
-
const tools = agent.header.tools.map(asVscodeTool).flat().
|
|
201
|
+
const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
|
|
202
|
+
const indexA = vscodeToolsOrder.indexOf(a);
|
|
203
|
+
const indexB = vscodeToolsOrder.indexOf(b);
|
|
204
|
+
if (indexA === -1 && indexB === -1)
|
|
205
|
+
return a.localeCompare(b);
|
|
206
|
+
if (indexA === -1)
|
|
207
|
+
return 1;
|
|
208
|
+
if (indexB === -1)
|
|
209
|
+
return -1;
|
|
210
|
+
return indexA - indexB;
|
|
211
|
+
}).map((tool) => `'${tool}'`).join(", ");
|
|
200
212
|
const lines = [];
|
|
201
213
|
lines.push(`---`);
|
|
202
|
-
lines.push(`description: ${agent.header.description}
|
|
214
|
+
lines.push(`description: ${agent.header.description}.`);
|
|
203
215
|
lines.push(`tools: [${tools}]`);
|
|
204
216
|
lines.push(`---`);
|
|
205
217
|
lines.push("");
|
|
206
218
|
lines.push(agent.instructions);
|
|
219
|
+
for (const example of agent.examples)
|
|
220
|
+
lines.push(`<example>${example}</example>`);
|
|
207
221
|
return lines.join("\n");
|
|
208
222
|
}
|
|
209
223
|
async function initVSCodeRepo() {
|
|
@@ -226,7 +240,8 @@ async function initVSCodeRepo() {
|
|
|
226
240
|
mcpJson.servers["playwright-test"] = {
|
|
227
241
|
type: "stdio",
|
|
228
242
|
command: commonMcpServers.playwrightTest.command,
|
|
229
|
-
args: commonMcpServers.playwrightTest.args
|
|
243
|
+
args: commonMcpServers.playwrightTest.args,
|
|
244
|
+
env: { "PLAYWRIGHT_MCP_TOOL_PREFIX": vscodeToolPrefix }
|
|
230
245
|
};
|
|
231
246
|
await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2));
|
|
232
247
|
}
|
package/lib/index.js
CHANGED
|
@@ -43,8 +43,6 @@ var import_utils = require("playwright-core/lib/utils");
|
|
|
43
43
|
var import_globals = require("./common/globals");
|
|
44
44
|
var import_testType = require("./common/testType");
|
|
45
45
|
var import_browserBackend = require("./mcp/test/browserBackend");
|
|
46
|
-
var import_babelBundle = require("./transform/babelBundle");
|
|
47
|
-
var import_util = require("./util");
|
|
48
46
|
var import_expect = require("./matchers/expect");
|
|
49
47
|
var import_configLoader = require("./common/configLoader");
|
|
50
48
|
var import_testType2 = require("./common/testType");
|
|
@@ -262,22 +260,6 @@ const playwrightFixtures = {
|
|
|
262
260
|
if (data.apiName === "tracing.group")
|
|
263
261
|
tracingGroupSteps.push(step);
|
|
264
262
|
},
|
|
265
|
-
onApiCallRecovery: (data, error, channelOwner, recoveryHandlers) => {
|
|
266
|
-
const testInfo2 = (0, import_globals.currentTestInfo)();
|
|
267
|
-
if (!testInfo2 || !testInfo2._pauseOnError())
|
|
268
|
-
return;
|
|
269
|
-
const step = data.userData;
|
|
270
|
-
if (!step)
|
|
271
|
-
return;
|
|
272
|
-
const page = channelToPage(channelOwner);
|
|
273
|
-
if (!page)
|
|
274
|
-
return;
|
|
275
|
-
recoveryHandlers.push(async () => {
|
|
276
|
-
await (0, import_browserBackend.runBrowserBackendOnError)(page, () => {
|
|
277
|
-
return (0, import_util.stripAnsiEscapes)(createErrorCodeframe(error.message, step.location));
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
},
|
|
281
263
|
onApiCallEnd: (data) => {
|
|
282
264
|
if (data.apiName === "tracing.group")
|
|
283
265
|
return;
|
|
@@ -398,12 +380,13 @@ const playwrightFixtures = {
|
|
|
398
380
|
attachConnectedHeaderIfNeeded(testInfo, browserImpl);
|
|
399
381
|
if (!_reuseContext) {
|
|
400
382
|
const { context: context2, close } = await _contextFactory();
|
|
383
|
+
testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context2, testInfo.errors[0]?.message));
|
|
401
384
|
await use(context2);
|
|
402
|
-
await (0, import_browserBackend.runBrowserBackendAtEnd)(context2);
|
|
403
385
|
await close();
|
|
404
386
|
return;
|
|
405
387
|
}
|
|
406
388
|
const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
|
|
389
|
+
testInfo._onDidFinishTestFunctions.unshift(() => (0, import_browserBackend.runBrowserBackendAtEnd)(context, testInfo.errors[0]?.message));
|
|
407
390
|
await use(context);
|
|
408
391
|
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
|
|
409
392
|
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
|
|
@@ -577,7 +560,7 @@ class ArtifactsRecorder {
|
|
|
577
560
|
}
|
|
578
561
|
async willStartTest(testInfo) {
|
|
579
562
|
this._testInfo = testInfo;
|
|
580
|
-
testInfo.
|
|
563
|
+
testInfo._onDidFinishTestFunctions.push(() => this.didFinishTestFunction());
|
|
581
564
|
this._screenshotRecorder.fixOrdinal();
|
|
582
565
|
await Promise.all(this._playwright._allContexts().map((context) => this.didCreateBrowserContext(context)));
|
|
583
566
|
const existingApiRequests = Array.from(this._playwright.request._contexts);
|
|
@@ -688,36 +671,6 @@ function tracing() {
|
|
|
688
671
|
return test.info()._tracing;
|
|
689
672
|
}
|
|
690
673
|
const test = _baseTest.extend(playwrightFixtures);
|
|
691
|
-
function channelToPage(channelOwner) {
|
|
692
|
-
if (channelOwner._type === "Page")
|
|
693
|
-
return channelOwner;
|
|
694
|
-
if (channelOwner._type === "Frame")
|
|
695
|
-
return channelOwner.page();
|
|
696
|
-
return void 0;
|
|
697
|
-
}
|
|
698
|
-
function createErrorCodeframe(message, location) {
|
|
699
|
-
let source;
|
|
700
|
-
try {
|
|
701
|
-
source = import_fs.default.readFileSync(location.file, "utf-8") + "\n//";
|
|
702
|
-
} catch (e) {
|
|
703
|
-
return "";
|
|
704
|
-
}
|
|
705
|
-
return (0, import_babelBundle.codeFrameColumns)(
|
|
706
|
-
source,
|
|
707
|
-
{
|
|
708
|
-
start: {
|
|
709
|
-
line: location.line,
|
|
710
|
-
column: location.column
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
highlightCode: true,
|
|
715
|
-
linesAbove: 5,
|
|
716
|
-
linesBelow: 5,
|
|
717
|
-
message: message.split("\n")[0] || void 0
|
|
718
|
-
}
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
674
|
// Annotate the CommonJS export names for ESM import in node:
|
|
722
675
|
0 && (module.exports = {
|
|
723
676
|
_baseTest,
|
|
@@ -23,7 +23,6 @@ __export(toBeTruthy_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(toBeTruthy_exports);
|
|
24
24
|
var import_util = require("../util");
|
|
25
25
|
var import_matcherHint = require("./matcherHint");
|
|
26
|
-
var import_browserBackend = require("../mcp/test/browserBackend");
|
|
27
26
|
async function toBeTruthy(matcherName, locator, receiverType, expected, arg, query, options = {}) {
|
|
28
27
|
(0, import_util.expectTypes)(locator, [receiverType], matcherName);
|
|
29
28
|
const timeout = options.timeout ?? this.timeout;
|
|
@@ -58,7 +57,6 @@ async function toBeTruthy(matcherName, locator, receiverType, expected, arg, que
|
|
|
58
57
|
log
|
|
59
58
|
});
|
|
60
59
|
};
|
|
61
|
-
await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
|
|
62
60
|
return {
|
|
63
61
|
message,
|
|
64
62
|
pass,
|
package/lib/matchers/toEqual.js
CHANGED
|
@@ -24,7 +24,6 @@ module.exports = __toCommonJS(toEqual_exports);
|
|
|
24
24
|
var import_utils = require("playwright-core/lib/utils");
|
|
25
25
|
var import_util = require("../util");
|
|
26
26
|
var import_matcherHint = require("./matcherHint");
|
|
27
|
-
var import_browserBackend = require("../mcp/test/browserBackend");
|
|
28
27
|
const EXPECTED_LABEL = "Expected";
|
|
29
28
|
const RECEIVED_LABEL = "Received";
|
|
30
29
|
async function toEqual(matcherName, locator, receiverType, query, expected, options = {}) {
|
|
@@ -84,7 +83,6 @@ async function toEqual(matcherName, locator, receiverType, query, expected, opti
|
|
|
84
83
|
log
|
|
85
84
|
});
|
|
86
85
|
};
|
|
87
|
-
await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
|
|
88
86
|
return {
|
|
89
87
|
actual: received,
|
|
90
88
|
expected,
|
|
@@ -25,7 +25,6 @@ var import_util = require("../util");
|
|
|
25
25
|
var import_expect = require("./expect");
|
|
26
26
|
var import_matcherHint = require("./matcherHint");
|
|
27
27
|
var import_expectBundle = require("../common/expectBundle");
|
|
28
|
-
var import_browserBackend = require("../mcp/test/browserBackend");
|
|
29
28
|
async function toMatchText(matcherName, receiver, receiverType, query, expected, options = {}) {
|
|
30
29
|
(0, import_util.expectTypes)(receiver, [receiverType], matcherName);
|
|
31
30
|
const locator = receiverType === "Locator" ? receiver : void 0;
|
|
@@ -84,8 +83,6 @@ ${this.utils.printWithType("Expected", expected, this.utils.printExpected)}`;
|
|
|
84
83
|
errorMessage
|
|
85
84
|
});
|
|
86
85
|
};
|
|
87
|
-
if (locator)
|
|
88
|
-
await (0, import_browserBackend.runBrowserBackendOnError)(locator.page(), message);
|
|
89
86
|
return {
|
|
90
87
|
name: matcherName,
|
|
91
88
|
expected,
|
|
@@ -80,16 +80,20 @@ class BaseContextFactory {
|
|
|
80
80
|
const browser = await this._obtainBrowser(clientInfo);
|
|
81
81
|
const browserContext = await this._doCreateContext(browser);
|
|
82
82
|
await addInitScript(browserContext, this.config.browser.initScript);
|
|
83
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
browserContext,
|
|
85
|
+
close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
|
|
86
|
+
};
|
|
84
87
|
}
|
|
85
88
|
async _doCreateContext(browser) {
|
|
86
89
|
throw new Error("Not implemented");
|
|
87
90
|
}
|
|
88
|
-
async _closeBrowserContext(browserContext, browser) {
|
|
91
|
+
async _closeBrowserContext(browserContext, browser, afterClose) {
|
|
89
92
|
(0, import_log.testDebug)(`close browser context (${this._logName})`);
|
|
90
93
|
if (browser.contexts().length === 1)
|
|
91
94
|
this._browserPromise = void 0;
|
|
92
95
|
await browserContext.close().catch(import_log.logUnhandledError);
|
|
96
|
+
await afterClose();
|
|
93
97
|
if (browser.contexts().length === 0) {
|
|
94
98
|
(0, import_log.testDebug)(`close browser (${this._logName})`);
|
|
95
99
|
await browser.close().catch(import_log.logUnhandledError);
|
|
@@ -103,8 +107,8 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
103
107
|
async _doObtainBrowser(clientInfo) {
|
|
104
108
|
await injectCdpPort(this.config.browser);
|
|
105
109
|
const browserType = playwright[this.config.browser.browserName];
|
|
106
|
-
const tracesDir = await (
|
|
107
|
-
if (this.config.saveTrace)
|
|
110
|
+
const tracesDir = await computeTracesDir(this.config, clientInfo);
|
|
111
|
+
if (tracesDir && this.config.saveTrace)
|
|
108
112
|
await startTraceServer(this.config, tracesDir);
|
|
109
113
|
return browserType.launch({
|
|
110
114
|
tracesDir,
|
|
@@ -158,8 +162,8 @@ class PersistentContextFactory {
|
|
|
158
162
|
await injectCdpPort(this.config.browser);
|
|
159
163
|
(0, import_log.testDebug)("create browser context (persistent)");
|
|
160
164
|
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
|
|
161
|
-
const tracesDir = await (
|
|
162
|
-
if (this.config.saveTrace)
|
|
165
|
+
const tracesDir = await computeTracesDir(this.config, clientInfo);
|
|
166
|
+
if (tracesDir && this.config.saveTrace)
|
|
163
167
|
await startTraceServer(this.config, tracesDir);
|
|
164
168
|
this._userDataDirs.add(userDataDir);
|
|
165
169
|
(0, import_log.testDebug)("lock user data dir", userDataDir);
|
|
@@ -179,7 +183,7 @@ class PersistentContextFactory {
|
|
|
179
183
|
try {
|
|
180
184
|
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
|
|
181
185
|
await addInitScript(browserContext, this.config.browser.initScript);
|
|
182
|
-
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
|
186
|
+
const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
|
|
183
187
|
return { browserContext, close };
|
|
184
188
|
} catch (error) {
|
|
185
189
|
if (error.message.includes("Executable doesn't exist"))
|
|
@@ -193,11 +197,12 @@ class PersistentContextFactory {
|
|
|
193
197
|
}
|
|
194
198
|
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
|
195
199
|
}
|
|
196
|
-
async _closeBrowserContext(browserContext, userDataDir) {
|
|
200
|
+
async _closeBrowserContext(browserContext, userDataDir, afterClose) {
|
|
197
201
|
(0, import_log.testDebug)("close browser context (persistent)");
|
|
198
202
|
(0, import_log.testDebug)("release user data dir", userDataDir);
|
|
199
203
|
await browserContext.close().catch(() => {
|
|
200
204
|
});
|
|
205
|
+
await afterClose();
|
|
201
206
|
this._userDataDirs.delete(userDataDir);
|
|
202
207
|
(0, import_log.testDebug)("close browser context complete (persistent)");
|
|
203
208
|
}
|
|
@@ -275,9 +280,15 @@ class SharedContextFactory {
|
|
|
275
280
|
if (!contextPromise)
|
|
276
281
|
return;
|
|
277
282
|
const { close } = await contextPromise;
|
|
278
|
-
await close()
|
|
283
|
+
await close(async () => {
|
|
284
|
+
});
|
|
279
285
|
}
|
|
280
286
|
}
|
|
287
|
+
async function computeTracesDir(config, clientInfo) {
|
|
288
|
+
if (!config.saveTrace && !config.capabilities?.includes("tracing"))
|
|
289
|
+
return;
|
|
290
|
+
return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
|
|
291
|
+
}
|
|
281
292
|
// Annotate the CommonJS export names for ESM import in node:
|
|
282
293
|
0 && (module.exports = {
|
|
283
294
|
SharedContextFactory,
|
|
@@ -52,6 +52,7 @@ class BrowserServerBackend {
|
|
|
52
52
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
|
53
53
|
const context = this._context;
|
|
54
54
|
const response = new import_response.Response(context, name, parsedArguments);
|
|
55
|
+
response.logBegin();
|
|
55
56
|
context.setRunningTool(name);
|
|
56
57
|
try {
|
|
57
58
|
await tool.handle(context, parsedArguments, response);
|
|
@@ -62,6 +63,7 @@ class BrowserServerBackend {
|
|
|
62
63
|
} finally {
|
|
63
64
|
context.setRunningTool(void 0);
|
|
64
65
|
}
|
|
66
|
+
response.logEnd();
|
|
65
67
|
return response.serialize();
|
|
66
68
|
}
|
|
67
69
|
serverClosed() {
|
|
@@ -34,7 +34,9 @@ __export(config_exports, {
|
|
|
34
34
|
dotenvFileLoader: () => dotenvFileLoader,
|
|
35
35
|
headerParser: () => headerParser,
|
|
36
36
|
numberParser: () => numberParser,
|
|
37
|
+
outputDir: () => outputDir,
|
|
37
38
|
outputFile: () => outputFile,
|
|
39
|
+
resolutionParser: () => resolutionParser,
|
|
38
40
|
resolveCLIConfig: () => resolveCLIConfig,
|
|
39
41
|
resolveConfig: () => resolveConfig,
|
|
40
42
|
semicolonSeparatedList: () => semicolonSeparatedList
|
|
@@ -91,6 +93,8 @@ async function validateConfig(config) {
|
|
|
91
93
|
throw new Error(`Init script file does not exist: ${script}`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
96
|
+
if (config.sharedBrowserContext && config.saveVideo)
|
|
97
|
+
throw new Error("saveVideo is not supported when sharedBrowserContext is true");
|
|
94
98
|
}
|
|
95
99
|
function configFromCLIOptions(cliOptions) {
|
|
96
100
|
let browserName;
|
|
@@ -136,22 +140,21 @@ function configFromCLIOptions(cliOptions) {
|
|
|
136
140
|
contextOptions.storageState = cliOptions.storageState;
|
|
137
141
|
if (cliOptions.userAgent)
|
|
138
142
|
contextOptions.userAgent = cliOptions.userAgent;
|
|
139
|
-
if (cliOptions.viewportSize)
|
|
140
|
-
|
|
141
|
-
const [width, height] = cliOptions.viewportSize.split(",").map((n) => +n);
|
|
142
|
-
if (isNaN(width) || isNaN(height))
|
|
143
|
-
throw new Error("bad values");
|
|
144
|
-
contextOptions.viewport = { width, height };
|
|
145
|
-
} catch (e) {
|
|
146
|
-
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
143
|
+
if (cliOptions.viewportSize)
|
|
144
|
+
contextOptions.viewport = cliOptions.viewportSize;
|
|
149
145
|
if (cliOptions.ignoreHttpsErrors)
|
|
150
146
|
contextOptions.ignoreHTTPSErrors = true;
|
|
151
147
|
if (cliOptions.blockServiceWorkers)
|
|
152
148
|
contextOptions.serviceWorkers = "block";
|
|
153
149
|
if (cliOptions.grantPermissions)
|
|
154
150
|
contextOptions.permissions = cliOptions.grantPermissions;
|
|
151
|
+
if (cliOptions.saveVideo) {
|
|
152
|
+
contextOptions.recordVideo = {
|
|
153
|
+
// Videos are moved to output directory on saveAs.
|
|
154
|
+
dir: tmpDir(),
|
|
155
|
+
size: cliOptions.saveVideo
|
|
156
|
+
};
|
|
157
|
+
}
|
|
155
158
|
const result = {
|
|
156
159
|
browser: {
|
|
157
160
|
browserName,
|
|
@@ -165,7 +168,8 @@ function configFromCLIOptions(cliOptions) {
|
|
|
165
168
|
},
|
|
166
169
|
server: {
|
|
167
170
|
port: cliOptions.port,
|
|
168
|
-
host: cliOptions.host
|
|
171
|
+
host: cliOptions.host,
|
|
172
|
+
allowedHosts: cliOptions.allowedHosts
|
|
169
173
|
},
|
|
170
174
|
capabilities: cliOptions.caps,
|
|
171
175
|
network: {
|
|
@@ -174,6 +178,7 @@ function configFromCLIOptions(cliOptions) {
|
|
|
174
178
|
},
|
|
175
179
|
saveSession: cliOptions.saveSession,
|
|
176
180
|
saveTrace: cliOptions.saveTrace,
|
|
181
|
+
saveVideo: cliOptions.saveVideo,
|
|
177
182
|
secrets: cliOptions.secrets,
|
|
178
183
|
sharedBrowserContext: cliOptions.sharedBrowserContext,
|
|
179
184
|
outputDir: cliOptions.outputDir,
|
|
@@ -187,6 +192,7 @@ function configFromCLIOptions(cliOptions) {
|
|
|
187
192
|
}
|
|
188
193
|
function configFromEnv() {
|
|
189
194
|
const options = {};
|
|
195
|
+
options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
|
|
190
196
|
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
|
191
197
|
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
|
192
198
|
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
|
@@ -213,13 +219,14 @@ function configFromEnv() {
|
|
|
213
219
|
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
|
214
220
|
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
|
|
215
221
|
options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
|
|
222
|
+
options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
|
|
216
223
|
options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
|
|
217
224
|
options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
|
|
218
225
|
options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
|
|
219
226
|
options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
|
|
220
227
|
options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
|
|
221
228
|
options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
|
|
222
|
-
options.viewportSize =
|
|
229
|
+
options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
|
|
223
230
|
return configFromCLIOptions(options);
|
|
224
231
|
}
|
|
225
232
|
async function loadConfig(configFile) {
|
|
@@ -231,19 +238,30 @@ async function loadConfig(configFile) {
|
|
|
231
238
|
throw new Error(`Failed to load config file: ${configFile}, ${error}`);
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
|
-
|
|
241
|
+
function tmpDir() {
|
|
242
|
+
return import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output");
|
|
243
|
+
}
|
|
244
|
+
function outputDir(config, clientInfo) {
|
|
235
245
|
const rootPath = (0, import_server.firstRootPath)(clientInfo);
|
|
236
|
-
|
|
246
|
+
return config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(tmpDir(), String(clientInfo.timestamp));
|
|
247
|
+
}
|
|
248
|
+
async function outputFile(config, clientInfo, fileName, options) {
|
|
249
|
+
const file = await resolveFile(config, clientInfo, fileName, options);
|
|
250
|
+
(0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
|
|
251
|
+
return file;
|
|
252
|
+
}
|
|
253
|
+
async function resolveFile(config, clientInfo, fileName, options) {
|
|
254
|
+
const dir = outputDir(config, clientInfo);
|
|
237
255
|
if (options.origin === "code")
|
|
238
|
-
return import_path.default.resolve(
|
|
256
|
+
return import_path.default.resolve(dir, fileName);
|
|
239
257
|
if (options.origin === "llm") {
|
|
240
258
|
fileName = fileName.split("\\").join("/");
|
|
241
|
-
const resolvedFile = import_path.default.resolve(
|
|
242
|
-
if (!resolvedFile.startsWith(import_path.default.resolve(
|
|
259
|
+
const resolvedFile = import_path.default.resolve(dir, fileName);
|
|
260
|
+
if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
|
|
243
261
|
throw new Error(`Resolved file path for ${fileName} is outside of the output directory`);
|
|
244
262
|
return resolvedFile;
|
|
245
263
|
}
|
|
246
|
-
return import_path.default.join(
|
|
264
|
+
return import_path.default.join(dir, sanitizeForFilePath(fileName));
|
|
247
265
|
}
|
|
248
266
|
function pickDefined(obj) {
|
|
249
267
|
return Object.fromEntries(
|
|
@@ -306,6 +324,23 @@ function numberParser(value) {
|
|
|
306
324
|
return void 0;
|
|
307
325
|
return +value;
|
|
308
326
|
}
|
|
327
|
+
function resolutionParser(name, value) {
|
|
328
|
+
if (!value)
|
|
329
|
+
return void 0;
|
|
330
|
+
if (value.includes("x")) {
|
|
331
|
+
const [width, height] = value.split("x").map((v) => +v);
|
|
332
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
|
333
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
334
|
+
return { width, height };
|
|
335
|
+
}
|
|
336
|
+
if (value.includes(",")) {
|
|
337
|
+
const [width, height] = value.split(",").map((v) => +v);
|
|
338
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
|
339
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
340
|
+
return { width, height };
|
|
341
|
+
}
|
|
342
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
343
|
+
}
|
|
309
344
|
function headerParser(arg, previous) {
|
|
310
345
|
if (!arg)
|
|
311
346
|
return previous || {};
|
|
@@ -339,7 +374,9 @@ function sanitizeForFilePath(s) {
|
|
|
339
374
|
dotenvFileLoader,
|
|
340
375
|
headerParser,
|
|
341
376
|
numberParser,
|
|
377
|
+
outputDir,
|
|
342
378
|
outputFile,
|
|
379
|
+
resolutionParser,
|
|
343
380
|
resolveCLIConfig,
|
|
344
381
|
resolveConfig,
|
|
345
382
|
semicolonSeparatedList
|
|
@@ -32,11 +32,14 @@ __export(context_exports, {
|
|
|
32
32
|
InputRecorder: () => InputRecorder
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(context_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
36
|
+
var import_path = __toESM(require("path"));
|
|
35
37
|
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
|
|
36
38
|
var import_log = require("../log");
|
|
37
39
|
var import_tab = require("./tab");
|
|
38
40
|
var import_config = require("./config");
|
|
39
41
|
var codegen = __toESM(require("./codegen"));
|
|
42
|
+
var import_utils = require("./tools/utils");
|
|
40
43
|
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
|
|
41
44
|
class Context {
|
|
42
45
|
constructor(options) {
|
|
@@ -135,7 +138,20 @@ class Context {
|
|
|
135
138
|
await promise.then(async ({ browserContext, close }) => {
|
|
136
139
|
if (this.config.saveTrace)
|
|
137
140
|
await browserContext.tracing.stop();
|
|
138
|
-
|
|
141
|
+
const videos = browserContext.pages().map((page) => page.video()).filter((video) => !!video);
|
|
142
|
+
await close(async () => {
|
|
143
|
+
for (const video of videos) {
|
|
144
|
+
const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
|
|
145
|
+
await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
|
|
146
|
+
const p = await video.path();
|
|
147
|
+
try {
|
|
148
|
+
if (import_fs.default.existsSync(p))
|
|
149
|
+
await import_fs.default.promises.rename(p, name);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
(0, import_log.logUnhandledError)(e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
139
155
|
});
|
|
140
156
|
}
|
|
141
157
|
async dispose() {
|
|
@@ -18,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/worker/testInfo.js
CHANGED
package/lib/worker/workerMain.js
CHANGED
|
@@ -318,7 +318,10 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
318
318
|
await testInfo._runAsStep({ title: "After Hooks", category: "hook" }, async () => {
|
|
319
319
|
let firstAfterHooksError;
|
|
320
320
|
try {
|
|
321
|
-
await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, async () =>
|
|
321
|
+
await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, async () => {
|
|
322
|
+
for (const fn of testInfo._onDidFinishTestFunctions)
|
|
323
|
+
await fn();
|
|
324
|
+
});
|
|
322
325
|
} catch (error) {
|
|
323
326
|
firstAfterHooksError = firstAfterHooksError ?? error;
|
|
324
327
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.56.0-alpha-2025-09-
|
|
3
|
+
"version": "1.56.0-alpha-2025-09-24",
|
|
4
4
|
"description": "A high-level API to automate web browsers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"license": "Apache-2.0",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"playwright-core": "1.56.0-alpha-2025-09-
|
|
67
|
+
"playwright-core": "1.56.0-alpha-2025-09-24"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|