@cotestdev/mcp_playwright 0.0.14 → 0.0.16

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 (49) hide show
  1. package/lib/mcp/browser/browserContextFactory.js +49 -13
  2. package/lib/mcp/browser/browserServerBackend.js +5 -2
  3. package/lib/mcp/browser/config.js +95 -23
  4. package/lib/mcp/browser/context.js +28 -3
  5. package/lib/mcp/browser/response.js +240 -57
  6. package/lib/mcp/browser/sessionLog.js +1 -1
  7. package/lib/mcp/browser/tab.js +96 -69
  8. package/lib/mcp/browser/tools/common.js +8 -8
  9. package/lib/mcp/browser/tools/console.js +6 -3
  10. package/lib/mcp/browser/tools/dialogs.js +13 -13
  11. package/lib/mcp/browser/tools/evaluate.js +9 -20
  12. package/lib/mcp/browser/tools/files.js +10 -5
  13. package/lib/mcp/browser/tools/form.js +11 -22
  14. package/lib/mcp/browser/tools/install.js +3 -3
  15. package/lib/mcp/browser/tools/keyboard.js +12 -12
  16. package/lib/mcp/browser/tools/mouse.js +14 -14
  17. package/lib/mcp/browser/tools/navigate.js +5 -5
  18. package/lib/mcp/browser/tools/network.js +16 -5
  19. package/lib/mcp/browser/tools/pdf.js +7 -18
  20. package/lib/mcp/browser/tools/runCode.js +77 -0
  21. package/lib/mcp/browser/tools/screenshot.js +44 -33
  22. package/lib/mcp/browser/tools/snapshot.js +42 -33
  23. package/lib/mcp/browser/tools/tabs.js +7 -10
  24. package/lib/mcp/browser/tools/tool.js +8 -7
  25. package/lib/mcp/browser/tools/tracing.js +4 -4
  26. package/lib/mcp/browser/tools/utils.js +50 -52
  27. package/lib/mcp/browser/tools/verify.js +23 -34
  28. package/lib/mcp/browser/tools/wait.js +6 -6
  29. package/lib/mcp/browser/tools.js +4 -3
  30. package/lib/mcp/extension/cdpRelay.js +1 -1
  31. package/lib/mcp/extension/extensionContextFactory.js +4 -3
  32. package/lib/mcp/log.js +2 -2
  33. package/lib/mcp/program.js +21 -29
  34. package/lib/mcp/sdk/exports.js +1 -5
  35. package/lib/mcp/sdk/http.js +37 -50
  36. package/lib/mcp/sdk/server.js +61 -9
  37. package/lib/mcp/sdk/tool.js +5 -4
  38. package/lib/mcp/test/browserBackend.js +67 -61
  39. package/lib/mcp/test/generatorTools.js +122 -0
  40. package/lib/mcp/test/plannerTools.js +144 -0
  41. package/lib/mcp/test/seed.js +82 -0
  42. package/lib/mcp/test/streams.js +10 -7
  43. package/lib/mcp/test/testBackend.js +44 -24
  44. package/lib/mcp/test/testContext.js +243 -14
  45. package/lib/mcp/test/testTools.js +23 -109
  46. package/lib/mcpBundle.js +84 -0
  47. package/lib/mcpBundleImpl/index.js +130 -0
  48. package/lib/util.js +12 -6
  49. package/package.json +1 -1
@@ -29,7 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var browserContextFactory_exports = {};
30
30
  __export(browserContextFactory_exports, {
31
31
  SharedContextFactory: () => SharedContextFactory,
32
- contextFactory: () => contextFactory
32
+ contextFactory: () => contextFactory,
33
+ identityBrowserContextFactory: () => identityBrowserContextFactory
33
34
  });
34
35
  module.exports = __toCommonJS(browserContextFactory_exports);
35
36
  var import_crypto = __toESM(require("crypto"));
@@ -53,6 +54,17 @@ function contextFactory(config) {
53
54
  return new IsolatedContextFactory(config);
54
55
  return new PersistentContextFactory(config);
55
56
  }
