@playwright/mcp 0.0.32 → 0.0.34

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/README.md CHANGED
@@ -61,7 +61,7 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
61
61
 
62
62
  #### Click the button to install:
63
63
 
64
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
64
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
65
65
 
66
66
  #### Or install manually:
67
67
 
@@ -100,6 +100,29 @@ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to you
100
100
  Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
101
101
  </details>
102
102
 
103
+ <details>
104
+ <summary>opencode</summary>
105
+
106
+ Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`:
107
+
108
+ ```json
109
+ {
110
+ "$schema": "https://opencode.ai/config.json",
111
+ "mcp": {
112
+ "playwright": {
113
+ "type": "local",
114
+ "command": [
115
+ "npx",
116
+ "@playwright/mcp@latest"
117
+ ],
118
+ "enabled": true
119
+ }
120
+ }
121
+ }
122
+
123
+ ```
124
+ </details>
125
+
103
126
  <details>
104
127
  <summary>Qodo Gen</summary>
105
128
 
@@ -158,6 +181,9 @@ Playwright MCP server supports following arguments. They can be provided in the
158
181
  --config <path> path to the configuration file.
159
182
  --device <device> device to emulate, for example: "iPhone 15"
160
183
  --executable-path <path> path to the browser executable.
184
+ --extension Connect to a running browser instance
185
+ (Edge/Chrome only). Requires the "Playwright MCP
186
+ Bridge" browser extension to be installed.
161
187
  --headless run browser in headless mode, headed by default
162
188
  --host <host> host to bind server to. Default is localhost. Use
163
189
  0.0.0.0 to bind to all interfaces.
@@ -191,7 +217,7 @@ Playwright MCP server supports following arguments. They can be provided in the
191
217
 
192
218
  ### User profile
193
219
 
194
- You can run Playwright MCP with persistent profile like a regular browser (default), or in the isolated contexts for the testing sessions.
220
+ You can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension.
195
221
 
196
222
  **Persistent profile**
197
223
 
@@ -231,6 +257,10 @@ state [here](https://playwright.dev/docs/auth).
231
257
  }
232
258
  ```
233
259
 
260
+ **Browser Extension**
261
+
262
+ The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [extension/README.md](extension/README.md) for installation and setup instructions.
263
+
234
264
  ### Configuration file
235
265
 
236
266
  The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file
