@playwright/mcp 0.0.9 → 0.0.13
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 +76 -74
- package/index.d.ts +7 -0
- package/lib/context.js +223 -106
- package/lib/index.js +30 -65
- package/lib/javascript.js +54 -0
- package/lib/program.js +8 -3
- package/lib/resources/console.js +1 -1
- package/lib/tools/common.js +29 -121
- package/lib/tools/files.js +47 -0
- package/lib/tools/install.js +59 -0
- package/lib/tools/keyboard.js +49 -0
- package/lib/tools/navigate.js +91 -0
- package/lib/tools/pdf.js +48 -0
- package/lib/tools/{screenshot.js → screen.js} +63 -30
- package/lib/tools/snapshot.js +121 -27
- package/lib/tools/tabs.js +116 -0
- package/lib/tools/utils.js +1 -26
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
|
|
|
43
43
|
const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
|
|
44
44
|
-->
|
|
45
45
|
|
|
46
|
-
[<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-y%2522%252C%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
|
|
46
|
+
[<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-y%2522%252C%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-y%2522%252C%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
|
|
47
47
|
|
|
48
48
|
Alternatively, you can install the Playwright MCP server using the VS Code CLI:
|
|
49
49
|
|
|
@@ -68,6 +68,7 @@ The Playwright MCP server supports the following command-line options:
|
|
|
68
68
|
- Chrome channels: `chrome-beta`, `chrome-canary`, `chrome-dev`
|
|
69
69
|
- Edge channels: `msedge-beta`, `msedge-canary`, `msedge-dev`
|
|
70
70
|
- Default: `chrome`
|
|
71
|
+
- `--caps <caps>`: Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.
|
|
71
72
|
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
|
|
72
73
|
- `--executable-path <path>`: Path to the browser executable
|
|
73
74
|
- `--headless`: Run browser in headless mode (headed by default)
|
|
@@ -167,22 +168,7 @@ transport = new SSEServerTransport("/messages", res);
|
|
|
167
168
|
server.connect(transport);
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
### Snapshot
|
|
171
|
-
|
|
172
|
-
The Playwright MCP provides a set of tools for browser automation. Here are all available tools:
|
|
173
|
-
|
|
174
|
-
- **browser_navigate**
|
|
175
|
-
- Description: Navigate to a URL
|
|
176
|
-
- Parameters:
|
|
177
|
-
- `url` (string): The URL to navigate to
|
|
178
|
-
|
|
179
|
-
- **browser_go_back**
|
|
180
|
-
- Description: Go back to the previous page
|
|
181
|
-
- Parameters: None
|
|
182
|
-
|
|
183
|
-
- **browser_go_forward**
|
|
184
|
-
- Description: Go forward to the next page
|
|
185
|
-
- Parameters: None
|
|
171
|
+
### Snapshot-based Interactions
|
|
186
172
|
|
|
187
173
|
- **browser_click**
|
|
188
174
|
- Description: Perform click on a web page
|
|
@@ -210,109 +196,121 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
|
|
|
210
196
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
211
197
|
- `ref` (string): Exact target element reference from the page snapshot
|
|
212
198
|
- `text` (string): Text to type into the element
|
|
213
|
-
- `submit` (boolean): Whether to submit entered text (press Enter after)
|
|
199
|
+
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
|
200
|
+
- `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
|
|
214
201
|
|
|
215
202
|
- **browser_select_option**
|
|
216
|
-
- Description: Select option in a dropdown
|
|
203
|
+
- Description: Select an option in a dropdown
|
|
217
204
|
- Parameters:
|
|
218
205
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
219
206
|
- `ref` (string): Exact target element reference from the page snapshot
|
|
220
|
-
- `values` (array): Array of values to select in the dropdown.
|
|
207
|
+
- `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
|
|
221
208
|
|
|
222
|
-
- **
|
|
223
|
-
- Description:
|
|
209
|
+
- **browser_snapshot**
|
|
210
|
+
- Description: Capture accessibility snapshot of the current page, this is better than screenshot
|
|
211
|
+
- Parameters: None
|
|
212
|
+
|
|
213
|
+
- **browser_take_screenshot**
|
|
214
|
+
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
|
|
224
215
|
- Parameters:
|
|
225
|
-
- `
|
|
216
|
+
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
|
|
226
217
|
|
|
227
|
-
-
|
|
228
|
-
|
|
218
|
+
### Vision-based Interactions
|
|
219
|
+
|
|
220
|
+
- **browser_screen_move_mouse**
|
|
221
|
+
- Description: Move mouse to a given position
|
|
229
222
|
- Parameters:
|
|
230
|
-
- `
|
|
223
|
+
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
224
|
+
- `x` (number): X coordinate
|
|
225
|
+
- `y` (number): Y coordinate
|
|
231
226
|
|
|
232
|
-
- **
|
|
233
|
-
- Description:
|
|
227
|
+
- **browser_screen_capture**
|
|
228
|
+
- Description: Take a screenshot of the current page
|
|
234
229
|
- Parameters: None
|
|
235
230
|
|
|
236
|
-
- **
|
|
237
|
-
- Description:
|
|
238
|
-
- Parameters:
|
|
231
|
+
- **browser_screen_click**
|
|
232
|
+
- Description: Click left mouse button
|
|
233
|
+
- Parameters:
|
|
234
|
+
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
235
|
+
- `x` (number): X coordinate
|
|
236
|
+
- `y` (number): Y coordinate
|
|
239
237
|
|
|
240
|
-
- **
|
|
241
|
-
- Description:
|
|
238
|
+
- **browser_screen_drag**
|
|
239
|
+
- Description: Drag left mouse button
|
|
242
240
|
- Parameters:
|
|
243
|
-
- `
|
|
241
|
+
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
242
|
+
- `startX` (number): Start X coordinate
|
|
243
|
+
- `startY` (number): Start Y coordinate
|
|
244
|
+
- `endX` (number): End X coordinate
|
|
245
|
+
- `endY` (number): End Y coordinate
|
|
244
246
|
|
|
245
|
-
- **
|
|
246
|
-
- Description:
|
|
247
|
+
- **browser_screen_type**
|
|
248
|
+
- Description: Type text
|
|
247
249
|
- Parameters:
|
|
248
|
-
- `
|
|
250
|
+
- `text` (string): Text to type
|
|
251
|
+
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
|
249
252
|
|
|
250
|
-
- **
|
|
251
|
-
- Description:
|
|
253
|
+
- **browser_press_key**
|
|
254
|
+
- Description: Press a key on the keyboard
|
|
255
|
+
- Parameters:
|
|
256
|
+
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
|
257
|
+
|
|
258
|
+
### Tab Management
|
|
259
|
+
|
|
260
|
+
- **browser_tab_list**
|
|
261
|
+
- Description: List browser tabs
|
|
252
262
|
- Parameters: None
|
|
253
263
|
|
|
264
|
+
- **browser_tab_new**
|
|
265
|
+
- Description: Open a new tab
|
|
266
|
+
- Parameters:
|
|
267
|
+
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
|
254
268
|
|
|
255
|
-
|
|
269
|
+
- **browser_tab_select**
|
|
270
|
+
- Description: Select a tab by index
|
|
271
|
+
- Parameters:
|
|
272
|
+
- `index` (number): The index of the tab to select
|
|
273
|
+
|
|
274
|
+
- **browser_tab_close**
|
|
275
|
+
- Description: Close a tab
|
|
276
|
+
- Parameters:
|
|
277
|
+
- `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
|
|
256
278
|
|
|
257
|
-
|
|
279
|
+
### Navigation
|
|
258
280
|
|
|
259
281
|
- **browser_navigate**
|
|
260
282
|
- Description: Navigate to a URL
|
|
261
283
|
- Parameters:
|
|
262
284
|
- `url` (string): The URL to navigate to
|
|
263
285
|
|
|
264
|
-
- **
|
|
286
|
+
- **browser_navigate_back**
|
|
265
287
|
- Description: Go back to the previous page
|
|
266
288
|
- Parameters: None
|
|
267
289
|
|
|
268
|
-
- **
|
|
290
|
+
- **browser_navigate_forward**
|
|
269
291
|
- Description: Go forward to the next page
|
|
270
292
|
- Parameters: None
|
|
271
293
|
|
|
272
|
-
|
|
273
|
-
- Description: Capture screenshot of the current page
|
|
274
|
-
- Parameters: None
|
|
275
|
-
|
|
276
|
-
- **browser_move_mouse**
|
|
277
|
-
- Description: Move mouse to specified coordinates
|
|
278
|
-
- Parameters:
|
|
279
|
-
- `x` (number): X coordinate
|
|
280
|
-
- `y` (number): Y coordinate
|
|
281
|
-
|
|
282
|
-
- **browser_click**
|
|
283
|
-
- Description: Click at specified coordinates
|
|
284
|
-
- Parameters:
|
|
285
|
-
- `x` (number): X coordinate to click at
|
|
286
|
-
- `y` (number): Y coordinate to click at
|
|
287
|
-
|
|
288
|
-
- **browser_drag**
|
|
289
|
-
- Description: Perform drag and drop operation
|
|
290
|
-
- Parameters:
|
|
291
|
-
- `startX` (number): Start X coordinate
|
|
292
|
-
- `startY` (number): Start Y coordinate
|
|
293
|
-
- `endX` (number): End X coordinate
|
|
294
|
-
- `endY` (number): End Y coordinate
|
|
295
|
-
|
|
296
|
-
- **browser_type**
|
|
297
|
-
- Description: Type text at specified coordinates
|
|
298
|
-
- Parameters:
|
|
299
|
-
- `text` (string): Text to type
|
|
300
|
-
- `submit` (boolean): Whether to submit entered text (press Enter after)
|
|
294
|
+
### Keyboard
|
|
301
295
|
|
|
302
296
|
- **browser_press_key**
|
|
303
297
|
- Description: Press a key on the keyboard
|
|
304
298
|
- Parameters:
|
|
305
299
|
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
|
306
300
|
|
|
307
|
-
|
|
301
|
+
### Files and Media
|
|
302
|
+
|
|
303
|
+
- **browser_file_upload**
|
|
308
304
|
- Description: Choose one or multiple files to upload
|
|
309
305
|
- Parameters:
|
|
310
306
|
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
|
311
307
|
|
|
312
|
-
- **
|
|
308
|
+
- **browser_pdf_save**
|
|
313
309
|
- Description: Save page as PDF
|
|
314
310
|
- Parameters: None
|
|
315
311
|
|
|
312
|
+
### Utilities
|
|
313
|
+
|
|
316
314
|
- **browser_wait**
|
|
317
315
|
- Description: Wait for a specified time in seconds
|
|
318
316
|
- Parameters:
|
|
@@ -321,3 +319,7 @@ Vision Mode provides tools for visual-based interactions using screenshots. Here
|
|
|
321
319
|
- **browser_close**
|
|
322
320
|
- Description: Close the page
|
|
323
321
|
- Parameters: None
|
|
322
|
+
|
|
323
|
+
- **browser_install**
|
|
324
|
+
- Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed.
|
|
325
|
+
- Parameters: None
|
package/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import type { LaunchOptions } from 'playwright';
|
|
19
19
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
20
|
|
|
21
|
+
type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
|
22
|
+
|
|
21
23
|
type Options = {
|
|
22
24
|
/**
|
|
23
25
|
* Path to the user data directory.
|
|
@@ -35,6 +37,11 @@ type Options = {
|
|
|
35
37
|
* @default false
|
|
36
38
|
*/
|
|
37
39
|
vision?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capabilities to enable.
|
|
43
|
+
*/
|
|
44
|
+
capabilities?: ToolCapability[];
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
export function createServer(options?: Options): Server;
|
package/lib/context.js
CHANGED
|
@@ -52,121 +52,120 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
54
|
exports.Context = void 0;
|
|
55
|
-
|
|
56
|
-
const path_1 = __importDefault(require("path"));
|
|
55
|
+
exports.generateLocator = generateLocator;
|
|
57
56
|
const playwright = __importStar(require("playwright"));
|
|
58
57
|
const yaml_1 = __importDefault(require("yaml"));
|
|
58
|
+
const utils_1 = require("./tools/utils");
|
|
59
59
|
class Context {
|
|
60
|
-
|
|
60
|
+
options;
|
|
61
61
|
_browser;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
_fileChooser;
|
|
66
|
-
_lastSnapshotFrames = [];
|
|
62
|
+
_browserContext;
|
|
63
|
+
_tabs = [];
|
|
64
|
+
_currentTab;
|
|
67
65
|
constructor(options) {
|
|
68
|
-
this.
|
|
69
|
-
}
|
|
70
|
-
async createPage() {
|
|
71
|
-
if (this._createPagePromise)
|
|
72
|
-
return this._createPagePromise;
|
|
73
|
-
this._createPagePromise = (async () => {
|
|
74
|
-
const { browser, page } = await this._createPage();
|
|
75
|
-
page.on('console', event => this._console.push(event));
|
|
76
|
-
page.on('framenavigated', frame => {
|
|
77
|
-
if (!frame.parentFrame())
|
|
78
|
-
this._console.length = 0;
|
|
79
|
-
});
|
|
80
|
-
page.on('close', () => this._onPageClose());
|
|
81
|
-
page.on('filechooser', chooser => this._fileChooser = chooser);
|
|
82
|
-
page.setDefaultNavigationTimeout(60000);
|
|
83
|
-
page.setDefaultTimeout(5000);
|
|
84
|
-
this._page = page;
|
|
85
|
-
this._browser = browser;
|
|
86
|
-
return page;
|
|
87
|
-
})();
|
|
88
|
-
return this._createPagePromise;
|
|
89
|
-
}
|
|
90
|
-
_onPageClose() {
|
|
91
|
-
const browser = this._browser;
|
|
92
|
-
const page = this._page;
|
|
93
|
-
void page?.context()?.close().then(() => browser?.close()).catch(() => { });
|
|
94
|
-
this._createPagePromise = undefined;
|
|
95
|
-
this._browser = undefined;
|
|
96
|
-
this._page = undefined;
|
|
97
|
-
this._fileChooser = undefined;
|
|
98
|
-
this._console.length = 0;
|
|
66
|
+
this.options = options;
|
|
99
67
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const cli = path_1.default.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
|
103
|
-
const child = (0, child_process_1.fork)(cli, ['install', channel], {
|
|
104
|
-
stdio: 'pipe',
|
|
105
|
-
});
|
|
106
|
-
const output = [];
|
|
107
|
-
child.stdout?.on('data', data => output.push(data.toString()));
|
|
108
|
-
child.stderr?.on('data', data => output.push(data.toString()));
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
child.on('close', code => {
|
|
111
|
-
if (code === 0)
|
|
112
|
-
resolve(channel);
|
|
113
|
-
else
|
|
114
|
-
reject(new Error(`Failed to install browser: ${output.join('')}`));
|
|
115
|
-
});
|
|
116
|
-
});
|
|
68
|
+
tabs() {
|
|
69
|
+
return this._tabs;
|
|
117
70
|
}
|
|
118
|
-
|
|
119
|
-
if (!this.
|
|
120
|
-
throw new Error('
|
|
121
|
-
return this.
|
|
71
|
+
currentTab() {
|
|
72
|
+
if (!this._currentTab)
|
|
73
|
+
throw new Error('No current snapshot available. Capture a snapshot of navigate to a new location first.');
|
|
74
|
+
return this._currentTab;
|
|
122
75
|
}
|
|
123
|
-
async
|
|
124
|
-
|
|
76
|
+
async newTab() {
|
|
77
|
+
const browserContext = await this._ensureBrowserContext();
|
|
78
|
+
const page = await browserContext.newPage();
|
|
79
|
+
this._currentTab = this._tabs.find(t => t.page === page);
|
|
80
|
+
return this._currentTab;
|
|
125
81
|
}
|
|
126
|
-
async
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await this._page.close();
|
|
82
|
+
async selectTab(index) {
|
|
83
|
+
this._currentTab = this._tabs[index - 1];
|
|
84
|
+
await this._currentTab.page.bringToFront();
|
|
130
85
|
}
|
|
131
|
-
async
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.
|
|
86
|
+
async ensureTab() {
|
|
87
|
+
const context = await this._ensureBrowserContext();
|
|
88
|
+
if (!this._currentTab)
|
|
89
|
+
await context.newPage();
|
|
90
|
+
return this._currentTab;
|
|
136
91
|
}
|
|
137
|
-
|
|
138
|
-
|
|
92
|
+
async listTabs() {
|
|
93
|
+
if (!this._tabs.length)
|
|
94
|
+
return '### No tabs open';
|
|
95
|
+
const lines = ['### Open tabs'];
|
|
96
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
97
|
+
const tab = this._tabs[i];
|
|
98
|
+
const title = await tab.page.title();
|
|
99
|
+
const url = tab.page.url();
|
|
100
|
+
const current = tab === this._currentTab ? ' (current)' : '';
|
|
101
|
+
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
139
104
|
}
|
|
140
|
-
|
|
141
|
-
this.
|
|
105
|
+
async closeTab(index) {
|
|
106
|
+
const tab = index === undefined ? this.currentTab() : this._tabs[index - 1];
|
|
107
|
+
await tab.page.close();
|
|
108
|
+
return await this.listTabs();
|
|
109
|
+
}
|
|
110
|
+
_onPageCreated(page) {
|
|
111
|
+
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
112
|
+
this._tabs.push(tab);
|
|
113
|
+
if (!this._currentTab)
|
|
114
|
+
this._currentTab = tab;
|
|
115
|
+
}
|
|
116
|
+
_onPageClosed(tab) {
|
|
117
|
+
const index = this._tabs.indexOf(tab);
|
|
118
|
+
if (index === -1)
|
|
119
|
+
return;
|
|
120
|
+
this._tabs.splice(index, 1);
|
|
121
|
+
if (this._currentTab === tab)
|
|
122
|
+
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
123
|
+
const browser = this._browser;
|
|
124
|
+
if (this._browserContext && !this._tabs.length) {
|
|
125
|
+
void this._browserContext.close().then(() => browser?.close()).catch(() => { });
|
|
126
|
+
this._browser = undefined;
|
|
127
|
+
this._browserContext = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async close() {
|
|
131
|
+
if (!this._browserContext)
|
|
132
|
+
return;
|
|
133
|
+
await this._browserContext.close();
|
|
134
|
+
}
|
|
135
|
+
async _ensureBrowserContext() {
|
|
136
|
+
if (!this._browserContext) {
|
|
137
|
+
const context = await this._createBrowserContext();
|
|
138
|
+
this._browser = context.browser;
|
|
139
|
+
this._browserContext = context.browserContext;
|
|
140
|
+
for (const page of this._browserContext.pages())
|
|
141
|
+
this._onPageCreated(page);
|
|
142
|
+
this._browserContext.on('page', page => this._onPageCreated(page));
|
|
143
|
+
}
|
|
144
|
+
return this._browserContext;
|
|
142
145
|
}
|
|
143
|
-
async
|
|
144
|
-
if (this.
|
|
145
|
-
const url = new URL(this.
|
|
146
|
-
if (this.
|
|
147
|
-
url.searchParams.set('browser', this.
|
|
148
|
-
if (this.
|
|
149
|
-
url.searchParams.set('launch-options', JSON.stringify(this.
|
|
150
|
-
const browser = await playwright[this.
|
|
151
|
-
const
|
|
152
|
-
return { browser,
|
|
146
|
+
async _createBrowserContext() {
|
|
147
|
+
if (this.options.remoteEndpoint) {
|
|
148
|
+
const url = new URL(this.options.remoteEndpoint);
|
|
149
|
+
if (this.options.browserName)
|
|
150
|
+
url.searchParams.set('browser', this.options.browserName);
|
|
151
|
+
if (this.options.launchOptions)
|
|
152
|
+
url.searchParams.set('launch-options', JSON.stringify(this.options.launchOptions));
|
|
153
|
+
const browser = await playwright[this.options.browserName ?? 'chromium'].connect(String(url));
|
|
154
|
+
const browserContext = await browser.newContext();
|
|
155
|
+
return { browser, browserContext };
|
|
153
156
|
}
|
|
154
|
-
if (this.
|
|
155
|
-
const browser = await playwright.chromium.connectOverCDP(this.
|
|
157
|
+
if (this.options.cdpEndpoint) {
|
|
158
|
+
const browser = await playwright.chromium.connectOverCDP(this.options.cdpEndpoint);
|
|
156
159
|
const browserContext = browser.contexts()[0];
|
|
157
|
-
|
|
158
|
-
if (!page)
|
|
159
|
-
page = await browserContext.newPage();
|
|
160
|
-
return { browser, page };
|
|
160
|
+
return { browser, browserContext };
|
|
161
161
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
return { page };
|
|
162
|
+
const browserContext = await this._launchPersistentContext();
|
|
163
|
+
return { browserContext };
|
|
165
164
|
}
|
|
166
165
|
async _launchPersistentContext() {
|
|
167
166
|
try {
|
|
168
|
-
const browserType = this.
|
|
169
|
-
return await browserType.launchPersistentContext(this.
|
|
167
|
+
const browserType = this.options.browserName ? playwright[this.options.browserName] : playwright.chromium;
|
|
168
|
+
return await browserType.launchPersistentContext(this.options.userDataDir, this.options.launchOptions);
|
|
170
169
|
}
|
|
171
170
|
catch (error) {
|
|
172
171
|
if (error.message.includes('Executable doesn\'t exist'))
|
|
@@ -174,13 +173,129 @@ class Context {
|
|
|
174
173
|
throw error;
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
}
|
|
177
|
+
exports.Context = Context;
|
|
178
|
+
class Tab {
|
|
179
|
+
context;
|
|
180
|
+
page;
|
|
181
|
+
_console = [];
|
|
182
|
+
_fileChooser;
|
|
183
|
+
_snapshot;
|
|
184
|
+
_onPageClose;
|
|
185
|
+
constructor(context, page, onPageClose) {
|
|
186
|
+
this.context = context;
|
|
187
|
+
this.page = page;
|
|
188
|
+
this._onPageClose = onPageClose;
|
|
189
|
+
page.on('console', event => this._console.push(event));
|
|
190
|
+
page.on('framenavigated', frame => {
|
|
191
|
+
if (!frame.parentFrame())
|
|
192
|
+
this._console.length = 0;
|
|
193
|
+
});
|
|
194
|
+
page.on('close', () => this._onClose());
|
|
195
|
+
page.on('filechooser', chooser => this._fileChooser = chooser);
|
|
196
|
+
page.setDefaultNavigationTimeout(60000);
|
|
197
|
+
page.setDefaultTimeout(5000);
|
|
198
|
+
}
|
|
199
|
+
_onClose() {
|
|
200
|
+
this._fileChooser = undefined;
|
|
201
|
+
this._console.length = 0;
|
|
202
|
+
this._onPageClose(this);
|
|
203
|
+
}
|
|
204
|
+
async navigate(url) {
|
|
205
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
206
|
+
// Cap load event to 5 seconds, the page is operational at this point.
|
|
207
|
+
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
208
|
+
}
|
|
209
|
+
async run(callback, options) {
|
|
210
|
+
let runResult;
|
|
211
|
+
try {
|
|
212
|
+
if (!options?.noClearFileChooser)
|
|
213
|
+
this._fileChooser = undefined;
|
|
214
|
+
if (options?.waitForCompletion)
|
|
215
|
+
runResult = await (0, utils_1.waitForCompletion)(this.page, () => callback(this)) ?? undefined;
|
|
216
|
+
else
|
|
217
|
+
runResult = await callback(this) ?? undefined;
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (options?.captureSnapshot)
|
|
221
|
+
this._snapshot = await PageSnapshot.create(this.page);
|
|
222
|
+
}
|
|
223
|
+
const result = [];
|
|
224
|
+
result.push(`- Ran code:
|
|
225
|
+
\`\`\`js
|
|
226
|
+
${runResult.code.join('\n')}
|
|
227
|
+
\`\`\`
|
|
228
|
+
`);
|
|
229
|
+
if (this.context.tabs().length > 1)
|
|
230
|
+
result.push(await this.context.listTabs(), '');
|
|
231
|
+
if (this._snapshot) {
|
|
232
|
+
if (this.context.tabs().length > 1)
|
|
233
|
+
result.push('### Current tab');
|
|
234
|
+
result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser }));
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
content: [{
|
|
238
|
+
type: 'text',
|
|
239
|
+
text: result.join('\n'),
|
|
240
|
+
}],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async runAndWait(callback, options) {
|
|
244
|
+
return await this.run(callback, {
|
|
245
|
+
waitForCompletion: true,
|
|
246
|
+
...options,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async runAndWaitWithSnapshot(callback, options) {
|
|
250
|
+
return await this.run(tab => callback(tab.lastSnapshot()), {
|
|
251
|
+
captureSnapshot: true,
|
|
252
|
+
waitForCompletion: true,
|
|
253
|
+
...options,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
lastSnapshot() {
|
|
257
|
+
if (!this._snapshot)
|
|
258
|
+
throw new Error('No snapshot available');
|
|
259
|
+
return this._snapshot;
|
|
181
260
|
}
|
|
182
|
-
async
|
|
183
|
-
|
|
261
|
+
async console() {
|
|
262
|
+
return this._console;
|
|
263
|
+
}
|
|
264
|
+
async submitFileChooser(paths) {
|
|
265
|
+
if (!this._fileChooser)
|
|
266
|
+
throw new Error('No file chooser visible');
|
|
267
|
+
await this._fileChooser.setFiles(paths);
|
|
268
|
+
this._fileChooser = undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
class PageSnapshot {
|
|
272
|
+
_frameLocators = [];
|
|
273
|
+
_text;
|
|
274
|
+
constructor() {
|
|
275
|
+
}
|
|
276
|
+
static async create(page) {
|
|
277
|
+
const snapshot = new PageSnapshot();
|
|
278
|
+
await snapshot._build(page);
|
|
279
|
+
return snapshot;
|
|
280
|
+
}
|
|
281
|
+
text(options) {
|
|
282
|
+
const results = [];
|
|
283
|
+
if (options.hasFileChooser) {
|
|
284
|
+
results.push('- There is a file chooser visible that requires browser_file_upload to be called');
|
|
285
|
+
results.push('');
|
|
286
|
+
}
|
|
287
|
+
results.push(this._text);
|
|
288
|
+
return results.join('\n');
|
|
289
|
+
}
|
|
290
|
+
async _build(page) {
|
|
291
|
+
const yamlDocument = await this._snapshotFrame(page);
|
|
292
|
+
const lines = [];
|
|
293
|
+
lines.push(`- Page URL: ${page.url()}`, `- Page Title: ${await page.title()}`);
|
|
294
|
+
lines.push(`- Page Snapshot`, '```yaml', yamlDocument.toString().trim(), '```', '');
|
|
295
|
+
this._text = lines.join('\n');
|
|
296
|
+
}
|
|
297
|
+
async _snapshotFrame(frame) {
|
|
298
|
+
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
184
299
|
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true });
|
|
185
300
|
const snapshot = yaml_1.default.parseDocument(snapshotString);
|
|
186
301
|
const visit = async (node) => {
|
|
@@ -202,7 +317,7 @@ class Context {
|
|
|
202
317
|
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
203
318
|
if (ref) {
|
|
204
319
|
try {
|
|
205
|
-
const childSnapshot = await this.
|
|
320
|
+
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
|
|
206
321
|
return snapshot.createPair(node.value, childSnapshot);
|
|
207
322
|
}
|
|
208
323
|
catch (error) {
|
|
@@ -218,11 +333,11 @@ class Context {
|
|
|
218
333
|
return snapshot;
|
|
219
334
|
}
|
|
220
335
|
refLocator(ref) {
|
|
221
|
-
let frame = this.
|
|
336
|
+
let frame = this._frameLocators[0];
|
|
222
337
|
const match = ref.match(/^f(\d+)(.*)/);
|
|
223
338
|
if (match) {
|
|
224
339
|
const frameIndex = parseInt(match[1], 10);
|
|
225
|
-
frame = this.
|
|
340
|
+
frame = this._frameLocators[frameIndex];
|
|
226
341
|
ref = match[2];
|
|
227
342
|
}
|
|
228
343
|
if (!frame)
|
|
@@ -230,4 +345,6 @@ class Context {
|
|
|
230
345
|
return frame.locator(`aria-ref=${ref}`);
|
|
231
346
|
}
|
|
232
347
|
}
|
|
233
|
-
|
|
348
|
+
async function generateLocator(locator) {
|
|
349
|
+
return locator._generateLocatorString();
|
|
350
|
+
}
|