57
+ function identityBrowserContextFactory(browserContext) {
58
+ return {
59
+ createContext: async (clientInfo, abortSignal, toolName) => {
60
+ return {
61
+ browserContext,
62
+ close: async () => {
63
+ }
64
+ };
65
+ }
66
+ };
67
+ }
56
68
  class BaseContextFactory {
57
69
  constructor(name, config) {
58
70
  this._logName = name;
@@ -80,16 +92,20 @@ class BaseContextFactory {
80
92
  const browser = await this._obtainBrowser(clientInfo);
81
93
  const browserContext = await this._doCreateContext(browser);
82
94
  await addInitScript(browserContext, this.config.browser.initScript);
83
- return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
95
+ return {
96
+ browserContext,
97
+ close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
98
+ };
84
99
  }
85
100
  async _doCreateContext(browser) {
86
101
  throw new Error("Not implemented");
87
102
  }
88
- async _closeBrowserContext(browserContext, browser) {
103
+ async _closeBrowserContext(browserContext, browser, afterClose) {
89
104
  (0, import_log.testDebug)(`close browser context (${this._logName})`);
90
105
  if (browser.contexts().length === 1)
91
106
  this._browserPromise = void 0;
92
107
  await browserContext.close().catch(import_log.logUnhandledError);
108
+ await afterClose();
93
109
  if (browser.contexts().length === 0) {
94
110
  (0, import_log.testDebug)(`close browser (${this._logName})`);
95
111
  await browser.close().catch(import_log.logUnhandledError);
@@ -103,8 +119,8 @@ class IsolatedContextFactory extends BaseContextFactory {
103
119
  async _doObtainBrowser(clientInfo) {
104
120
  await injectCdpPort(this.config.browser);
105
121
  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)
122
+ const tracesDir = await computeTracesDir(this.config, clientInfo);
123
+ if (tracesDir && this.config.saveTrace)
108
124
  await startTraceServer(this.config, tracesDir);
109
125
  return browserType.launch({
110
126
  tracesDir,
@@ -118,7 +134,7 @@ class IsolatedContextFactory extends BaseContextFactory {
118
134
  });
119
135
  }
120
136
  async _doCreateContext(browser) {
121
- return browser.newContext(this.config.browser.contextOptions);
137
+ return browser.newContext(browserContextOptionsFromConfig(this.config));
122
138
  }
123
139
  }
124
140
  class CdpContextFactory extends BaseContextFactory {
@@ -158,8 +174,8 @@ class PersistentContextFactory {
158
174
  await injectCdpPort(this.config.browser);
159
175
  (0, import_log.testDebug)("create browser context (persistent)");
160
176
  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)
177
+ const tracesDir = await computeTracesDir(this.config, clientInfo);
178
+ if (tracesDir && this.config.saveTrace)
163
179
  await startTraceServer(this.config, tracesDir);
164
180
  this._userDataDirs.add(userDataDir);
165
181
  (0, import_log.testDebug)("lock user data dir", userDataDir);
@@ -168,7 +184,7 @@ class PersistentContextFactory {
168
184
  const launchOptions = {
169
185
  tracesDir,
170
186
  ...this.config.browser.launchOptions,
171
- ...this.config.browser.contextOptions,
187
+ ...browserContextOptionsFromConfig(this.config),
172
188
  handleSIGINT: false,
173
189
  handleSIGTERM: false,
174
190
  ignoreDefaultArgs: [
@@ -179,7 +195,7 @@ class PersistentContextFactory {
179
195
  try {
180
196
  const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
181
197
  await addInitScript(browserContext, this.config.browser.initScript);
182
- const close = () => this._closeBrowserContext(browserContext, userDataDir);
198
+ const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
183
199
  return { browserContext, close };
184
200
  } catch (error) {
185
201
  if (error.message.includes("Executable doesn't exist"))
@@ -193,12 +209,15 @@ class PersistentContextFactory {
193
209
  }
194
210
  throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
195
211
  }
196
- async _closeBrowserContext(browserContext, userDataDir) {
212
+ async _closeBrowserContext(browserContext, userDataDir, afterClose) {
197
213
  (0, import_log.testDebug)("close browser context (persistent)");
198
214
  (0, import_log.testDebug)("release user data dir", userDataDir);
199
215
  await browserContext.close().catch(() => {
200
216
  });
217
+ await afterClose();
201
218
  this._userDataDirs.delete(userDataDir);
219
+ if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
220
+ await import_fs.default.promises.rm(userDataDir, { recursive: true }).catch(import_log.logUnhandledError);
202
221
  (0, import_log.testDebug)("close browser context complete (persistent)");
203
222
  }
204
223
  async _createUserDataDir(clientInfo) {
@@ -275,11 +294,28 @@ class SharedContextFactory {
275
294
  if (!contextPromise)
276
295
  return;
277
296
  const { close } = await contextPromise;
278
- await close();
297
+ await close(async () => {
298
+ });
299
+ }
300
+ }
301
+ async function computeTracesDir(config, clientInfo) {
302
+ if (!config.saveTrace && !config.capabilities?.includes("tracing"))
303
+ return;
304
+ return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
305
+ }
306
+ function browserContextOptionsFromConfig(config) {
307
+ const result = { ...config.browser.contextOptions };
308
+ if (config.saveVideo) {
309
+ result.recordVideo = {
310
+ dir: (0, import_config.tmpDir)(),
311
+ size: config.saveVideo
312
+ };
279
313
  }
314
+ return result;
280
315
  }
281
316
  // Annotate the CommonJS export names for ESM import in node:
282
317
  0 && (module.exports = {
283
318
  SharedContextFactory,
284
- contextFactory
319
+ contextFactory,
320
+ identityBrowserContextFactory
285
321
  });
@@ -33,7 +33,7 @@ class BrowserServerBackend {
33
33
  this._browserContextFactory = factory;
34
34
  this._tools = (0, import_tools.filteredTools)(config);
35
35
  }
36
- async initialize(server, clientInfo) {
36
+ async initialize(clientInfo) {
37
37
  this._sessionLog = this._config.saveSession ? await import_sessionLog.SessionLog.create(this._config, clientInfo) : void 0;
38
38
  this._context = new import_context.Context({
39
39
  config: this._config,
@@ -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,7 +63,9 @@ class BrowserServerBackend {
62
63
  } finally {
63
64
  context.setRunningTool(void 0);
64
65
  }
65
- return response.serialize();
66
+ response.logEnd();
67
+ const _meta = rawArguments?._meta;
68
+ return response.serialize({ _meta });
66
69
  }
67
70
  serverClosed() {
68
71
  void this._context?.dispose().catch(import_log.logUnhandledError);
@@ -32,12 +32,16 @@ __export(config_exports, {
32
32
  configFromCLIOptions: () => configFromCLIOptions,
33
33
  defaultConfig: () => defaultConfig,
34
34
  dotenvFileLoader: () => dotenvFileLoader,
35
+ enumParser: () => enumParser,
35
36
  headerParser: () => headerParser,
36
37
  numberParser: () => numberParser,
38
+ outputDir: () => outputDir,
37
39
  outputFile: () => outputFile,
40
+ resolutionParser: () => resolutionParser,
38
41
  resolveCLIConfig: () => resolveCLIConfig,
39
42
  resolveConfig: () => resolveConfig,
40
- semicolonSeparatedList: () => semicolonSeparatedList
43
+ semicolonSeparatedList: () => semicolonSeparatedList,
44
+ tmpDir: () => tmpDir
41
45
  });
42
46
  module.exports = __toCommonJS(config_exports);
43
47
  var import_fs = __toESM(require("fs"));
@@ -59,12 +63,18 @@ const defaultConfig = {
59
63
  viewport: null
60
64
  }
61
65
  },
66
+ console: {
67
+ level: "info"
68
+ },
62
69
  network: {
63
70
  allowedOrigins: void 0,
64
71
  blockedOrigins: void 0
65
72
  },
66
73
  server: {},
67
74
  saveTrace: false,
75
+ snapshot: {
76
+ mode: "incremental"
77
+ },
68
78
  timeouts: {
69
79
  action: 5e3,
70
80
  navigation: 6e4
@@ -91,6 +101,14 @@ async function validateConfig(config) {
91
101
  throw new Error(`Init script file does not exist: ${script}`);
92
102
  }
93
103
  }
104
+ if (config.browser.initPage) {
105
+ for (const page of config.browser.initPage) {
106
+ if (!await (0, import_util.fileExistsAsync)(page))
107
+ throw new Error(`Init page file does not exist: ${page}`);
108
+ }
109
+ }
110
+ if (config.sharedBrowserContext && config.saveVideo)
111
+ throw new Error("saveVideo is not supported when sharedBrowserContext is true");
94
112
  }
95
113
  function configFromCLIOptions(cliOptions) {
96
114
  let browserName;
@@ -136,16 +154,8 @@ function configFromCLIOptions(cliOptions) {
136
154
  contextOptions.storageState = cliOptions.storageState;
137
155
  if (cliOptions.userAgent)
138
156
  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
- }
157
+ if (cliOptions.viewportSize)
158
+ contextOptions.viewport = cliOptions.viewportSize;
149
159
  if (cliOptions.ignoreHttpsErrors)
150
160
  contextOptions.ignoreHTTPSErrors = true;
151
161
  if (cliOptions.blockServiceWorkers)
@@ -161,23 +171,31 @@ function configFromCLIOptions(cliOptions) {
161
171
  contextOptions,
162
172
  cdpEndpoint: cliOptions.cdpEndpoint,
163
173
  cdpHeaders: cliOptions.cdpHeader,
174
+ initPage: cliOptions.initPage,
164
175
  initScript: cliOptions.initScript
165
176
  },
166
177
  server: {
167
178
  port: cliOptions.port,
168
- host: cliOptions.host
179
+ host: cliOptions.host,
180
+ allowedHosts: cliOptions.allowedHosts
169
181
  },
170
182
  capabilities: cliOptions.caps,
183
+ console: {
184
+ level: cliOptions.consoleLevel
185
+ },
171
186
  network: {
172
187
  allowedOrigins: cliOptions.allowedOrigins,
173
188
  blockedOrigins: cliOptions.blockedOrigins
174
189
  },
175
190
  saveSession: cliOptions.saveSession,
176
191
  saveTrace: cliOptions.saveTrace,
192
+ saveVideo: cliOptions.saveVideo,
177
193
  secrets: cliOptions.secrets,
178
194
  sharedBrowserContext: cliOptions.sharedBrowserContext,
195
+ snapshot: cliOptions.snapshotMode ? { mode: cliOptions.snapshotMode } : void 0,
179
196
  outputDir: cliOptions.outputDir,
180
197
  imageResponses: cliOptions.imageResponses,
198
+ testIdAttribute: cliOptions.testIdAttribute,
181
199
  timeouts: {
182
200
  action: cliOptions.timeoutAction,
183
201
  navigation: cliOptions.timeoutNavigation
@@ -187,6 +205,7 @@ function configFromCLIOptions(cliOptions) {
187
205
  }
188
206
  function configFromEnv() {
189
207
  const options = {};
208
+ options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
190
209
  options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
191
210
  options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
192
211
  options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
@@ -195,31 +214,38 @@ function configFromEnv() {
195
214
  options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
196
215
  options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
197
216
  options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
217
+ if (process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
218
+ options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
198
219
  options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
199
220
  options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
200
221
  options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
201
222
  options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
202
223
  options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
203
224
  options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
225
+ const initPage = envToString(process.env.PLAYWRIGHT_MCP_INIT_PAGE);
226
+ if (initPage)
227
+ options.initPage = [initPage];
204
228
  const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
205
229
  if (initScript)
206
230
  options.initScript = [initScript];
207
231
  options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
208
- if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === "omit")
209
- options.imageResponses = "omit";
232
+ if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
233
+ options.imageResponses = enumParser("--image-responses", ["allow", "omit"], process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
210
234
  options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
211
235
  options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
212
236
  options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
213
237
  options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
214
238
  options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
215
239
  options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
240
+ options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
216
241
  options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
217
242
  options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
243
+ options.testIdAttribute = envToString(process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
218
244
  options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
219
245
  options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
220
246
  options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
221
247
  options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
222
- options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
248
+ options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
223
249
  return configFromCLIOptions(options);
224
250
  }
225
251
  async function loadConfig(configFile) {
@@ -231,19 +257,31 @@ async function loadConfig(configFile) {
231
257
  throw new Error(`Failed to load config file: ${configFile}, ${error}`);
232
258
  }
233
259
  }
234
- async function outputFile(config, clientInfo, fileName, options) {
260
+ function tmpDir() {
261
+ return import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output");
262
+ }
263
+ function outputDir(config, clientInfo) {
235
264
  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));
265
+ return config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(tmpDir(), String(clientInfo.timestamp));
266
+ }
267
+ async function outputFile(config, clientInfo, fileName, options) {
268
+ const file = await resolveFile(config, clientInfo, fileName, options);
269
+ await import_fs.default.promises.mkdir(import_path.default.dirname(file), { recursive: true });
270
+ (0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
271
+ return file;
272
+ }
273
+ async function resolveFile(config, clientInfo, fileName, options) {
274
+ const dir = outputDir(config, clientInfo);
237
275
  if (options.origin === "code")
238
- return import_path.default.resolve(outputDir, fileName);
276
+ return import_path.default.resolve(dir, fileName);
239
277
  if (options.origin === "llm") {
240
278
  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))
243
- throw new Error(`Resolved file path for ${fileName} is outside of the output directory`);
279
+ const resolvedFile = import_path.default.resolve(dir, fileName);
280
+ if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
281
+ throw new Error(`Resolved file path ${resolvedFile} is outside of the output directory ${dir}. Use relative file names to stay within the output directory.`);
244
282
  return resolvedFile;
245
283
  }
246
- return import_path.default.join(outputDir, sanitizeForFilePath(fileName));
284
+ return import_path.default.join(dir, sanitizeForFilePath(fileName));
247
285
  }
248
286
  function pickDefined(obj) {
249
287
  return Object.fromEntries(
@@ -272,6 +310,10 @@ function mergeConfig(base, overrides) {
272
310
  ...pickDefined(base),
273
311
  ...pickDefined(overrides),
274
312
  browser,
313
+ console: {
314
+ ...pickDefined(base.console),
315
+ ...pickDefined(overrides.console)
316
+ },
275
317
  network: {
276
318
  ...pickDefined(base.network),
277
319
  ...pickDefined(overrides.network)
@@ -280,6 +322,10 @@ function mergeConfig(base, overrides) {
280
322
  ...pickDefined(base.server),
281
323
  ...pickDefined(overrides.server)
282
324
  },
325
+ snapshot: {
326
+ ...pickDefined(base.snapshot),
327
+ ...pickDefined(overrides.snapshot)
328
+ },
283
329
  timeouts: {
284
330
  ...pickDefined(base.timeouts),
285
331
  ...pickDefined(overrides.timeouts)
@@ -306,6 +352,23 @@ function numberParser(value) {
306
352
  return void 0;
307
353
  return +value;
308
354
  }
355
+ function resolutionParser(name, value) {
356
+ if (!value)
357
+ return void 0;
358
+ if (value.includes("x")) {
359
+ const [width, height] = value.split("x").map((v) => +v);
360
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
361
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
362
+ return { width, height };
363
+ }
364
+ if (value.includes(",")) {
365
+ const [width, height] = value.split(",").map((v) => +v);
366
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
367
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
368
+ return { width, height };
369
+ }
370
+ throw new Error(`Invalid resolution format: use ${name}="800x600"`);
371
+ }
309
372
  function headerParser(arg, previous) {
310
373
  if (!arg)
311
374
  return previous || {};
@@ -314,6 +377,11 @@ function headerParser(arg, previous) {
314
377
  result[name] = value;
315
378
  return result;
316
379
  }
380
+ function enumParser(name, options, value) {
381
+ if (!options.includes(value))
382
+ throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
383
+ return value;
384
+ }
317
385
  function envToBoolean(value) {
318
386
  if (value === "true" || value === "1")
319
387
  return true;
@@ -337,10 +405,14 @@ function sanitizeForFilePath(s) {
337
405
  configFromCLIOptions,
338
406
  defaultConfig,
339
407
  dotenvFileLoader,
408
+ enumParser,
340
409
  headerParser,
341
410
  numberParser,
411
+ outputDir,
342
412
  outputFile,
413
+ resolutionParser,
343
414
  resolveCLIConfig,
344
415
  resolveConfig,
345
- semicolonSeparatedList
416
+ semicolonSeparatedList,
417
+ tmpDir
346
418
  });
@@ -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"));
35
36
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
37
+ var import_utils = require("playwright-core/lib/utils");
38
+ var import_playwright_core = require("playwright-core");
36
39
  var import_log = require("../log");
37
40
  var import_tab = require("./tab");
38
41
  var import_config = require("./config");
39
- var codegen = __toESM(require("./codegen"));
42
+ var import_utils2 = 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,27 @@ 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 = this.config.saveVideo ? 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_utils2.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
145
+ const p = await video.path();
146
+ if (import_fs.default.existsSync(p)) {
147
+ try {
148
+ await import_fs.default.promises.rename(p, name);
149
+ } catch (e) {
150
+ if (e.code !== "EXDEV")
151
+ (0, import_log.logUnhandledError)(e);
152
+ try {
153
+ await import_fs.default.promises.copyFile(p, name);
154
+ await import_fs.default.promises.unlink(p);
155
+ } catch (e2) {
156
+ (0, import_log.logUnhandledError)(e2);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ });
139
162
  });
140
163
  }
141
164
  async dispose() {
@@ -170,6 +193,8 @@ class Context {
170
193
  async _setupBrowserContext() {
171
194
  if (this._closeBrowserContextPromise)
172
195
  throw new Error("Another browser context is being closed.");
196
+ if (this.config.testIdAttribute)
197
+ import_playwright_core.selectors.setTestIdAttribute(this.config.testIdAttribute);
173
198
  const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
174
199
  const { browserContext } = result;
175
200
  await this._setupRequestInterception(browserContext);
@@ -190,7 +215,7 @@ class Context {
190
215
  }
191
216
  lookupSecret(secretName) {
192
217
  if (!this.config.secrets?.[secretName])
193
- return { value: secretName, code: codegen.quote(secretName) };
218
+ return { value: secretName, code: (0, import_utils.escapeWithQuotes)(secretName, "'") };
194
219
  return {
195
220
  value: this.config.secrets[secretName],
196
221
  code: `process.env['${secretName}']`