@@ -544,7 +574,7 @@ http.createServer(async (req, res) => {
544
574
  - Title: Take a screenshot
545
575
  - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
546
576
  - Parameters:
547
- - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
577
+ - `type` (string, optional): Image format for the screenshot. Default is png.
548
578
  - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
549
579
  - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
550
580
  - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
@@ -13,34 +13,41 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import fs from 'node:fs';
17
- import net from 'node:net';
18
- import path from 'node:path';
19
- import os from 'node:os';
16
+ import fs from 'fs';
17
+ import net from 'net';
18
+ import path from 'path';
20
19
  import * as playwright from 'playwright';
21
- import { logUnhandledError, testDebug } from './log.js';
22
- export function contextFactory(browserConfig) {
23
- if (browserConfig.remoteEndpoint)
24
- return new RemoteContextFactory(browserConfig);
25
- if (browserConfig.cdpEndpoint)
26
- return new CdpContextFactory(browserConfig);
27
- if (browserConfig.isolated)
28
- return new IsolatedContextFactory(browserConfig);
29
- return new PersistentContextFactory(browserConfig);
20
+ // @ts-ignore
21
+ import { registryDirectory } from 'playwright-core/lib/server/registry/index';
22
+ // @ts-ignore
23
+ import { startTraceViewerServer } from 'playwright-core/lib/server';
24
+ import { logUnhandledError, testDebug } from './utils/log.js';
25
+ import { createHash } from './utils/guid.js';
26
+ import { outputFile } from './config.js';
27
+ export function contextFactory(config) {
28
+ if (config.browser.remoteEndpoint)
29
+ return new RemoteContextFactory(config);
30
+ if (config.browser.cdpEndpoint)
31
+ return new CdpContextFactory(config);
32
+ if (config.browser.isolated)
33
+ return new IsolatedContextFactory(config);
34
+ return new PersistentContextFactory(config);
30
35
  }
31
36
  class BaseContextFactory {
32
- browserConfig;
33
- _browserPromise;
34
37
  name;
35
- constructor(name, browserConfig) {
38
+ description;
39
+ config;
40
+ _browserPromise;
41
+ constructor(name, description, config) {
36
42
  this.name = name;
37
- this.browserConfig = browserConfig;
43
+ this.description = description;
44
+ this.config = config;
38
45
  }
39
- async _obtainBrowser() {
46
+ async _obtainBrowser(clientInfo) {
40
47
  if (this._browserPromise)
41
48
  return this._browserPromise;
42
49
  testDebug(`obtain browser (${this.name})`);
43
- this._browserPromise = this._doObtainBrowser();
50
+ this._browserPromise = this._doObtainBrowser(clientInfo);
44
51
  void this._browserPromise.then(browser => {
45
52
  browser.on('disconnected', () => {
46
53
  this._browserPromise = undefined;
@@ -50,12 +57,12 @@ class BaseContextFactory {
50
57
  });
51
58
  return this._browserPromise;
52
59
  }
53
- async _doObtainBrowser() {
60
+ async _doObtainBrowser(clientInfo) {
54
61
  throw new Error('Not implemented');
55
62
  }
56
- async createContext() {
63
+ async createContext(clientInfo) {
57
64
  testDebug(`create browser context (${this.name})`);
58
- const browser = await this._obtainBrowser();
65
+ const browser = await this._obtainBrowser(clientInfo);
59
66
  const browserContext = await this._doCreateContext(browser);
60
67
  return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
61
68
  }
@@ -74,14 +81,15 @@ class BaseContextFactory {
74
81
  }
75
82
  }
76
83
  class IsolatedContextFactory extends BaseContextFactory {
77
- constructor(browserConfig) {
78
- super('isolated', browserConfig);
84
+ constructor(config) {
85
+ super('isolated', 'Create a new isolated browser context', config);
79
86
  }
80
- async _doObtainBrowser() {
81
- await injectCdpPort(this.browserConfig);
82
- const browserType = playwright[this.browserConfig.browserName];
87
+ async _doObtainBrowser(clientInfo) {
88
+ await injectCdpPort(this.config.browser);
89
+ const browserType = playwright[this.config.browser.browserName];
83
90
  return browserType.launch({
84
- ...this.browserConfig.launchOptions,
91
+ tracesDir: await startTraceServer(this.config, clientInfo.rootPath),
92
+ ...this.config.browser.launchOptions,
85
93
  handleSIGINT: false,
86
94
  handleSIGTERM: false,
87
95
  }).catch(error => {
@@ -91,53 +99,57 @@ class IsolatedContextFactory extends BaseContextFactory {
91
99
  });
92
100
  }
93
101
  async _doCreateContext(browser) {
94
- return browser.newContext(this.browserConfig.contextOptions);
102
+ return browser.newContext(this.config.browser.contextOptions);
95
103
  }
96
104
  }
97
105
  class CdpContextFactory extends BaseContextFactory {
98
- constructor(browserConfig) {
99
- super('cdp', browserConfig);
106
+ constructor(config) {
107
+ super('cdp', 'Connect to a browser over CDP', config);
100
108
  }
101
109
  async _doObtainBrowser() {
102
- return playwright.chromium.connectOverCDP(this.browserConfig.cdpEndpoint);
110
+ return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
103
111
  }
104
112
  async _doCreateContext(browser) {
105
- return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
113
+ return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
106
114
  }
107
115
  }
108
116
  class RemoteContextFactory extends BaseContextFactory {
109
- constructor(browserConfig) {
110
- super('remote', browserConfig);
117
+ constructor(config) {
118
+ super('remote', 'Connect to a browser using a remote endpoint', config);
111
119
  }
112
120
  async _doObtainBrowser() {
113
- const url = new URL(this.browserConfig.remoteEndpoint);
114
- url.searchParams.set('browser', this.browserConfig.browserName);
115
- if (this.browserConfig.launchOptions)
116
- url.searchParams.set('launch-options', JSON.stringify(this.browserConfig.launchOptions));
117
- return playwright[this.browserConfig.browserName].connect(String(url));
121
+ const url = new URL(this.config.browser.remoteEndpoint);
122
+ url.searchParams.set('browser', this.config.browser.browserName);
123
+ if (this.config.browser.launchOptions)
124
+ url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
125
+ return playwright[this.config.browser.browserName].connect(String(url));
118
126
  }
119
127
  async _doCreateContext(browser) {
120
128
  return browser.newContext();
121
129
  }
122
130
  }
123
131
  class PersistentContextFactory {
124
- browserConfig;
132
+ config;
133
+ name = 'persistent';
134
+ description = 'Create a new persistent browser context';
125
135
  _userDataDirs = new Set();
126
- constructor(browserConfig) {
127
- this.browserConfig = browserConfig;
136
+ constructor(config) {
137
+ this.config = config;
128
138
  }
129
- async createContext() {
130
- await injectCdpPort(this.browserConfig);
139
+ async createContext(clientInfo) {
140
+ await injectCdpPort(this.config.browser);
131
141
  testDebug('create browser context (persistent)');
132
- const userDataDir = this.browserConfig.userDataDir ?? await this._createUserDataDir();
142
+ const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
143
+ const tracesDir = await startTraceServer(this.config, clientInfo.rootPath);
133
144
  this._userDataDirs.add(userDataDir);
134
145
  testDebug('lock user data dir', userDataDir);
135
- const browserType = playwright[this.browserConfig.browserName];
146
+ const browserType = playwright[this.config.browser.browserName];
136
147
  for (let i = 0; i < 5; i++) {
137
148
  try {
138
149
  const browserContext = await browserType.launchPersistentContext(userDataDir, {
139
- ...this.browserConfig.launchOptions,
140
- ...this.browserConfig.contextOptions,
150
+ tracesDir,
151
+ ...this.config.browser.launchOptions,
152
+ ...this.config.browser.contextOptions,
141
153
  handleSIGINT: false,
142
154
  handleSIGTERM: false,
143
155
  });
@@ -164,17 +176,12 @@ class PersistentContextFactory {
164
176
  this._userDataDirs.delete(userDataDir);
165
177
  testDebug('close browser context complete (persistent)');
166
178
  }
167
- async _createUserDataDir() {
168
- let cacheDirectory;
169
- if (process.platform === 'linux')
170
- cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
171
- else if (process.platform === 'darwin')
172
- cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
173
- else if (process.platform === 'win32')
174
- cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
175
- else
176
- throw new Error('Unsupported platform: ' + process.platform);
177
- const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${this.browserConfig.launchOptions?.channel ?? this.browserConfig?.browserName}-profile`);
179
+ async _createUserDataDir(rootPath) {
180
+ const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
181
+ const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
182
+ // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead.
183
+ const rootPathToken = rootPath ? `-${createHash(rootPath)}` : '';
184
+ const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`);
178
185
  await fs.promises.mkdir(result, { recursive: true });
179
186
  return result;
180
187
  }
@@ -193,3 +200,14 @@ async function findFreePort() {
193
200
  server.on('error', reject);
194
201
  });
195
202
  }
203
+ async function startTraceServer(config, rootPath) {
204
+ if (!config.saveTrace)
205
+ return undefined;
206
+ const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`);
207
+ const server = await startTraceViewerServer();
208
+ const urlPrefix = server.urlPrefix('human-readable');
209
+ const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json';
210
+ // eslint-disable-next-line no-console
211
+ console.error('\nTrace viewer listening on ' + url);
212
+ return tracesDir;
213
+ }
@@ -13,42 +13,68 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import { fileURLToPath } from 'url';
16
17
  import { Context } from './context.js';
17
- import { logUnhandledError } from './log.js';
18
+ import { logUnhandledError } from './utils/log.js';
18
19
  import { Response } from './response.js';
19
20
  import { SessionLog } from './sessionLog.js';
20
21
  import { filteredTools } from './tools.js';
21
- import { packageJSON } from './package.js';
22
+ import { packageJSON } from './utils/package.js';
23
+ import { toMcpTool } from './mcp/tool.js';
22
24
  export class BrowserServerBackend {
23
25
  name = 'Playwright';
24
26
  version = packageJSON.version;
25
- onclose;
26
27
  _tools;
27
28
  _context;
28
29
  _sessionLog;
29
- constructor(config, browserContextFactory) {
30
+ _config;
31
+ _browserContextFactory;
32
+ constructor(config, factory) {
33
+ this._config = config;
34
+ this._browserContextFactory = factory;
30
35
  this._tools = filteredTools(config);
31
- this._context = new Context(this._tools, config, browserContextFactory);
32
36
  }
33
- async initialize() {
34
- this._sessionLog = this._context.config.saveSession ? await SessionLog.create(this._context.config) : undefined;
37
+ async initialize(clientVersion, roots) {
38
+ let rootPath;
39
+ if (roots.length > 0) {
40
+ const firstRootUri = roots[0]?.uri;
41
+ const url = firstRootUri ? new URL(firstRootUri) : undefined;
42
+ rootPath = url ? fileURLToPath(url) : undefined;
43
+ }
44
+ this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined;
45
+ this._context = new Context({
46
+ tools: this._tools,
47
+ config: this._config,
48
+ browserContextFactory: this._browserContextFactory,
49
+ sessionLog: this._sessionLog,
50
+ clientInfo: { ...clientVersion, rootPath },
51
+ });
35
52
  }
36
- tools() {
37
- return this._tools.map(tool => tool.schema);
53
+ async listTools() {
54
+ return this._tools.map(tool => toMcpTool(tool.schema));
38
55
  }
39
- async callTool(schema, parsedArguments) {
40
- const response = new Response(this._context, schema.name, parsedArguments);
41
- const tool = this._tools.find(tool => tool.schema.name === schema.name);
42
- await tool.handle(this._context, parsedArguments, response);
43
- if (this._sessionLog)
44
- await this._sessionLog.log(response);
45
- return await response.serialize();
46
- }
47
- serverInitialized(version) {
48
- this._context.clientVersion = version;
56
+ async callTool(name, rawArguments) {
57
+ const tool = this._tools.find(tool => tool.schema.name === name);
58
+ if (!tool)
59
+ throw new Error(`Tool "${name}" not found`);
60
+ const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
61
+ const context = this._context;
62
+ const response = new Response(context, name, parsedArguments);
63
+ context.setRunningTool(true);
64
+ try {
65
+ await tool.handle(context, parsedArguments, response);
66
+ await response.finish();
67
+ this._sessionLog?.logResponse(response);
68
+ }
69
+ catch (error) {
70
+ response.addError(String(error));
71
+ }
72
+ finally {
73
+ context.setRunningTool(false);
74
+ }
75
+ return response.serialize();
49
76
  }
50
77
  serverClosed() {
51
- this.onclose?.();
52
- void this._context.dispose().catch(logUnhandledError);
78
+ void this._context?.dispose().catch(logUnhandledError);
53
79
  }
54
80
  }
package/lib/config.js CHANGED
@@ -17,7 +17,7 @@ import fs from 'fs';
17
17
  import os from 'os';
18
18
  import path from 'path';
19
19
  import { devices } from 'playwright';
20
- import { sanitizeForFilePath } from './tools/utils.js';
20
+ import { sanitizeForFilePath } from './utils/fileUtils.js';
21
21
  const defaultConfig = {
22
22
  browser: {
23
23
  browserName: 'chromium',
@@ -35,7 +35,7 @@ const defaultConfig = {
35
35
  blockedOrigins: undefined,
36
36
  },
37
37
  server: {},
38
- outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
38
+ saveTrace: false,
39
39
  };
40
40
  export async function resolveConfig(config) {
41
41
  return mergeConfig(defaultConfig, config);
@@ -48,9 +48,6 @@ export async function resolveCLIConfig(cliOptions) {
48
48
  result = mergeConfig(result, configInFile);
49
49
  result = mergeConfig(result, envOverrides);
50
50
  result = mergeConfig(result, cliOverrides);
51
- // Derive artifact output directory from config.outputDir
52
- if (result.saveTrace)
53
- result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
54
51
  return result;
55
52
  }
56
53
  export function configFromCLIOptions(cliOptions) {
@@ -179,10 +176,13 @@ async function loadConfig(configFile) {
179
176
  throw new Error(`Failed to load config file: ${configFile}, ${error}`);
180
177
  }
181
178
  }
182
- export async function outputFile(config, name) {
183
- await fs.promises.mkdir(config.outputDir, { recursive: true });
179
+ export async function outputFile(config, rootPath, name) {
180
+ const outputDir = config.outputDir
181
+ ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined)
182
+ ?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString()));
183
+ await fs.promises.mkdir(outputDir, { recursive: true });
184
184
  const fileName = sanitizeForFilePath(name);
185
- return path.join(config.outputDir, fileName);
185
+ return path.join(outputDir, fileName);
186
186
  }
187
187
  function pickDefined(obj) {
188
188
  return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined));
package/lib/context.js CHANGED
@@ -14,23 +14,31 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import debug from 'debug';
17
- import { logUnhandledError } from './log.js';
17
+ import { logUnhandledError } from './utils/log.js';
18
18
  import { Tab } from './tab.js';
19
+ import { outputFile } from './config.js';
19
20
  const testDebug = debug('pw:mcp:test');
20
21
  export class Context {
21
22
  tools;
22
23
  config;
24
+ sessionLog;
25
+ options;
23
26
  _browserContextPromise;
24
27
  _browserContextFactory;
25
28
  _tabs = [];
26
29
  _currentTab;
27
- clientVersion;
30
+ _clientInfo;
28
31
  static _allContexts = new Set();
29
32
  _closeBrowserContextPromise;
30
- constructor(tools, config, browserContextFactory) {
31
- this.tools = tools;
32
- this.config = config;
33
- this._browserContextFactory = browserContextFactory;
33
+ _isRunningTool = false;
34
+ _abortController = new AbortController();
35
+ constructor(options) {
36
+ this.tools = options.tools;
37
+ this.config = options.config;
38
+ this.sessionLog = options.sessionLog;
39
+ this.options = options;
40
+ this._browserContextFactory = options.browserContextFactory;
41
+ this._clientInfo = options.clientInfo;
34
42
  testDebug('create context');
35
43
  Context._allContexts.add(this);
36
44
  }
@@ -68,27 +76,6 @@ export class Context {
68
76
  await browserContext.newPage();
69
77
  return this._currentTab;
70
78
  }
71
- async listTabsMarkdown(force = false) {
72
- if (this._tabs.length === 1 && !force)
73
- return [];
74
- if (!this._tabs.length) {
75
- return [
76
- '### No open tabs',
77
- 'Use the "browser_navigate" tool to navigate to a page first.',
78
- '',
79
- ];
80
- }
81
- const lines = ['### Open tabs'];
82
- for (let i = 0; i < this._tabs.length; i++) {
83
- const tab = this._tabs[i];
84
- const title = await tab.title();
85
- const url = tab.page.url();
86
- const current = tab === this._currentTab ? ' (current)' : '';
87
- lines.push(`- ${i}:${current} [${title}] (${url})`);
88
- }
89
- lines.push('');
90
- return lines;
91
- }
92
79
  async closeTab(index) {
93
80
  const tab = index === undefined ? this._currentTab : this._tabs[index];
94
81
  if (!tab)
@@ -97,6 +84,9 @@ export class Context {
97
84
  await tab.page.close();
98
85
  return url;
99
86
  }
87
+ async outputFile(name) {
88
+ return outputFile(this.config, this._clientInfo.rootPath, name);
89
+ }
100
90
  _onPageCreated(page) {
101
91
  const tab = new Tab(this, page, tab => this._onPageClosed(tab));
102
92
  this._tabs.push(tab);
@@ -119,6 +109,12 @@ export class Context {
119
109
  await this._closeBrowserContextPromise;
120
110
  this._closeBrowserContextPromise = undefined;
121
111
  }
112
+ isRunningTool() {
113
+ return this._isRunningTool;
114
+ }
115
+ setRunningTool(isRunningTool) {
116
+ this._isRunningTool = isRunningTool;
117
+ }
122
118
  async _closeBrowserContextImpl() {
123
119
  if (!this._browserContextPromise)
124
120
  return;
@@ -132,6 +128,7 @@ export class Context {
132
128
  });
133
129
  }
134
130
  async dispose() {
131
+ this._abortController.abort('MCP context disposed');
135
132
  await this.closeBrowserContext();
136
133
  Context._allContexts.delete(this);
137
134
  }
@@ -159,9 +156,11 @@ export class Context {
159
156
  if (this._closeBrowserContextPromise)
160
157
  throw new Error('Another browser context is being closed.');
161
158
  // TODO: move to the browser context factory to make it based on isolation mode.
162
- const result = await this._browserContextFactory.createContext(this.clientVersion);
159
+ const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal);
163
160
  const { browserContext } = result;
164
161
  await this._setupRequestInterception(browserContext);
162
+ if (this.sessionLog)
163
+ await InputRecorder.create(this, browserContext);
165
164
  for (const page of browserContext.pages())
166
165
  this._onPageCreated(page);
167
166
  browserContext.on('page', page => this._onPageCreated(page));
@@ -176,3 +175,52 @@ export class Context {
176
175
  return result;
177
176
  }
178
177
  }
178
+ export class InputRecorder {
179
+ _context;
180
+ _browserContext;
181
+ constructor(context, browserContext) {
182
+ this._context = context;
183
+ this._browserContext = browserContext;
184
+ }
185
+ static async create(context, browserContext) {
186
+ const recorder = new InputRecorder(context, browserContext);
187
+ await recorder._initialize();
188
+ return recorder;
189
+ }
190
+ async _initialize() {
191
+ const sessionLog = this._context.sessionLog;
192
+ await this._browserContext._enableRecorder({
193
+ mode: 'recording',
194
+ recorderMode: 'api',
195
+ }, {
196
+ actionAdded: (page, data, code) => {
197
+ if (this._context.isRunningTool())
198
+ return;
199
+ const tab = Tab.forPage(page);
200
+ if (tab)
201
+ sessionLog.logUserAction(data.action, tab, code, false);
202
+ },
203
+ actionUpdated: (page, data, code) => {
204
+ if (this._context.isRunningTool())
205
+ return;
206
+ const tab = Tab.forPage(page);
207
+ if (tab)
208
+ sessionLog.logUserAction(data.action, tab, code, true);
209
+ },
210
+ signalAdded: (page, data) => {
211
+ if (this._context.isRunningTool())
212
+ return;
213
+ if (data.signal.name !== 'navigation')
214
+ return;
215
+ const tab = Tab.forPage(page);
216
+ const navigateAction = {
217
+ name: 'navigate',
218
+ url: data.signal.url,
219
+ signals: [],
220
+ };
221
+ if (tab)
222
+ sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
223
+ },
224
+ });
225
+ }
226
+ }