@playwright/mcp 0.0.30 → 0.0.32

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 (54) hide show
  1. package/README.md +180 -320
  2. package/config.d.ts +5 -14
  3. package/index.d.ts +1 -6
  4. package/lib/browserContextFactory.js +3 -35
  5. package/lib/browserServerBackend.js +54 -0
  6. package/lib/config.js +64 -7
  7. package/lib/context.js +50 -163
  8. package/lib/extension/cdpRelay.js +370 -0
  9. package/lib/extension/main.js +33 -0
  10. package/lib/httpServer.js +20 -182
  11. package/lib/index.js +3 -2
  12. package/lib/log.js +21 -0
  13. package/lib/loop/loop.js +69 -0
  14. package/lib/loop/loopClaude.js +152 -0
  15. package/lib/loop/loopOpenAI.js +141 -0
  16. package/lib/loop/main.js +60 -0
  17. package/lib/loopTools/context.js +66 -0
  18. package/lib/loopTools/main.js +49 -0
  19. package/lib/loopTools/perform.js +32 -0
  20. package/lib/loopTools/snapshot.js +29 -0
  21. package/lib/loopTools/tool.js +18 -0
  22. package/lib/mcp/inProcessTransport.js +72 -0
  23. package/lib/mcp/server.js +88 -0
  24. package/lib/{transport.js → mcp/transport.js} +30 -42
  25. package/lib/package.js +3 -3
  26. package/lib/program.js +47 -17
  27. package/lib/response.js +98 -0
  28. package/lib/sessionLog.js +70 -0
  29. package/lib/tab.js +166 -21
  30. package/lib/tools/common.js +13 -25
  31. package/lib/tools/console.js +4 -15
  32. package/lib/tools/dialogs.js +14 -19
  33. package/lib/tools/evaluate.js +53 -0
  34. package/lib/tools/files.js +13 -19
  35. package/lib/tools/install.js +4 -8
  36. package/lib/tools/keyboard.js +51 -17
  37. package/lib/tools/mouse.js +99 -0
  38. package/lib/tools/navigate.js +22 -42
  39. package/lib/tools/network.js +5 -15
  40. package/lib/tools/pdf.js +8 -15
  41. package/lib/tools/screenshot.js +29 -30
  42. package/lib/tools/snapshot.js +49 -109
  43. package/lib/tools/tabs.js +21 -52
  44. package/lib/tools/tool.js +14 -0
  45. package/lib/tools/utils.js +7 -6
  46. package/lib/tools/wait.js +8 -11
  47. package/lib/tools.js +15 -26
  48. package/package.json +12 -5
  49. package/lib/browserServer.js +0 -151
  50. package/lib/connection.js +0 -82
  51. package/lib/pageSnapshot.js +0 -43
  52. package/lib/server.js +0 -48
  53. package/lib/tools/testing.js +0 -60
  54. package/lib/tools/vision.js +0 -189
package/config.d.ts CHANGED
@@ -16,18 +16,13 @@
16
16
 
17
17
  import type * as playwright from 'playwright';
18
18
 
19
- export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install' | 'testing';
19
+ export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf';
20
20
 
