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.
Files changed (37) hide show
  1. package/lib/agents/generateAgents.js +25 -10
  2. package/lib/agents/generator.md +5 -5
  3. package/lib/agents/healer.md +4 -4
  4. package/lib/agents/planner.md +5 -5
  5. package/lib/index.js +3 -50
  6. package/lib/matchers/toBeTruthy.js +0 -2
  7. package/lib/matchers/toEqual.js +0 -2
  8. package/lib/matchers/toMatchText.js +0 -3
  9. package/lib/mcp/browser/browserContextFactory.js +20 -9
  10. package/lib/mcp/browser/browserServerBackend.js +2 -0
  11. package/lib/mcp/browser/config.js +55 -18
  12. package/lib/mcp/browser/context.js +17 -1
  13. package/lib/mcp/browser/response.js +27 -20
  14. package/lib/mcp/browser/sessionLog.js +1 -1
  15. package/lib/mcp/browser/tab.js +6 -32
  16. package/lib/mcp/browser/tools/dialogs.js +6 -1
  17. package/lib/mcp/browser/tools/files.js +6 -1
  18. package/lib/mcp/browser/tools/pdf.js +1 -1
  19. package/lib/mcp/browser/tools/screenshot.js +1 -1
  20. package/lib/mcp/browser/tools/snapshot.js +1 -1
  21. package/lib/mcp/browser/tools/tracing.js +1 -1
  22. package/lib/mcp/browser/tools/utils.js +2 -2
  23. package/lib/mcp/browser/tools.js +5 -0
  24. package/lib/mcp/log.js +2 -2
  25. package/lib/mcp/program.js +1 -1
  26. package/lib/mcp/sdk/http.js +17 -5
  27. package/lib/mcp/sdk/server.js +1 -1
  28. package/lib/mcp/test/browserBackend.js +29 -33
  29. package/lib/program.js +6 -4
  30. package/lib/runner/lastRun.js +6 -16
  31. package/lib/runner/loadUtils.js +35 -2
  32. package/lib/runner/tasks.js +9 -1
  33. package/lib/runner/testRunner.js +2 -1
  34. package/lib/util.js +12 -6
  35. package/lib/worker/testInfo.js +1 -0
  36. package/lib/worker/workerMain.js +4 -1
  37. 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: ${agent.header.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/${agent.header.name}.md}`,
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/${agent.header.name}.md`, saveAsClaudeCode(agent));
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().map((tool) => `'${tool}'`).join(", ");
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}. Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}`);
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/${agent.header.name}.md`, prompt.join("\n"));
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
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: playwright-test-generator
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 playwright-test-generator agent to create and validate this login test for you'
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 playwright-test-generator agent
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 playwright-test-generator agent to build a comprehensive checkout flow test'
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 playwright-test-generator
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>
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: playwright-test-healer
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 playwright-test-healer agent to debug and fix the failing login test.'
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
- playwright-test-healer agent is designed for.
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 playwright-test-healer agent to investigate and fix the user-registration test.'
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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: playwright-test-planner
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 playwright-test-planner agent to navigate to your checkout page and create comprehensive test
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 playwright-test-planner agent to explore and create
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 playwright-test-planner agent to explore your dashboard and develop detailed test
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 playwright-test-planner agent.
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._onDidFinishTestFunction = () => this.didFinishTestFunction();
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,
@@ -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 { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
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 (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
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 (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
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
- try {
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 = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
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
- async function outputFile(config, clientInfo, fileName, options) {
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
- const outputDir = config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output", String(clientInfo.timestamp));
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(outputDir, fileName);
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(outputDir, fileName);
242
- if (!resolvedFile.startsWith(import_path.default.resolve(outputDir) + import_path.default.sep))
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(outputDir, sanitizeForFilePath(fileName));
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
- await close();
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() {