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
|
@@ -107,7 +107,7 @@ function saveAsClaudeCode(agent) {
|
|
|
107
107
|
}
|
|
108
108
|
const lines = [];
|
|
109
109
|
lines.push(`---`);
|
|
110
|
-
lines.push(`name:
|
|
110
|
+
lines.push(`name: playwright-test-${agent.header.name}`);
|
|
111
111
|
lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}`);
|
|
112
112
|
lines.push(`tools: ${agent.header.tools.map((tool) => asClaudeTool(tool)).join(", ")}`);
|
|
113
113
|
lines.push(`model: ${agent.header.model}`);
|
|
@@ -143,10 +143,10 @@ function saveAsOpencodeJson(agents) {
|
|
|
143
143
|
result["agent"] = {};
|
|
144
144
|
for (const agent of agents) {
|
|
145
145
|
const tools = {};
|
|
146
|
-
result["agent"][agent.header.name] = {
|
|
146
|
+
result["agent"]["playwright-test-" + agent.header.name] = {
|
|
147
147
|
description: agent.header.description,
|
|
148
148
|
mode: "subagent",
|
|
149
|
-
prompt: `{file:.opencode/prompts
|
|
149
|
+
prompt: `{file:.opencode/prompts/playwright-test-${agent.header.name}.md}`,
|
|
150
150
|
tools
|
|
151
151
|
};
|
|
152
152
|
for (const tool of agent.header.tools)
|
|
@@ -172,7 +172,7 @@ async function initClaudeCodeRepo() {
|
|
|
172
172
|
const agents = await loadAgents();
|
|
173
173
|
await import_fs.default.promises.mkdir(".claude/agents", { recursive: true });
|
|
174
174
|
for (const agent of agents)
|
|
175
|
-
await writeFile(`.claude/agents
|
|
175
|
+
await writeFile(`.claude/agents/playwright-test-${agent.header.name}.md`, saveAsClaudeCode(agent));
|
|
176
176
|
await writeFile(".mcp.json", JSON.stringify({
|
|
177
177
|
mcpServers: {
|
|
178
178
|
"playwright-test": {
|
|
@@ -189,28 +189,42 @@ 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() {
|
|
210
224
|
const agents = await loadAgents();
|
|
211
225
|
await import_fs.default.promises.mkdir(".github/chatmodes", { recursive: true });
|
|
212
226
|
for (const agent of agents)
|
|
213
|
-
await writeFile(`.github/chatmodes/${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent));
|
|
227
|
+
await writeFile(`.github/chatmodes/${agent.header.name === "planner" ? " " : ""}\u{1F3AD} ${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent));
|
|
214
228
|
await import_fs.default.promises.mkdir(".vscode", { recursive: true });
|
|
215
229
|
const mcpJsonPath = ".vscode/mcp.json";
|
|
216
230
|
let mcpJson = {
|
|
@@ -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
|
}
|
|
@@ -237,7 +252,7 @@ async function initOpencodeRepo() {
|
|
|
237
252
|
const prompt = [agent.instructions];
|
|
238
253
|
prompt.push("");
|
|
239
254
|
prompt.push(...agent.examples.map((example) => `<example>${example}</example>`));
|
|
240
|
-
await writeFile(`.opencode/prompts
|
|
255
|
+
await writeFile(`.opencode/prompts/playwright-test-${agent.header.name}.md`, prompt.join("\n"));
|
|
241
256
|
}
|
|
242
257
|
await writeFile("opencode.json", saveAsOpencodeJson(agents));
|
|
243
258
|
}
|
package/lib/agents/generator.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: generator
|
|
3
3
|
description: Use this agent when you need to create automated browser tests using Playwright
|
|
4
4
|
model: sonnet
|
|
5
5
|
color: blue
|
|
@@ -104,9 +104,9 @@ Your process is methodical and thorough:
|
|
|
104
104
|
Context: User wants to test a login flow on their web application.
|
|
105
105
|
user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then
|
|
106
106
|
verifies the dashboard page loads'
|
|
107
|
-
assistant: 'I'll use the
|
|
107
|
+
assistant: 'I'll use the generator agent to create and validate this login test for you'
|
|
108
108
|
<commentary>
|
|
109
|
-
The user needs a specific browser automation test created, which is exactly what the
|
|
109
|
+
The user needs a specific browser automation test created, which is exactly what the generator agent
|
|
110
110
|
is designed for.
|
|
111
111
|
</commentary>
|
|
112
112
|
</example>
|
|
@@ -114,9 +114,9 @@ Your process is methodical and thorough:
|
|
|
114
114
|
Context: User has built a new checkout flow and wants to ensure it works correctly.
|
|
115
115
|
user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the
|
|
116
116
|
order?'
|
|
117
|
-
assistant: 'I'll use the
|
|
117
|
+
assistant: 'I'll use the generator agent to build a comprehensive checkout flow test'
|
|
118
118
|
<commentary>
|
|
119
|
-
This is a complex user journey that needs to be automated and tested, perfect for the
|
|
119
|
+
This is a complex user journey that needs to be automated and tested, perfect for the generator
|
|
120
120
|
agent.
|
|
121
121
|
</commentary>
|
|
122
122
|
</example>
|
package/lib/agents/healer.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: healer
|
|
3
3
|
description: Use this agent when you need to debug and fix failing Playwright tests
|
|
4
4
|
color: red
|
|
5
5
|
model: sonnet
|
|
@@ -58,17 +58,17 @@ Key principles:
|
|
|
58
58
|
<example>
|
|
59
59
|
Context: A developer has a failing Playwright test that needs to be debugged and fixed.
|
|
60
60
|
user: 'The login test is failing, can you fix it?'
|
|
61
|
-
assistant: 'I'll use the
|
|
61
|
+
assistant: 'I'll use the healer agent to debug and fix the failing login test.'
|
|
62
62
|
<commentary>
|
|
63
63
|
The user has identified a specific failing test that needs debugging and fixing, which is exactly what the
|
|
64
|
-
|
|
64
|
+
healer agent is designed for.
|
|
65
65
|
</commentary>
|
|
66
66
|
</example>
|
|
67
67
|
|
|
68
68
|
<example>
|
|
69
69
|
Context: After running a test suite, several tests are reported as failing.
|
|
70
70
|
user: 'Test user-registration.spec.ts is broken after the recent changes'
|
|
71
|
-
assistant: 'Let me use the
|
|
71
|
+
assistant: 'Let me use the healer agent to investigate and fix the user-registration test.'
|
|
72
72
|
<commentary>
|
|
73
73
|
A specific test file is failing and needs debugging, which requires the systematic approach of the
|
|
74
74
|
playwright-test-healer agent.
|
package/lib/agents/planner.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: planner
|
|
3
3
|
description: Use this agent when you need to create comprehensive test plan for a web application or website
|
|
4
4
|
model: sonnet
|
|
5
5
|
color: green
|
|
@@ -117,19 +117,19 @@ professional formatting suitable for sharing with development and QA teams.
|
|
|
117
117
|
<example>
|
|
118
118
|
Context: User wants to test a new e-commerce checkout flow.
|
|
119
119
|
user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout'
|
|
120
|
-
assistant: 'I'll use the
|
|
120
|
+
assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test
|
|
121
121
|
scenarios.'
|
|
122
122
|
<commentary>
|
|
123
|
-
The user needs test planning for a specific web page, so use the
|
|
123
|
+
The user needs test planning for a specific web page, so use the planner agent to explore and create
|
|
124
124
|
test scenarios.
|
|
125
125
|
</commentary>
|
|
126
126
|
</example>
|
|
127
127
|
<example>
|
|
128
128
|
Context: User has deployed a new feature and wants thorough testing coverage.
|
|
129
129
|
user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?'
|
|
130
|
-
assistant: 'I'll launch the
|
|
130
|
+
assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test
|
|
131
131
|
scenarios.'
|
|
132
132
|
<commentary>
|
|
133
|
-
This requires web exploration and test scenario creation, perfect for the
|
|
133
|
+
This requires web exploration and test scenario creation, perfect for the planner agent.
|
|
134
134
|
</commentary>
|
|
135
135
|
</example>
|
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() {
|