21
21
  export type Config = {
22
22
  /**
23
23
  * The browser to use.
24
24
  */
25
25
  browser?: {
26
- /**
27
- * Use browser agent (experimental).
28
- */
29
- browserAgent?: string;
30
-
31
26
  /**
32
27
  * The type of browser to use.
33
28
  */
@@ -85,19 +80,15 @@ export type Config = {
85
80
  /**
86
81
  * List of enabled tool capabilities. Possible values:
87
82
  * - 'core': Core browser automation features.
88
- * - 'tabs': Tab management features.
89
83
  * - 'pdf': PDF generation and manipulation.
90
- * - 'history': Browser history access.
91
- * - 'wait': Wait and timing utilities.
92
- * - 'files': File upload/download support.
93
- * - 'install': Browser installation utilities.
84
+ * - 'vision': Coordinate-based interactions.
94
85
  */
95
86
  capabilities?: ToolCapability[];
96
87
 
97
88
  /**
98
- * Run server that uses screenshots (Aria snapshots are used by default).
89
+ * Whether to save the Playwright session into the output directory.
99
90
  */
100
- vision?: boolean;
91
+ saveSession?: boolean;
101
92
 
102
93
  /**
103
94
  * Whether to save the Playwright trace of the session into the output directory.
@@ -124,5 +115,5 @@ export type Config = {
124
115
  /**
125
116
  * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
126
117
  */
127
- imageResponses?: 'allow' | 'omit' | 'auto';
118
+ imageResponses?: 'allow' | 'omit';
128
119
  };
package/index.d.ts CHANGED
@@ -19,10 +19,5 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
19
  import type { Config } from './config.js';
20
20
  import type { BrowserContext } from 'playwright';
21
21
 
22
- export type Connection = {
23
- server: Server;
24
- close(): Promise<void>;
25
- };
26
-
27
- export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Connection>;
22
+ export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Server>;
28
23
  export {};
@@ -17,10 +17,8 @@ import fs from 'node:fs';
17
17
  import net from 'node:net';
18
18
  import path from 'node:path';
19
19
  import os from 'node:os';
20
- import debug from 'debug';
21
20
  import * as playwright from 'playwright';
22
- import { userDataDir } from './fileUtils.js';
23
- const testDebug = debug('pw:mcp:test');
21
+ import { logUnhandledError, testDebug } from './log.js';
24
22
  export function contextFactory(browserConfig) {
25
23
  if (browserConfig.remoteEndpoint)
26
24
  return new RemoteContextFactory(browserConfig);
@@ -28,8 +26,6 @@ export function contextFactory(browserConfig) {
28
26
  return new CdpContextFactory(browserConfig);
29
27
  if (browserConfig.isolated)
30
28
  return new IsolatedContextFactory(browserConfig);
31
- if (browserConfig.browserAgent)
32
- return new BrowserServerContextFactory(browserConfig);
33
29
  return new PersistentContextFactory(browserConfig);
34
30
  }
35
31
  class BaseContextFactory {
@@ -70,10 +66,10 @@ class BaseContextFactory {
70
66
  testDebug(`close browser context (${this.name})`);
71
67
  if (browser.contexts().length === 1)
72
68
  this._browserPromise = undefined;
73
- await browserContext.close().catch(() => { });
69
+ await browserContext.close().catch(logUnhandledError);
74
70
  if (browser.contexts().length === 0) {
75
71
  testDebug(`close browser (${this.name})`);
76
- await browser.close().catch(() => { });
72
+ await browser.close().catch(logUnhandledError);
77
73
  }
78
74
  }
79
75
  }
@@ -183,34 +179,6 @@ class PersistentContextFactory {
183
179
  return result;
184
180
  }
185
181
  }
186
- export class BrowserServerContextFactory extends BaseContextFactory {
187
- constructor(browserConfig) {
188
- super('persistent', browserConfig);
189
- }
190
- async _doObtainBrowser() {
191
- const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), {
192
- method: 'POST',
193
- body: JSON.stringify({
194
- browserType: this.browserConfig.browserName,
195
- userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(),
196
- launchOptions: this.browserConfig.launchOptions,
197
- contextOptions: this.browserConfig.contextOptions,
198
- }),
199
- });
200
- const info = await response.json();
201
- if (info.error)
202
- throw new Error(info.error);
203
- return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`);
204
- }
205
- async _doCreateContext(browser) {
206
- return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
207
- }
208
- async _createUserDataDir() {
209
- const dir = await userDataDir(this.browserConfig);
210
- await fs.promises.mkdir(dir, { recursive: true });
211
- return dir;
212
- }
213
- }
214
182
  async function injectCdpPort(browserConfig) {
215
183
  if (browserConfig.browserName === 'chromium')
216
184
  browserConfig.launchOptions.cdpPort = await findFreePort();
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { Context } from './context.js';
17
+ import { logUnhandledError } from './log.js';
18
+ import { Response } from './response.js';
19
+ import { SessionLog } from './sessionLog.js';
20
+ import { filteredTools } from './tools.js';
21
+ import { packageJSON } from './package.js';
22
+ export class BrowserServerBackend {
23
+ name = 'Playwright';
24
+ version = packageJSON.version;
25
+ onclose;
26
+ _tools;
27
+ _context;
28
+ _sessionLog;
29
+ constructor(config, browserContextFactory) {
30
+ this._tools = filteredTools(config);
31
+ this._context = new Context(this._tools, config, browserContextFactory);
32
+ }
33
+ async initialize() {
34
+ this._sessionLog = this._context.config.saveSession ? await SessionLog.create(this._context.config) : undefined;
35
+ }
36
+ tools() {
37
+ return this._tools.map(tool => tool.schema);
38
+ }
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;
49
+ }
50
+ serverClosed() {
51
+ this.onclose?.();
52
+ void this._context.dispose().catch(logUnhandledError);
53
+ }
54
+ }
package/lib/config.js CHANGED
@@ -42,14 +42,18 @@ export async function resolveConfig(config) {
42
42
  }
43
43
  export async function resolveCLIConfig(cliOptions) {
44
44
  const configInFile = await loadConfig(cliOptions.config);
45
- const cliOverrides = await configFromCLIOptions(cliOptions);
46
- const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
45
+ const envOverrides = configFromEnv();
46
+ const cliOverrides = configFromCLIOptions(cliOptions);
47
+ let result = defaultConfig;
48
+ result = mergeConfig(result, configInFile);
49
+ result = mergeConfig(result, envOverrides);
50
+ result = mergeConfig(result, cliOverrides);
47
51
  // Derive artifact output directory from config.outputDir
48
52
  if (result.saveTrace)
49
53
  result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
50
54
  return result;
51
55
  }
52
- export async function configFromCLIOptions(cliOptions) {
56
+ export function configFromCLIOptions(cliOptions) {
53
57
  let browserName;
54
58
  let channel;
55
59
  switch (cliOptions.browser) {
@@ -79,7 +83,7 @@ export async function configFromCLIOptions(cliOptions) {
79
83
  headless: cliOptions.headless,
80
84
  };
81
85
  // --no-sandbox was passed, disable the sandbox
82
- if (!cliOptions.sandbox)
86
+ if (cliOptions.sandbox === false)
83
87
  launchOptions.chromiumSandbox = false;
84
88
  if (cliOptions.proxyServer) {
85
89
  launchOptions.proxy = {
@@ -113,7 +117,6 @@ export async function configFromCLIOptions(cliOptions) {
113
117
  contextOptions.serviceWorkers = 'block';
114
118
  const result = {
115
119
  browser: {
116
- browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT,
117
120
  browserName,
118
121
  isolated: cliOptions.isolated,
119
122
  userDataDir: cliOptions.userDataDir,
@@ -125,18 +128,47 @@ export async function configFromCLIOptions(cliOptions) {
125
128
  port: cliOptions.port,
126
129
  host: cliOptions.host,
127
130
  },
128
- capabilities: cliOptions.caps?.split(',').map((c) => c.trim()),
129
- vision: !!cliOptions.vision,
131
+ capabilities: cliOptions.caps,
130
132
  network: {
131
133
  allowedOrigins: cliOptions.allowedOrigins,
132
134
  blockedOrigins: cliOptions.blockedOrigins,
133
135
  },
136
+ saveSession: cliOptions.saveSession,
134
137
  saveTrace: cliOptions.saveTrace,
135
138
  outputDir: cliOptions.outputDir,
136
139
  imageResponses: cliOptions.imageResponses,
137
140
  };
138
141
  return result;
139
142
  }
143
+ function configFromEnv() {
144
+ const options = {};
145
+ options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
146
+ options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
147
+ options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
148
+ options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
149
+ options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
150
+ options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
151
+ options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
152
+ options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
153
+ options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
154
+ options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
155
+ options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
156
+ options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
157
+ options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
158
+ if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit')
159
+ options.imageResponses = 'omit';
160
+ options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
161
+ options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
162
+ options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);
163
+ options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
164
+ options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
165
+ options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
166
+ options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
167
+ options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
168
+ options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
169
+ options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
170
+ return configFromCLIOptions(options);
171
+ }
140
172
  async function loadConfig(configFile) {
141
173
  if (!configFile)
142
174
  return {};
@@ -187,3 +219,28 @@ function mergeConfig(base, overrides) {
187
219
  },
188
220
  };
189
221
  }
222
+ export function semicolonSeparatedList(value) {
223
+ if (!value)
224
+ return undefined;
225
+ return value.split(';').map(v => v.trim());
226
+ }
227
+ export function commaSeparatedList(value) {
228
+ if (!value)
229
+ return undefined;
230
+ return value.split(',').map(v => v.trim());
231
+ }
232
+ function envToNumber(value) {
233
+ if (!value)
234
+ return undefined;
235
+ return +value;
236
+ }
237
+ function envToBoolean(value) {
238
+ if (value === 'true' || value === '1')
239
+ return true;
240
+ if (value === 'false' || value === '0')
241
+ return false;
242
+ return undefined;
243
+ }
244
+ function envToString(value) {
245
+ return value ? value.trim() : undefined;
246
+ }
package/lib/context.js CHANGED
@@ -14,10 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import debug from 'debug';
17
- import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
- import { ManualPromise } from './manualPromise.js';
17
+ import { logUnhandledError } from './log.js';
19
18
  import { Tab } from './tab.js';
20
- import { outputFile } from './config.js';
21
19
  const testDebug = debug('pw:mcp:test');
22
20
  export class Context {
23
21
  tools;
@@ -26,48 +24,28 @@ export class Context {
26
24
  _browserContextFactory;
27
25
  _tabs = [];
28
26
  _currentTab;
29
- _modalStates = [];
30
- _pendingAction;
31
- _downloads = [];
32
27
  clientVersion;
28
+ static _allContexts = new Set();
29
+ _closeBrowserContextPromise;
33
30
  constructor(tools, config, browserContextFactory) {
34
31
  this.tools = tools;
35
32
  this.config = config;
36
33
  this._browserContextFactory = browserContextFactory;
37
34
  testDebug('create context');
35
+ Context._allContexts.add(this);
38
36
  }
39
- clientSupportsImages() {
40
- if (this.config.imageResponses === 'allow')
41
- return true;
42
- if (this.config.imageResponses === 'omit')
43
- return false;
44
- return !this.clientVersion?.name.includes('cursor');
45
- }
46
- modalStates() {
47
- return this._modalStates;
48
- }
49
- setModalState(modalState, inTab) {
50
- this._modalStates.push({ ...modalState, tab: inTab });
51
- }
52
- clearModalState(modalState) {
53
- this._modalStates = this._modalStates.filter(state => state !== modalState);
54
- }
55
- modalStatesMarkdown() {
56
- const result = ['### Modal state'];
57
- if (this._modalStates.length === 0)
58
- result.push('- There is no modal state present');
59
- for (const state of this._modalStates) {
60
- const tool = this.tools.find(tool => tool.clearsModalState === state.type);
61
- result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
62
- }
63
- return result;
37
+ static async disposeAll() {
38
+ await Promise.all([...Context._allContexts].map(context => context.dispose()));
64
39
  }
65
40
  tabs() {
66
41
  return this._tabs;
67
42
  }
43
+ currentTab() {
44
+ return this._currentTab;
45
+ }
68
46
  currentTabOrDie() {
69
47
  if (!this._currentTab)
70
- throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location first.');
48
+ throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.');
71
49
  return this._currentTab;
72
50
  }
73
51
  async newTab() {
@@ -77,8 +55,12 @@ export class Context {
77
55
  return this._currentTab;
78
56
  }
79
57
  async selectTab(index) {
80
- this._currentTab = this._tabs[index - 1];
81
- await this._currentTab.page.bringToFront();
58
+ const tab = this._tabs[index];
59
+ if (!tab)
60
+ throw new Error(`Tab ${index} not found`);
61
+ await tab.page.bringToFront();
62
+ this._currentTab = tab;
63
+ return tab;
82
64
  }
83
65
  async ensureTab() {
84
66
  const { browserContext } = await this._ensureBrowserContext();
@@ -86,140 +68,34 @@ export class Context {
86
68
  await browserContext.newPage();
87
69
  return this._currentTab;
88
70
  }
89
- async listTabsMarkdown() {
90
- if (!this._tabs.length)
91
- return '### No tabs open';
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
+ }
92
81
  const lines = ['### Open tabs'];
93
82
  for (let i = 0; i < this._tabs.length; i++) {
94
83
  const tab = this._tabs[i];
95
84
  const title = await tab.title();
96
85
  const url = tab.page.url();
97
86
  const current = tab === this._currentTab ? ' (current)' : '';
98
- lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
87
+ lines.push(`- ${i}:${current} [${title}] (${url})`);
99
88
  }
100
- return lines.join('\n');
89
+ lines.push('');
90
+ return lines;
101
91
  }
102
92
  async closeTab(index) {
103
- const tab = index === undefined ? this._currentTab : this._tabs[index - 1];
104
- await tab?.page.close();
105
- return await this.listTabsMarkdown();
106
- }
107
- async run(tool, params) {
108
- // Tab management is done outside of the action() call.
109
- const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
110
- const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
111
- const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
112
- if (resultOverride)
113
- return resultOverride;
114
- if (!this._currentTab) {
115
- return {
116
- content: [{
117
- type: 'text',
118
- text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.',
119
- }],
120
- };
121
- }
122
- const tab = this.currentTabOrDie();
123
- // TODO: race against modal dialogs to resolve clicks.
124
- let actionResult;
125
- try {
126
- if (waitForNetwork)
127
- actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
128
- else
129
- actionResult = await racingAction?.() ?? undefined;
130
- }
131
- finally {
132
- if (captureSnapshot && !this._javaScriptBlocked())
133
- await tab.captureSnapshot();
134
- }
135
- const result = [];
136
- result.push(`- Ran Playwright code:
137
- \`\`\`js
138
- ${code.join('\n')}
139
- \`\`\`
140
- `);
141
- if (this.modalStates().length) {
142
- result.push(...this.modalStatesMarkdown());
143
- return {
144
- content: [{
145
- type: 'text',
146
- text: result.join('\n'),
147
- }],
148
- };
149
- }
150
- if (this._downloads.length) {
151
- result.push('', '### Downloads');
152
- for (const entry of this._downloads) {
153
- if (entry.finished)
154
- result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
155
- else
156
- result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
157
- }
158
- result.push('');
159
- }
160
- if (this.tabs().length > 1)
161
- result.push(await this.listTabsMarkdown(), '');
162
- if (this.tabs().length > 1)
163
- result.push('### Current tab');
164
- result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
165
- if (captureSnapshot && tab.hasSnapshot())
166
- result.push(tab.snapshotOrDie().text());
167
- const content = actionResult?.content ?? [];
168
- return {
169
- content: [
170
- ...content,
171
- {
172
- type: 'text',
173
- text: result.join('\n'),
174
- }
175
- ],
176
- };
177
- }
178
- async waitForTimeout(time) {
179
- if (!this._currentTab || this._javaScriptBlocked()) {
180
- await new Promise(f => setTimeout(f, time));
181
- return;
182
- }
183
- await callOnPageNoTrace(this._currentTab.page, page => {
184
- return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
185
- });
186
- }
187
- async _raceAgainstModalDialogs(action) {
188
- this._pendingAction = {
189
- dialogShown: new ManualPromise(),
190
- };
191
- let result;
192
- try {
193
- await Promise.race([
194
- action().then(r => result = r),
195
- this._pendingAction.dialogShown,
196
- ]);
197
- }
198
- finally {
199
- this._pendingAction = undefined;
200
- }
201
- return result;
202
- }
203
- _javaScriptBlocked() {
204
- return this._modalStates.some(state => state.type === 'dialog');
205
- }
206
- dialogShown(tab, dialog) {
207
- this.setModalState({
208
- type: 'dialog',
209
- description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
210
- dialog,
211
- }, tab);
212
- this._pendingAction?.dialogShown.resolve();
213
- }
214
- async downloadStarted(tab, download) {
215
- const entry = {
216
- download,
217
- finished: false,
218
- outputFile: await outputFile(this.config, download.suggestedFilename())
219
- };
220
- this._downloads.push(entry);
221
- await download.saveAs(entry.outputFile);
222
- entry.finished = true;
93
+ const tab = index === undefined ? this._currentTab : this._tabs[index];
94
+ if (!tab)
95
+ throw new Error(`Tab ${index} not found`);
96
+ const url = tab.page.url();
97
+ await tab.page.close();
98
+ return url;
223
99
  }
