@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.
- package/README.md +180 -320
- package/config.d.ts +5 -14
- package/index.d.ts +1 -6
- package/lib/browserContextFactory.js +3 -35
- package/lib/browserServerBackend.js +54 -0
- package/lib/config.js +64 -7
- package/lib/context.js +50 -163
- 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/log.js +21 -0
- 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 +47 -17
- package/lib/response.js +98 -0
- package/lib/sessionLog.js +70 -0
- package/lib/tab.js +166 -21
- package/lib/tools/common.js +13 -25
- package/lib/tools/console.js +4 -15
- package/lib/tools/dialogs.js +14 -19
- package/lib/tools/evaluate.js +53 -0
- package/lib/tools/files.js +13 -19
- package/lib/tools/install.js +4 -8
- package/lib/tools/keyboard.js +51 -17
- package/lib/tools/mouse.js +99 -0
- package/lib/tools/navigate.js +22 -42
- 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 +49 -109
- package/lib/tools/tabs.js +21 -52
- package/lib/tools/tool.js +14 -0
- package/lib/tools/utils.js +7 -6
- package/lib/tools/wait.js +8 -11
- package/lib/tools.js +15 -26
- package/package.json +12 -5
- package/lib/browserServer.js +0 -151
- package/lib/connection.js +0 -82
- package/lib/pageSnapshot.js +0 -43
- package/lib/server.js +0 -48
- package/lib/tools/testing.js +0 -60
- package/lib/tools/vision.js +0 -189
package/lib/tab.js
CHANGED
|
@@ -13,41 +13,93 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import { callOnPageNoTrace } from './tools/utils.js';
|
|
18
|
-
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
|
18
|
+
import { logUnhandledError } from './log.js';
|
|
19
|
+
import { ManualPromise } from './manualPromise.js';
|
|
20
|
+
import { outputFile } from './config.js';
|
|
21
|
+
export const TabEvents = {
|
|
22
|
+
modalState: 'modalState'
|
|
23
|
+
};
|
|
24
|
+
export class Tab extends EventEmitter {
|
|
19
25
|
context;
|
|
20
26
|
page;
|
|
21
27
|
_consoleMessages = [];
|
|
28
|
+
_recentConsoleMessages = [];
|
|
22
29
|
_requests = new Map();
|
|
23
|
-
_snapshot;
|
|
24
30
|
_onPageClose;
|
|
31
|
+
_modalStates = [];
|
|
32
|
+
_downloads = [];
|
|
25
33
|
constructor(context, page, onPageClose) {
|
|
34
|
+
super();
|
|
26
35
|
this.context = context;
|
|
27
36
|
this.page = page;
|
|
28
37
|
this._onPageClose = onPageClose;
|
|
29
|
-
page.on('console', event => this.
|
|
38
|
+
page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event)));
|
|
39
|
+
page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
|
|
30
40
|
page.on('request', request => this._requests.set(request, null));
|
|
31
41
|
page.on('response', response => this._requests.set(response.request(), response));
|
|
32
42
|
page.on('close', () => this._onClose());
|
|
33
43
|
page.on('filechooser', chooser => {
|
|
34
|
-
this.
|
|
44
|
+
this.setModalState({
|
|
35
45
|
type: 'fileChooser',
|
|
36
46
|
description: 'File chooser',
|
|
37
47
|
fileChooser: chooser,
|
|
38
|
-
}
|
|
48
|
+
});
|
|
39
49
|
});
|
|
40
|
-
page.on('dialog', dialog => this.
|
|
50
|
+
page.on('dialog', dialog => this._dialogShown(dialog));
|
|
41
51
|
page.on('download', download => {
|
|
42
|
-
void this.
|
|
52
|
+
void this._downloadStarted(download);
|
|
43
53
|
});
|
|
44
54
|
page.setDefaultNavigationTimeout(60000);
|
|
45
55
|
page.setDefaultTimeout(5000);
|
|
46
56
|
}
|
|
57
|
+
modalStates() {
|
|
58
|
+
return this._modalStates;
|
|
59
|
+
}
|
|
60
|
+
setModalState(modalState) {
|
|
61
|
+
this._modalStates.push(modalState);
|
|
62
|
+
this.emit(TabEvents.modalState, modalState);
|
|
63
|
+
}
|
|
64
|
+
clearModalState(modalState) {
|
|
65
|
+
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
|
66
|
+
}
|
|
67
|
+
modalStatesMarkdown() {
|
|
68
|
+
const result = ['### Modal state'];
|
|
69
|
+
if (this._modalStates.length === 0)
|
|
70
|
+
result.push('- There is no modal state present');
|
|
71
|
+
for (const state of this._modalStates) {
|
|
72
|
+
const tool = this.context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
|
|
73
|
+
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
_dialogShown(dialog) {
|
|
78
|
+
this.setModalState({
|
|
79
|
+
type: 'dialog',
|
|
80
|
+
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
|
81
|
+
dialog,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async _downloadStarted(download) {
|
|
85
|
+
const entry = {
|
|
86
|
+
download,
|
|
87
|
+
finished: false,
|
|
88
|
+
outputFile: await outputFile(this.context.config, download.suggestedFilename())
|
|
89
|
+
};
|
|
90
|
+
this._downloads.push(entry);
|
|
91
|
+
await download.saveAs(entry.outputFile);
|
|
92
|
+
entry.finished = true;
|
|
93
|
+
}
|
|
47
94
|
_clearCollectedArtifacts() {
|
|
48
95
|
this._consoleMessages.length = 0;
|
|
96
|
+
this._recentConsoleMessages.length = 0;
|
|
49
97
|
this._requests.clear();
|
|
50
98
|
}
|
|
99
|
+
_handleConsoleMessage(message) {
|
|
100
|
+
this._consoleMessages.push(message);
|
|
101
|
+
this._recentConsoleMessages.push(message);
|
|
102
|
+
}
|
|
51
103
|
_onClose() {
|
|
52
104
|
this._clearCollectedArtifacts();
|
|
53
105
|
this._onPageClose(this);
|
|
@@ -56,11 +108,11 @@ export class Tab {
|
|
|
56
108
|
return await callOnPageNoTrace(this.page, page => page.title());
|
|
57
109
|
}
|
|
58
110
|
async waitForLoadState(state, options) {
|
|
59
|
-
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(
|
|
111
|
+
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError));
|
|
60
112
|
}
|
|
61
113
|
async navigate(url) {
|
|
62
114
|
this._clearCollectedArtifacts();
|
|
63
|
-
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(
|
|
115
|
+
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError));
|
|
64
116
|
try {
|
|
65
117
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
66
118
|
}
|
|
@@ -73,29 +125,122 @@ export class Tab {
|
|
|
73
125
|
// on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
|
|
74
126
|
const download = await Promise.race([
|
|
75
127
|
downloadEvent,
|
|
76
|
-
new Promise(resolve => setTimeout(resolve,
|
|
128
|
+
new Promise(resolve => setTimeout(resolve, 3000)),
|
|
77
129
|
]);
|
|
78
130
|
if (!download)
|
|
79
131
|
throw e;
|
|
132
|
+
// Make sure other "download" listeners are notified first.
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
134
|
+
return;
|
|
80
135
|
}
|
|
81
136
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
82
137
|
await this.waitForLoadState('load', { timeout: 5000 });
|
|
83
138
|
}
|
|
84
|
-
hasSnapshot() {
|
|
85
|
-
return !!this._snapshot;
|
|
86
|
-
}
|
|
87
|
-
snapshotOrDie() {
|
|
88
|
-
if (!this._snapshot)
|
|
89
|
-
throw new Error('No snapshot available');
|
|
90
|
-
return this._snapshot;
|
|
91
|
-
}
|
|
92
139
|
consoleMessages() {
|
|
93
140
|
return this._consoleMessages;
|
|
94
141
|
}
|
|
95
142
|
requests() {
|
|
96
143
|
return this._requests;
|
|
97
144
|
}
|
|
145
|
+
_takeRecentConsoleMarkdown() {
|
|
146
|
+
if (!this._recentConsoleMessages.length)
|
|
147
|
+
return [];
|
|
148
|
+
const result = this._recentConsoleMessages.map(message => {
|
|
149
|
+
return `- ${trim(message.toString(), 100)}`;
|
|
150
|
+
});
|
|
151
|
+
return [`### New console messages`, ...result, ''];
|
|
152
|
+
}
|
|
153
|
+
_listDownloadsMarkdown() {
|
|
154
|
+
if (!this._downloads.length)
|
|
155
|
+
return [];
|
|
156
|
+
const result = ['### Downloads'];
|
|
157
|
+
for (const entry of this._downloads) {
|
|
158
|
+
if (entry.finished)
|
|
159
|
+
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
|
160
|
+
else
|
|
161
|
+
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
162
|
+
}
|
|
163
|
+
result.push('');
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
98
166
|
async captureSnapshot() {
|
|
99
|
-
|
|
167
|
+
const result = [];
|
|
168
|
+
if (this.modalStates().length) {
|
|
169
|
+
result.push(...this.modalStatesMarkdown());
|
|
170
|
+
return result.join('\n');
|
|
171
|
+
}
|
|
172
|
+
result.push(...this._takeRecentConsoleMarkdown());
|
|
173
|
+
result.push(...this._listDownloadsMarkdown());
|
|
174
|
+
await this._raceAgainstModalStates(async () => {
|
|
175
|
+
const snapshot = await this.page._snapshotForAI();
|
|
176
|
+
result.push(`### Page state`, `- Page URL: ${this.page.url()}`, `- Page Title: ${await this.page.title()}`, `- Page Snapshot:`, '```yaml', snapshot, '```');
|
|
177
|
+
});
|
|
178
|
+
return result.join('\n');
|
|
179
|
+
}
|
|
180
|
+
_javaScriptBlocked() {
|
|
181
|
+
return this._modalStates.some(state => state.type === 'dialog');
|
|
182
|
+
}
|
|
183
|
+
async _raceAgainstModalStates(action) {
|
|
184
|
+
if (this.modalStates().length)
|
|
185
|
+
return this.modalStates()[0];
|
|
186
|
+
const promise = new ManualPromise();
|
|
187
|
+
const listener = (modalState) => promise.resolve(modalState);
|
|
188
|
+
this.once(TabEvents.modalState, listener);
|
|
189
|
+
return await Promise.race([
|
|
190
|
+
action().then(() => {
|
|
191
|
+
this.off(TabEvents.modalState, listener);
|
|
192
|
+
return undefined;
|
|
193
|
+
}),
|
|
194
|
+
promise,
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
async waitForCompletion(callback) {
|
|
198
|
+
await this._raceAgainstModalStates(() => waitForCompletion(this, callback));
|
|
199
|
+
}
|
|
200
|
+
async refLocator(params) {
|
|
201
|
+
return (await this.refLocators([params]))[0];
|
|
202
|
+
}
|
|
203
|
+
async refLocators(params) {
|
|
204
|
+
const snapshot = await this.page._snapshotForAI();
|
|
205
|
+
return params.map(param => {
|
|
206
|
+
if (!snapshot.includes(`[ref=${param.ref}]`))
|
|
207
|
+
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
|
|
208
|
+
return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
|
|
209
|
+
});
|
|
100
210
|
}
|
|
211
|
+
async waitForTimeout(time) {
|
|
212
|
+
if (this._javaScriptBlocked()) {
|
|
213
|
+
await new Promise(f => setTimeout(f, time));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await callOnPageNoTrace(this.page, page => {
|
|
217
|
+
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function messageToConsoleMessage(message) {
|
|
222
|
+
return {
|
|
223
|
+
type: message.type(),
|
|
224
|
+
text: message.text(),
|
|
225
|
+
toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function pageErrorToConsoleMessage(errorOrValue) {
|
|
229
|
+
if (errorOrValue instanceof Error) {
|
|
230
|
+
return {
|
|
231
|
+
type: undefined,
|
|
232
|
+
text: errorOrValue.message,
|
|
233
|
+
toString: () => errorOrValue.stack || errorOrValue.message,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
type: undefined,
|
|
238
|
+
text: String(errorOrValue),
|
|
239
|
+
toString: () => String(errorOrValue),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function trim(text, maxLength) {
|
|
243
|
+
if (text.length <= maxLength)
|
|
244
|
+
return text;
|
|
245
|
+
return text.slice(0, maxLength) + '...';
|
|
101
246
|
}
|
package/lib/tools/common.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import { defineTool } from './tool.js';
|
|
17
|
+
import { defineTabTool, defineTool } from './tool.js';
|
|
18
18
|
const close = defineTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
@@ -24,16 +24,13 @@ const close = defineTool({
|
|
|
24
24
|
inputSchema: z.object({}),
|
|
25
25
|
type: 'readOnly',
|
|
26
26
|
},
|
|
27
|
-
handle: async (context) => {
|
|
28
|
-
await context.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
captureSnapshot: false,
|
|
32
|
-
waitForNetwork: false,
|
|
33
|
-
};
|
|
27
|
+
handle: async (context, params, response) => {
|
|
28
|
+
await context.closeBrowserContext();
|
|
29
|
+
response.setIncludeTabs();
|
|
30
|
+
response.addCode(`await page.close()`);
|
|
34
31
|
},
|
|
35
32
|
});
|
|
36
|
-
const resize =
|
|
33
|
+
const resize = defineTabTool({
|
|
37
34
|
capability: 'core',
|
|
38
35
|
schema: {
|
|
39
36
|
name: 'browser_resize',
|
|
@@ -45,24 +42,15 @@ const resize = captureSnapshot => defineTool({
|
|
|
45
42
|
}),
|
|
46
43
|
type: 'readOnly',
|
|
47
44
|
},
|
|
48
|
-
handle: async (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
|
|
53
|
-
];
|
|
54
|
-
const action = async () => {
|
|
45
|
+
handle: async (tab, params, response) => {
|
|
46
|
+
response.addCode(`// Resize browser window to ${params.width}x${params.height}`);
|
|
47
|
+
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
|
|
48
|
+
await tab.waitForCompletion(async () => {
|
|
55
49
|
await tab.page.setViewportSize({ width: params.width, height: params.height });
|
|
56
|
-
};
|
|
57
|
-
return {
|
|
58
|
-
code,
|
|
59
|
-
action,
|
|
60
|
-
captureSnapshot,
|
|
61
|
-
waitForNetwork: true
|
|
62
|
-
};
|
|
50
|
+
});
|
|
63
51
|
},
|
|
64
52
|
});
|
|
65
|
-
export default
|
|
53
|
+
export default [
|
|
66
54
|
close,
|
|
67
|
-
resize
|
|
55
|
+
resize
|
|
68
56
|
];
|
package/lib/tools/console.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
const console =
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
const console = defineTabTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_console_messages',
|
|
@@ -24,19 +24,8 @@ const console = defineTool({
|
|
|
24
24
|
inputSchema: z.object({}),
|
|
25
25
|
type: 'readOnly',
|
|
26
26
|
},
|
|
27
|
-
handle: async (
|
|
28
|
-
|
|
29
|
-
const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
|
|
30
|
-
return {
|
|
31
|
-
code: [`// <internal code to get console messages>`],
|
|
32
|
-
action: async () => {
|
|
33
|
-
return {
|
|
34
|
-
content: [{ type: 'text', text: log }]
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
captureSnapshot: false,
|
|
38
|
-
waitForNetwork: false,
|
|
39
|
-
};
|
|
27
|
+
handle: async (tab, params, response) => {
|
|
28
|
+
tab.consoleMessages().map(message => response.addResult(message.toString()));
|
|
40
29
|
},
|
|
41
30
|
});
|
|
42
31
|
export default [
|
package/lib/tools/dialogs.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
const handleDialog =
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
const handleDialog = defineTabTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_handle_dialog',
|
|
@@ -27,26 +27,21 @@ const handleDialog = captureSnapshot => defineTool({
|
|
|
27
27
|
}),
|
|
28
28
|
type: 'destructive',
|
|
29
29
|
},
|
|
30
|
-
handle: async (
|
|
31
|
-
|
|
30
|
+
handle: async (tab, params, response) => {
|
|
31
|
+
response.setIncludeSnapshot();
|
|
32
|
+
const dialogState = tab.modalStates().find(state => state.type === 'dialog');
|
|
32
33
|
if (!dialogState)
|
|
33
34
|
throw new Error('No dialog visible');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
];
|
|
42
|
-
return {
|
|
43
|
-
code,
|
|
44
|
-
captureSnapshot,
|
|
45
|
-
waitForNetwork: false,
|
|
46
|
-
};
|
|
35
|
+
tab.clearModalState(dialogState);
|
|
36
|
+
await tab.waitForCompletion(async () => {
|
|
37
|
+
if (params.accept)
|
|
38
|
+
await dialogState.dialog.accept(params.promptText);
|
|
39
|
+
else
|
|
40
|
+
await dialogState.dialog.dismiss();
|
|
41
|
+
});
|
|
47
42
|
},
|
|
48
43
|
clearsModalState: 'dialog',
|
|
49
44
|
});
|
|
50
|
-
export default
|
|
51
|
-
handleDialog
|
|
45
|
+
export default [
|
|
46
|
+
handleDialog,
|
|
52
47
|
];
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { z } from 'zod';
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
import * as javascript from '../javascript.js';
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
|
+
const evaluateSchema = z.object({
|
|
21
|
+
function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
|
|
22
|
+
element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
|
|
23
|
+
ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
|
|
24
|
+
});
|
|
25
|
+
const evaluate = defineTabTool({
|
|
26
|
+
capability: 'core',
|
|
27
|
+
schema: {
|
|
28
|
+
name: 'browser_evaluate',
|
|
29
|
+
title: 'Evaluate JavaScript',
|
|
30
|
+
description: 'Evaluate JavaScript expression on page or element',
|
|
31
|
+
inputSchema: evaluateSchema,
|
|
32
|
+
type: 'destructive',
|
|
33
|
+
},
|
|
34
|
+
handle: async (tab, params, response) => {
|
|
35
|
+
response.setIncludeSnapshot();
|
|
36
|
+
let locator;
|
|
37
|
+
if (params.ref && params.element) {
|
|
38
|
+
locator = await tab.refLocator({ ref: params.ref, element: params.element });
|
|
39
|
+
response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
|
|
43
|
+
}
|
|
44
|
+
await tab.waitForCompletion(async () => {
|
|
45
|
+
const receiver = locator ?? tab.page;
|
|
46
|
+
const result = await receiver._evaluateFunction(params.function);
|
|
47
|
+
response.addResult(JSON.stringify(result, null, 2) || 'undefined');
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
export default [
|
|
52
|
+
evaluate,
|
|
53
|
+
];
|
package/lib/tools/files.js
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
const uploadFile =
|
|
19
|
-
capability: '
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
const uploadFile = defineTabTool({
|
|
19
|
+
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_file_upload',
|
|
22
22
|
title: 'Upload files',
|
|
@@ -26,26 +26,20 @@ const uploadFile = captureSnapshot => defineTool({
|
|
|
26
26
|
}),
|
|
27
27
|
type: 'destructive',
|
|
28
28
|
},
|
|
29
|
-
handle: async (
|
|
30
|
-
|
|
29
|
+
handle: async (tab, params, response) => {
|
|
30
|
+
response.setIncludeSnapshot();
|
|
31
|
+
const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
|
|
31
32
|
if (!modalState)
|
|
32
33
|
throw new Error('No file chooser visible');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
response.addCode(`// Select files for upload`);
|
|
35
|
+
response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
|
|
36
|
+
tab.clearModalState(modalState);
|
|
37
|
+
await tab.waitForCompletion(async () => {
|
|
37
38
|
await modalState.fileChooser.setFiles(params.paths);
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
return {
|
|
41
|
-
code,
|
|
42
|
-
action,
|
|
43
|
-
captureSnapshot,
|
|
44
|
-
waitForNetwork: true,
|
|
45
|
-
};
|
|
39
|
+
});
|
|
46
40
|
},
|
|
47
41
|
clearsModalState: 'fileChooser',
|
|
48
42
|
});
|
|
49
|
-
export default
|
|
50
|
-
uploadFile
|
|
43
|
+
export default [
|
|
44
|
+
uploadFile,
|
|
51
45
|
];
|
package/lib/tools/install.js
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { fork } from 'child_process';
|
|
17
17
|
import path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
18
19
|
import { z } from 'zod';
|
|
19
20
|
import { defineTool } from './tool.js';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
21
|
const install = defineTool({
|
|
22
|
-
capability: 'install',
|
|
22
|
+
capability: 'core-install',
|
|
23
23
|
schema: {
|
|
24
24
|
name: 'browser_install',
|
|
25
25
|
title: 'Install the browser specified in the config',
|
|
@@ -27,7 +27,7 @@ const install = defineTool({
|
|
|
27
27
|
inputSchema: z.object({}),
|
|
28
28
|
type: 'destructive',
|
|
29
29
|
},
|
|
30
|
-
handle: async (context) => {
|
|
30
|
+
handle: async (context, params, response) => {
|
|
31
31
|
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
|
32
32
|
const cliUrl = import.meta.resolve('playwright/package.json');
|
|
33
33
|
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
|
@@ -45,11 +45,7 @@ const install = defineTool({
|
|
|
45
45
|
reject(new Error(`Failed to install browser: ${output.join('')}`));
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
-
|
|
49
|
-
code: [`// Browser ${channel} installed`],
|
|
50
|
-
captureSnapshot: false,
|
|
51
|
-
waitForNetwork: false,
|
|
52
|
-
};
|
|
48
|
+
response.setIncludeTabs();
|
|
53
49
|
},
|
|
54
50
|
});
|
|
55
51
|
export default [
|
package/lib/tools/keyboard.js
CHANGED
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
import { elementSchema } from './snapshot.js';
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
|
+
import * as javascript from '../javascript.js';
|
|
21
|
+
const pressKey = defineTabTool({
|
|
19
22
|
capability: 'core',
|
|
20
23
|
schema: {
|
|
21
24
|
name: 'browser_press_key',
|
|
@@ -26,21 +29,52 @@ const pressKey = captureSnapshot => defineTool({
|
|
|
26
29
|
}),
|
|
27
30
|
type: 'destructive',
|
|
28
31
|
},
|
|
29
|
-
handle: async (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
code,
|
|
38
|
-
action,
|
|
39
|
-
captureSnapshot,
|
|
40
|
-
waitForNetwork: true
|
|
41
|
-
};
|
|
32
|
+
handle: async (tab, params, response) => {
|
|
33
|
+
response.setIncludeSnapshot();
|
|
34
|
+
response.addCode(`// Press ${params.key}`);
|
|
35
|
+
response.addCode(`await page.keyboard.press('${params.key}');`);
|
|
36
|
+
await tab.waitForCompletion(async () => {
|
|
37
|
+
await tab.page.keyboard.press(params.key);
|
|
38
|
+
});
|
|
42
39
|
},
|
|
43
40
|
});
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const typeSchema = elementSchema.extend({
|
|
42
|
+
text: z.string().describe('Text to type into the element'),
|
|
43
|
+
submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
|
|
44
|
+
slowly: z.boolean().optional().describe('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.'),
|
|
45
|
+
});
|
|
46
|
+
const type = defineTabTool({
|
|
47
|
+
capability: 'core',
|
|
48
|
+
schema: {
|
|
49
|
+
name: 'browser_type',
|
|
50
|
+
title: 'Type text',
|
|
51
|
+
description: 'Type text into editable element',
|
|
52
|
+
inputSchema: typeSchema,
|
|
53
|
+
type: 'destructive',
|
|
54
|
+
},
|
|
55
|
+
handle: async (tab, params, response) => {
|
|
56
|
+
const locator = await tab.refLocator(params);
|
|
57
|
+
await tab.waitForCompletion(async () => {
|
|
58
|
+
if (params.slowly) {
|
|
59
|
+
response.setIncludeSnapshot();
|
|
60
|
+
response.addCode(`// Press "${params.text}" sequentially into "${params.element}"`);
|
|
61
|
+
response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
|
|
62
|
+
await locator.pressSequentially(params.text);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
response.addCode(`// Fill "${params.text}" into "${params.element}"`);
|
|
66
|
+
response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
|
|
67
|
+
await locator.fill(params.text);
|
|
68
|
+
}
|
|
69
|
+
if (params.submit) {
|
|
70
|
+
response.setIncludeSnapshot();
|
|
71
|
+
response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
|
|
72
|
+
await locator.press('Enter');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
export default [
|
|
78
|
+
pressKey,
|
|
79
|
+
type,
|
|
46
80
|
];
|