@playwright/mcp 0.0.31 → 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.
- package/README.md +25 -4
- package/config.d.ts +5 -0
- package/index.d.ts +1 -6
- package/lib/browserServerBackend.js +54 -0
- package/lib/config.js +2 -1
- package/lib/context.js +48 -171
- package/lib/extension/cdpRelay.js +370 -0
- package/lib/extension/main.js +33 -0
- package/lib/httpServer.js +20 -182
- package/lib/index.js +3 -2
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +141 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +66 -0
- package/lib/loopTools/main.js +49 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/server.js +88 -0
- package/lib/{transport.js → mcp/transport.js} +30 -42
- package/lib/package.js +3 -3
- package/lib/program.js +38 -9
- package/lib/response.js +98 -0
- package/lib/sessionLog.js +70 -0
- package/lib/tab.js +133 -22
- package/lib/tools/common.js +11 -23
- package/lib/tools/console.js +4 -15
- package/lib/tools/dialogs.js +12 -17
- package/lib/tools/evaluate.js +12 -21
- package/lib/tools/files.js +10 -16
- package/lib/tools/install.js +3 -7
- package/lib/tools/keyboard.js +30 -42
- package/lib/tools/mouse.js +27 -50
- package/lib/tools/navigate.js +15 -35
- package/lib/tools/network.js +5 -15
- package/lib/tools/pdf.js +8 -15
- package/lib/tools/screenshot.js +29 -30
- package/lib/tools/snapshot.js +45 -65
- package/lib/tools/tabs.js +10 -41
- package/lib/tools/tool.js +14 -0
- package/lib/tools/utils.js +2 -2
- package/lib/tools/wait.js +3 -6
- package/lib/tools.js +3 -0
- package/package.json +9 -3
- package/lib/connection.js +0 -81
- package/lib/pageSnapshot.js +0 -43
- package/lib/server.js +0 -48
package/README.md
CHANGED
|
@@ -88,6 +88,18 @@ Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/
|
|
|
88
88
|
Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension".
|
|
89
89
|
</details>
|
|
90
90
|
|
|
91
|
+
<details>
|
|
92
|
+
<summary>LM Studio</summary>
|
|
93
|
+
|
|
94
|
+
#### Click the button to install:
|
|
95
|
+
|
|
96
|
+
[](https://lmstudio.ai/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAcGxheXdyaWdodC9tY3BAbGF0ZXN0Il19)
|
|
97
|
+
|
|
98
|
+
#### Or install manually:
|
|
99
|
+
|
|
100
|
+
Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
|
|
101
|
+
</details>
|
|
102
|
+
|
|
91
103
|
<details>
|
|
92
104
|
<summary>Qodo Gen</summary>
|
|
93
105
|
|
|
@@ -99,7 +111,13 @@ Click <code>Save</code>.
|
|
|
99
111
|
<details>
|
|
100
112
|
<summary>VS Code</summary>
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
#### Click the button to install:
|
|
115
|
+
|
|
116
|
+
[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
|
|
117
|
+
|
|
118
|
+
#### Or install manually:
|
|
119
|
+
|
|
120
|
+
Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the Playwright MCP server using the VS Code CLI:
|
|
103
121
|
|
|
104
122
|
```bash
|
|
105
123
|
# For VS Code
|
|
@@ -156,6 +174,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
156
174
|
example ".com,chromium.org,.domain.com"
|
|
157
175
|
--proxy-server <proxy> specify proxy server, for example
|
|
158
176
|
"http://myproxy:3128" or "socks5://myproxy:8080"
|
|
177
|
+
--save-session Whether to save the Playwright MCP session into
|
|
178
|
+
the output directory.
|
|
159
179
|
--save-trace Whether to save the Playwright Trace of the
|
|
160
180
|
session into the output directory.
|
|
161
181
|
--storage-state <path> path to the storage state file for isolated
|
|
@@ -297,19 +317,19 @@ npx @playwright/mcp@latest --config path/to/config.json
|
|
|
297
317
|
### Standalone MCP server
|
|
298
318
|
|
|
299
319
|
When running headed browser on system w/o display or from worker processes of the IDEs,
|
|
300
|
-
run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable
|
|
320
|
+
run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport.
|
|
301
321
|
|
|
302
322
|
```bash
|
|
303
323
|
npx @playwright/mcp@latest --port 8931
|
|
304
324
|
```
|
|
305
325
|
|
|
306
|
-
And then in MCP client config, set the `url` to the
|
|
326
|
+
And then in MCP client config, set the `url` to the HTTP endpoint:
|
|
307
327
|
|
|
308
328
|
```js
|
|
309
329
|
{
|
|
310
330
|
"mcpServers": {
|
|
311
331
|
"playwright": {
|
|
312
|
-
"url": "http://localhost:8931/
|
|
332
|
+
"url": "http://localhost:8931/mcp"
|
|
313
333
|
}
|
|
314
334
|
}
|
|
315
335
|
}
|
|
@@ -528,6 +548,7 @@ http.createServer(async (req, res) => {
|
|
|
528
548
|
- `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
|
|
529
549
|
- `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.
|
|
530
550
|
- `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.
|
|
551
|
+
- `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.
|
|
531
552
|
- Read-only: **true**
|
|
532
553
|
|
|
533
554
|
<!-- NOTE: This has been generated via update-readme.js -->
|
package/config.d.ts
CHANGED
|
@@ -85,6 +85,11 @@ export type Config = {
|
|
|
85
85
|
*/
|
|
86
86
|
capabilities?: ToolCapability[];
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Whether to save the Playwright session into the output directory.
|
|
90
|
+
*/
|
|
91
|
+
saveSession?: boolean;
|
|
92
|
+
|
|
88
93
|
/**
|
|
89
94
|
* Whether to save the Playwright trace of the session into the output directory.
|
|
90
95
|
*/
|
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
|
|
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 {};
|
|
@@ -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
|
@@ -83,7 +83,7 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
83
83
|
headless: cliOptions.headless,
|
|
84
84
|
};
|
|
85
85
|
// --no-sandbox was passed, disable the sandbox
|
|
86
|
-
if (
|
|
86
|
+
if (cliOptions.sandbox === false)
|
|
87
87
|
launchOptions.chromiumSandbox = false;
|
|
88
88
|
if (cliOptions.proxyServer) {
|
|
89
89
|
launchOptions.proxy = {
|
|
@@ -133,6 +133,7 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
133
133
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
134
134
|
blockedOrigins: cliOptions.blockedOrigins,
|
|
135
135
|
},
|
|
136
|
+
saveSession: cliOptions.saveSession,
|
|
136
137
|
saveTrace: cliOptions.saveTrace,
|
|
137
138
|
outputDir: cliOptions.outputDir,
|
|
138
139
|
imageResponses: cliOptions.imageResponses,
|
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 {
|
|
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,46 +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
|
-
|
|
40
|
-
|
|
41
|
-
return false;
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
modalStates() {
|
|
45
|
-
return this._modalStates;
|
|
46
|
-
}
|
|
47
|
-
setModalState(modalState, inTab) {
|
|
48
|
-
this._modalStates.push({ ...modalState, tab: inTab });
|
|
49
|
-
}
|
|
50
|
-
clearModalState(modalState) {
|
|
51
|
-
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
|
52
|
-
}
|
|
53
|
-
modalStatesMarkdown() {
|
|
54
|
-
const result = ['### Modal state'];
|
|
55
|
-
if (this._modalStates.length === 0)
|
|
56
|
-
result.push('- There is no modal state present');
|
|
57
|
-
for (const state of this._modalStates) {
|
|
58
|
-
const tool = this.tools.find(tool => tool.clearsModalState === state.type);
|
|
59
|
-
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
37
|
+
static async disposeAll() {
|
|
38
|
+
await Promise.all([...Context._allContexts].map(context => context.dispose()));
|
|
62
39
|
}
|
|
63
40
|
tabs() {
|
|
64
41
|
return this._tabs;
|
|
65
42
|
}
|
|
43
|
+
currentTab() {
|
|
44
|
+
return this._currentTab;
|
|
45
|
+
}
|
|
66
46
|
currentTabOrDie() {
|
|
67
47
|
if (!this._currentTab)
|
|
68
|
-
throw new Error('No
|
|
48
|
+
throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.');
|
|
69
49
|
return this._currentTab;
|
|
70
50
|
}
|
|
71
51
|
async newTab() {
|
|
@@ -75,8 +55,12 @@ export class Context {
|
|
|
75
55
|
return this._currentTab;
|
|
76
56
|
}
|
|
77
57
|
async selectTab(index) {
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
80
64
|
}
|
|
81
65
|
async ensureTab() {
|
|
82
66
|
const { browserContext } = await this._ensureBrowserContext();
|
|
@@ -84,9 +68,16 @@ export class Context {
|
|
|
84
68
|
await browserContext.newPage();
|
|
85
69
|
return this._currentTab;
|
|
86
70
|
}
|
|
87
|
-
async listTabsMarkdown() {
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
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
|
+
}
|
|
90
81
|
const lines = ['### Open tabs'];
|
|
91
82
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
92
83
|
const tab = this._tabs[i];
|
|
@@ -95,136 +86,16 @@ export class Context {
|
|
|
95
86
|
const current = tab === this._currentTab ? ' (current)' : '';
|
|
96
87
|
lines.push(`- ${i}:${current} [${title}] (${url})`);
|
|
97
88
|
}
|
|
98
|
-
|
|
89
|
+
lines.push('');
|
|
90
|
+
return lines;
|
|
99
91
|
}
|
|
100
92
|
async closeTab(index) {
|
|
101
93
|
const tab = index === undefined ? this._currentTab : this._tabs[index];
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
108
|
-
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
109
|
-
if (resultOverride)
|
|
110
|
-
return resultOverride;
|
|
111
|
-
if (!this._currentTab) {
|
|
112
|
-
return {
|
|
113
|
-
content: [{
|
|
114
|
-
type: 'text',
|
|
115
|
-
text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.',
|
|
116
|
-
}],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const tab = this.currentTabOrDie();
|
|
120
|
-
// TODO: race against modal dialogs to resolve clicks.
|
|
121
|
-
const actionResult = await this._raceAgainstModalDialogs(async () => {
|
|
122
|
-
try {
|
|
123
|
-
if (waitForNetwork)
|
|
124
|
-
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
|
|
125
|
-
else
|
|
126
|
-
return await action?.() ?? undefined;
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
if (captureSnapshot && !this._javaScriptBlocked())
|
|
130
|
-
await tab.captureSnapshot();
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
const result = [];
|
|
134
|
-
result.push(`### Ran Playwright code
|
|
135
|
-
\`\`\`js
|
|
136
|
-
${code.join('\n')}
|
|
137
|
-
\`\`\``);
|
|
138
|
-
if (this.modalStates().length) {
|
|
139
|
-
result.push('', ...this.modalStatesMarkdown());
|
|
140
|
-
return {
|
|
141
|
-
content: [{
|
|
142
|
-
type: 'text',
|
|
143
|
-
text: result.join('\n'),
|
|
144
|
-
}],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
const messages = tab.takeRecentConsoleMessages();
|
|
148
|
-
if (messages.length) {
|
|
149
|
-
result.push('', `### New console messages`);
|
|
150
|
-
for (const message of messages)
|
|
151
|
-
result.push(`- ${trim(message.toString(), 100)}`);
|
|
152
|
-
}
|
|
153
|
-
if (this._downloads.length) {
|
|
154
|
-
result.push('', '### Downloads');
|
|
155
|
-
for (const entry of this._downloads) {
|
|
156
|
-
if (entry.finished)
|
|
157
|
-
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
|
158
|
-
else
|
|
159
|
-
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (captureSnapshot && tab.hasSnapshot()) {
|
|
163
|
-
if (this.tabs().length > 1)
|
|
164
|
-
result.push('', await this.listTabsMarkdown());
|
|
165
|
-
if (this.tabs().length > 1)
|
|
166
|
-
result.push('', '### Current tab');
|
|
167
|
-
else
|
|
168
|
-
result.push('', '### Page state');
|
|
169
|
-
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
|
|
170
|
-
result.push(tab.snapshotOrDie().text());
|
|
171
|
-
}
|
|
172
|
-
const content = actionResult?.content ?? [];
|
|
173
|
-
return {
|
|
174
|
-
content: [
|
|
175
|
-
...content,
|
|
176
|
-
{
|
|
177
|
-
type: 'text',
|
|
178
|
-
text: result.join('\n'),
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
async waitForTimeout(time) {
|
|
184
|
-
if (!this._currentTab || this._javaScriptBlocked()) {
|
|
185
|
-
await new Promise(f => setTimeout(f, time));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
await callOnPageNoTrace(this._currentTab.page, page => {
|
|
189
|
-
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
async _raceAgainstModalDialogs(action) {
|
|
193
|
-
this._pendingAction = {
|
|
194
|
-
dialogShown: new ManualPromise(),
|
|
195
|
-
};
|
|
196
|
-
let result;
|
|
197
|
-
try {
|
|
198
|
-
await Promise.race([
|
|
199
|
-
action().then(r => result = r),
|
|
200
|
-
this._pendingAction.dialogShown,
|
|
201
|
-
]);
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
this._pendingAction = undefined;
|
|
205
|
-
}
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
_javaScriptBlocked() {
|
|
209
|
-
return this._modalStates.some(state => state.type === 'dialog');
|
|
210
|
-
}
|
|
211
|
-
dialogShown(tab, dialog) {
|
|
212
|
-
this.setModalState({
|
|
213
|
-
type: 'dialog',
|
|
214
|
-
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
|
215
|
-
dialog,
|
|
216
|
-
}, tab);
|
|
217
|
-
this._pendingAction?.dialogShown.resolve();
|
|
218
|
-
}
|
|
219
|
-
async downloadStarted(tab, download) {
|
|
220
|
-
const entry = {
|
|
221
|
-
download,
|
|
222
|
-
finished: false,
|
|
223
|
-
outputFile: await outputFile(this.config, download.suggestedFilename())
|
|
224
|
-
};
|
|
225
|
-
this._downloads.push(entry);
|
|
226
|
-
await download.saveAs(entry.outputFile);
|
|
227
|
-
entry.finished = true;
|
|
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;
|
|
228
99
|
}
|
|
229
100
|
_onPageCreated(page) {
|
|
230
101
|
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
@@ -233,7 +104,6 @@ ${code.join('\n')}
|
|
|
233
104
|
this._currentTab = tab;
|
|
234
105
|
}
|
|
235
106
|
_onPageClosed(tab) {
|
|
236
|
-
this._modalStates = this._modalStates.filter(state => state.tab !== tab);
|
|
237
107
|
const index = this._tabs.indexOf(tab);
|
|
238
108
|
if (index === -1)
|
|
239
109
|
return;
|
|
@@ -241,9 +111,15 @@ ${code.join('\n')}
|
|
|
241
111
|
if (this._currentTab === tab)
|
|
242
112
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
243
113
|
if (!this._tabs.length)
|
|
244
|
-
void this.
|
|
114
|
+
void this.closeBrowserContext();
|
|
245
115
|
}
|
|
246
|
-
async
|
|
116
|
+
async closeBrowserContext() {
|
|
117
|
+
if (!this._closeBrowserContextPromise)
|
|
118
|
+
this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(logUnhandledError);
|
|
119
|
+
await this._closeBrowserContextPromise;
|
|
120
|
+
this._closeBrowserContextPromise = undefined;
|
|
121
|
+
}
|
|
122
|
+
async _closeBrowserContextImpl() {
|
|
247
123
|
if (!this._browserContextPromise)
|
|
248
124
|
return;
|
|
249
125
|
testDebug('close context');
|
|
@@ -255,6 +131,10 @@ ${code.join('\n')}
|
|
|
255
131
|
await close();
|
|
256
132
|
});
|
|
257
133
|
}
|
|
134
|
+
async dispose() {
|
|
135
|
+
await this.closeBrowserContext();
|
|
136
|
+
Context._allContexts.delete(this);
|
|
137
|
+
}
|
|
258
138
|
async _setupRequestInterception(context) {
|
|
259
139
|
if (this.config.network?.allowedOrigins?.length) {
|
|
260
140
|
await context.route('**', route => route.abort('blockedbyclient'));
|
|
@@ -276,8 +156,10 @@ ${code.join('\n')}
|
|
|
276
156
|
return this._browserContextPromise;
|
|
277
157
|
}
|
|
278
158
|
async _setupBrowserContext() {
|
|
159
|
+
if (this._closeBrowserContextPromise)
|
|
160
|
+
throw new Error('Another browser context is being closed.');
|
|
279
161
|
// TODO: move to the browser context factory to make it based on isolation mode.
|
|
280
|
-
const result = await this._browserContextFactory.createContext();
|
|
162
|
+
const result = await this._browserContextFactory.createContext(this.clientVersion);
|
|
281
163
|
const { browserContext } = result;
|
|
282
164
|
await this._setupRequestInterception(browserContext);
|
|
283
165
|
for (const page of browserContext.pages())
|
|
@@ -294,8 +176,3 @@ ${code.join('\n')}
|
|
|
294
176
|
return result;
|
|
295
177
|
}
|
|
296
178
|
}
|
|
297
|
-
function trim(text, maxLength) {
|
|
298
|
-
if (text.length <= maxLength)
|
|
299
|
-
return text;
|
|
300
|
-
return text.slice(0, maxLength) + '...';
|
|
301
|
-
}
|