224
100
  _onPageCreated(page) {
225
101
  const tab = new Tab(this, page, tab => this._onPageClosed(tab));
@@ -228,7 +104,6 @@ ${code.join('\n')}
228
104
  this._currentTab = tab;
229
105
  }
230
106
  _onPageClosed(tab) {
231
- this._modalStates = this._modalStates.filter(state => state.tab !== tab);
232
107
  const index = this._tabs.indexOf(tab);
233
108
  if (index === -1)
234
109
  return;
@@ -236,9 +111,15 @@ ${code.join('\n')}
236
111
  if (this._currentTab === tab)
237
112
  this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
238
113
  if (!this._tabs.length)
239
- void this.close();
114
+ void this.closeBrowserContext();
115
+ }
116
+ async closeBrowserContext() {
117
+ if (!this._closeBrowserContextPromise)
118
+ this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(logUnhandledError);
119
+ await this._closeBrowserContextPromise;
120
+ this._closeBrowserContextPromise = undefined;
240
121
  }
241
- async close() {
122
+ async _closeBrowserContextImpl() {
242
123
  if (!this._browserContextPromise)
243
124
  return;
244
125
  testDebug('close context');
@@ -250,6 +131,10 @@ ${code.join('\n')}
250
131
  await close();
251
132
  });
252
133
  }
134
+ async dispose() {
135
+ await this.closeBrowserContext();
136
+ Context._allContexts.delete(this);
137
+ }
253
138
  async _setupRequestInterception(context) {
254
139
  if (this.config.network?.allowedOrigins?.length) {
255
140
  await context.route('**', route => route.abort('blockedbyclient'));
@@ -271,8 +156,10 @@ ${code.join('\n')}
271
156
  return this._browserContextPromise;
272
157
  }
273
158
  async _setupBrowserContext() {
159
+ if (this._closeBrowserContextPromise)
160
+ throw new Error('Another browser context is being closed.');
274
161
  // TODO: move to the browser context factory to make it based on isolation mode.
275
- const result = await this._browserContextFactory.createContext();
162
+ const result = await this._browserContextFactory.createContext(this.clientVersion);
276
163
  const { browserContext } = result;
277
164
  await this._setupRequestInterception(browserContext);
278
165
  for (const page of browserContext.pages())