@n8n/mcp-browser 0.1.0-rc1
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/LICENSE.md +88 -0
- package/README.md +175 -0
- package/dist/adapters/playwright.d.ts +72 -0
- package/dist/adapters/playwright.js +684 -0
- package/dist/adapters/playwright.js.map +1 -0
- package/dist/browser-discovery.d.ts +12 -0
- package/dist/browser-discovery.js +219 -0
- package/dist/browser-discovery.js.map +1 -0
- package/dist/build.tsbuildinfo +1 -0
- package/dist/cdp-relay-protocol.d.ts +81 -0
- package/dist/cdp-relay-protocol.js +5 -0
- package/dist/cdp-relay-protocol.js.map +1 -0
- package/dist/cdp-relay.d.ts +52 -0
- package/dist/cdp-relay.js +508 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/connection.d.ts +15 -0
- package/dist/connection.js +137 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +83 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +88 -0
- package/dist/logger.js.map +1 -0
- package/dist/server-config.d.ts +7 -0
- package/dist/server-config.js +45 -0
- package/dist/server-config.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/helpers.d.ts +15 -0
- package/dist/tools/helpers.js +77 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +25 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/inspection.d.ts +3 -0
- package/dist/tools/inspection.js +205 -0
- package/dist/tools/inspection.js.map +1 -0
- package/dist/tools/interaction.d.ts +3 -0
- package/dist/tools/interaction.js +211 -0
- package/dist/tools/interaction.js.map +1 -0
- package/dist/tools/navigation.d.ts +3 -0
- package/dist/tools/navigation.js +75 -0
- package/dist/tools/navigation.js.map +1 -0
- package/dist/tools/response-envelope.d.ts +13 -0
- package/dist/tools/response-envelope.js +92 -0
- package/dist/tools/response-envelope.js.map +1 -0
- package/dist/tools/schemas.d.ts +236 -0
- package/dist/tools/schemas.js +46 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/session.d.ts +3 -0
- package/dist/tools/session.js +81 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/state.d.ts +3 -0
- package/dist/tools/state.js +108 -0
- package/dist/tools/state.js.map +1 -0
- package/dist/tools/tabs.d.ts +3 -0
- package/dist/tools/tabs.js +121 -0
- package/dist/tools/tabs.js.map +1 -0
- package/dist/tools/wait.d.ts +3 -0
- package/dist/tools/wait.js +39 -0
- package/dist/tools/wait.js.map +1 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +54 -0
- package/dist/utils.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlaywrightAdapter = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const playwright_core_1 = require("playwright-core");
|
|
6
|
+
const cdp_relay_1 = require("../cdp-relay");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const logger_1 = require("../logger");
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const log = (0, logger_1.createLogger)('playwright');
|
|
11
|
+
const BROWSER_BRIDGE_EXTENSION_ID = 'agklaocphkdbepcjccjpnbcglmpebhpo';
|
|
12
|
+
class PlaywrightAdapter {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.name = 'playwright';
|
|
15
|
+
this.pageStates = new Map();
|
|
16
|
+
this.resolvedConfig = config;
|
|
17
|
+
}
|
|
18
|
+
async launch(config) {
|
|
19
|
+
log.debug('launch: browser =', config.browser);
|
|
20
|
+
this.relay = new cdp_relay_1.CDPRelayServer();
|
|
21
|
+
const port = await this.relay.listen();
|
|
22
|
+
const extensionEndpoint = this.relay.extensionEndpoint(port);
|
|
23
|
+
const connectUrl = `chrome-extension://${BROWSER_BRIDGE_EXTENSION_ID}/dist/connect.html` +
|
|
24
|
+
`?mcpRelayUrl=${encodeURIComponent(extensionEndpoint)}`;
|
|
25
|
+
const browserInfo = this.resolvedConfig.browsers.get(config.browser);
|
|
26
|
+
const chromePath = browserInfo?.executablePath;
|
|
27
|
+
if (!chromePath) {
|
|
28
|
+
throw new errors_1.BrowserExecutableNotFoundError(config.browser);
|
|
29
|
+
}
|
|
30
|
+
log.debug('launching browser:', chromePath);
|
|
31
|
+
log.debug('connect URL:', connectUrl);
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const child = (0, node_child_process_1.execFile)(chromePath, [connectUrl]);
|
|
34
|
+
const earlyFailTimer = setTimeout(() => resolve(), 2_000);
|
|
35
|
+
child.on('error', (spawnError) => {
|
|
36
|
+
clearTimeout(earlyFailTimer);
|
|
37
|
+
log.error('browser spawn error:', spawnError.message);
|
|
38
|
+
reject(new errors_1.BrowserExecutableNotFoundError(`${config.browser} (${spawnError.message})`));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
log.debug('waiting for extension...');
|
|
42
|
+
await this.relay.waitForExtension({ browserWasLaunched: true });
|
|
43
|
+
const cdpEndpoint = this.relay.cdpEndpoint(port);
|
|
44
|
+
log.debug('connecting Playwright over CDP:', cdpEndpoint);
|
|
45
|
+
this.browser = await playwright_core_1.chromium.connectOverCDP(cdpEndpoint);
|
|
46
|
+
const contexts = this.browser.contexts();
|
|
47
|
+
log.debug('browser contexts:', contexts.length);
|
|
48
|
+
this.context = contexts[0] ?? (await this.browser.newContext());
|
|
49
|
+
this.context.on('page', (page) => {
|
|
50
|
+
if (this.pendingActivation) {
|
|
51
|
+
const { id, resolve } = this.pendingActivation;
|
|
52
|
+
this.pendingActivation = undefined;
|
|
53
|
+
log.debug('page event: consumed pendingActivation, id =', id);
|
|
54
|
+
if (!this.pageStates.has(id)) {
|
|
55
|
+
this.trackPage(page, id);
|
|
56
|
+
}
|
|
57
|
+
resolve(page);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
log.debug('page event: no pendingActivation, assigning random id');
|
|
61
|
+
if (!this.findPageState(page)) {
|
|
62
|
+
this.trackPage(page);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
log.debug('launch complete, context ready for lazy activation');
|
|
66
|
+
}
|
|
67
|
+
async close() {
|
|
68
|
+
try {
|
|
69
|
+
if (this.context)
|
|
70
|
+
await this.context.close();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
if (this.browser)
|
|
76
|
+
await this.browser.close();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
}
|
|
80
|
+
if (this.relay) {
|
|
81
|
+
this.relay.stop();
|
|
82
|
+
this.relay = undefined;
|
|
83
|
+
}
|
|
84
|
+
this.pageStates.clear();
|
|
85
|
+
}
|
|
86
|
+
async newPage(url) {
|
|
87
|
+
log.debug('newPage: creating page, url =', url ?? '(none)');
|
|
88
|
+
const page = await this.requireContext().newPage();
|
|
89
|
+
const tabId = this.relay?.getLastCreatedTabId();
|
|
90
|
+
log.debug('newPage: relay tabId =', tabId);
|
|
91
|
+
const state = this.findPageState(page) ?? this.trackPage(page, tabId);
|
|
92
|
+
if (url) {
|
|
93
|
+
await page.goto(url, { waitUntil: 'load' });
|
|
94
|
+
state.info.title = await page.title();
|
|
95
|
+
state.info.url = page.url();
|
|
96
|
+
}
|
|
97
|
+
return { ...state.info };
|
|
98
|
+
}
|
|
99
|
+
async closePage(pageId) {
|
|
100
|
+
this.pageStates.delete(pageId);
|
|
101
|
+
if (this.relay) {
|
|
102
|
+
await this.relay.closeTab(pageId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async focusPage(pageId) {
|
|
106
|
+
const state = await this.ensurePage(pageId);
|
|
107
|
+
await state.page.bringToFront();
|
|
108
|
+
}
|
|
109
|
+
async listPages() {
|
|
110
|
+
const result = [];
|
|
111
|
+
for (const state of this.pageStates.values()) {
|
|
112
|
+
try {
|
|
113
|
+
state.info.title = await state.page.title();
|
|
114
|
+
state.info.url = state.page.url();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
}
|
|
118
|
+
result.push({ ...state.info });
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
async listTabs() {
|
|
123
|
+
if (this.relay) {
|
|
124
|
+
const tabs = (await this.relay.listTabs()).map((t) => ({
|
|
125
|
+
id: t.id,
|
|
126
|
+
title: t.title,
|
|
127
|
+
url: t.url,
|
|
128
|
+
}));
|
|
129
|
+
log.debug('listTabs: relay returned', tabs.length, 'tabs');
|
|
130
|
+
return tabs;
|
|
131
|
+
}
|
|
132
|
+
const pages = await this.listPages();
|
|
133
|
+
log.debug('listTabs: fallback to listPages, returned', pages.length, 'pages');
|
|
134
|
+
return pages;
|
|
135
|
+
}
|
|
136
|
+
listTabSessionIds() {
|
|
137
|
+
return Array.from(this.pageStates.keys());
|
|
138
|
+
}
|
|
139
|
+
async listTabIds() {
|
|
140
|
+
if (this.relay) {
|
|
141
|
+
const tabs = await this.relay.listTabs();
|
|
142
|
+
log.debug(`listTabIds: relay returned ${tabs.length} tab(s)`);
|
|
143
|
+
return tabs.map((t) => t.id);
|
|
144
|
+
}
|
|
145
|
+
const ids = this.listTabSessionIds();
|
|
146
|
+
log.debug(`listTabIds: fallback to pageStates, ${ids.length} page(s)`);
|
|
147
|
+
return ids;
|
|
148
|
+
}
|
|
149
|
+
async navigate(pageId, url, waitUntil = 'load') {
|
|
150
|
+
const { page } = await this.ensurePage(pageId);
|
|
151
|
+
const response = await page.goto(url, { waitUntil });
|
|
152
|
+
return {
|
|
153
|
+
title: await page.title(),
|
|
154
|
+
url: page.url(),
|
|
155
|
+
status: response?.status() ?? 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async back(pageId) {
|
|
159
|
+
const { page } = await this.ensurePage(pageId);
|
|
160
|
+
await page.goBack({ waitUntil: 'load' });
|
|
161
|
+
return { title: await page.title(), url: page.url(), status: 0 };
|
|
162
|
+
}
|
|
163
|
+
async forward(pageId) {
|
|
164
|
+
const { page } = await this.ensurePage(pageId);
|
|
165
|
+
await page.goForward({ waitUntil: 'load' });
|
|
166
|
+
return { title: await page.title(), url: page.url(), status: 0 };
|
|
167
|
+
}
|
|
168
|
+
async reload(pageId, waitUntil = 'load') {
|
|
169
|
+
const { page } = await this.ensurePage(pageId);
|
|
170
|
+
const response = await page.reload({ waitUntil });
|
|
171
|
+
return {
|
|
172
|
+
title: await page.title(),
|
|
173
|
+
url: page.url(),
|
|
174
|
+
status: response?.status() ?? 0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async click(pageId, target, options) {
|
|
178
|
+
await this.ensurePage(pageId);
|
|
179
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
180
|
+
await locator.click({
|
|
181
|
+
button: options?.button,
|
|
182
|
+
clickCount: options?.clickCount,
|
|
183
|
+
modifiers: options?.modifiers,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async type(pageId, target, text, options) {
|
|
187
|
+
await this.ensurePage(pageId);
|
|
188
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
189
|
+
if (options?.clear) {
|
|
190
|
+
await locator.clear();
|
|
191
|
+
}
|
|
192
|
+
await locator.pressSequentially(text, { delay: options?.delay });
|
|
193
|
+
if (options?.submit) {
|
|
194
|
+
await locator.press('Enter');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async select(pageId, target, values) {
|
|
198
|
+
await this.ensurePage(pageId);
|
|
199
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
200
|
+
return await locator.selectOption(values);
|
|
201
|
+
}
|
|
202
|
+
async hover(pageId, target) {
|
|
203
|
+
await this.ensurePage(pageId);
|
|
204
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
205
|
+
await locator.hover();
|
|
206
|
+
}
|
|
207
|
+
async press(pageId, keys) {
|
|
208
|
+
const { page } = await this.ensurePage(pageId);
|
|
209
|
+
await page.keyboard.press(keys);
|
|
210
|
+
}
|
|
211
|
+
async drag(pageId, from, to) {
|
|
212
|
+
await this.ensurePage(pageId);
|
|
213
|
+
const fromLocator = await this.resolveLocator(pageId, from);
|
|
214
|
+
const toLocator = await this.resolveLocator(pageId, to);
|
|
215
|
+
await fromLocator.dragTo(toLocator);
|
|
216
|
+
}
|
|
217
|
+
async scroll(pageId, target, options) {
|
|
218
|
+
const { page } = await this.ensurePage(pageId);
|
|
219
|
+
if (target) {
|
|
220
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
221
|
+
await locator.scrollIntoViewIfNeeded();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const amount = options?.amount ?? 500;
|
|
225
|
+
const delta = options?.direction === 'up' ? -amount : amount;
|
|
226
|
+
await page.mouse.wheel(0, delta);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async upload(pageId, target, files) {
|
|
230
|
+
const state = await this.ensurePage(pageId);
|
|
231
|
+
if (state.pendingFileChooser) {
|
|
232
|
+
await state.pendingFileChooser.setFiles(files);
|
|
233
|
+
state.pendingFileChooser = undefined;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!target) {
|
|
237
|
+
throw new Error('No file chooser pending and no element target provided');
|
|
238
|
+
}
|
|
239
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
240
|
+
await locator.setInputFiles(files);
|
|
241
|
+
}
|
|
242
|
+
async dialog(pageId, action, text) {
|
|
243
|
+
const state = await this.ensurePage(pageId);
|
|
244
|
+
if (state.pendingDialog) {
|
|
245
|
+
const dialogType = state.pendingDialog.type();
|
|
246
|
+
if (action === 'accept') {
|
|
247
|
+
await state.pendingDialog.accept(text);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
await state.pendingDialog.dismiss();
|
|
251
|
+
}
|
|
252
|
+
state.pendingDialog = undefined;
|
|
253
|
+
return dialogType;
|
|
254
|
+
}
|
|
255
|
+
return await new Promise((resolve, reject) => {
|
|
256
|
+
const timeout = setTimeout(() => {
|
|
257
|
+
reject(new Error('No dialog appeared within 10 seconds'));
|
|
258
|
+
}, 10_000);
|
|
259
|
+
state.page.once('dialog', async (dlg) => {
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
try {
|
|
262
|
+
const dialogType = dlg.type();
|
|
263
|
+
if (action === 'accept') {
|
|
264
|
+
await dlg.accept(text);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
await dlg.dismiss();
|
|
268
|
+
}
|
|
269
|
+
resolve(dialogType);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
reject((0, utils_1.toError)(error));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async screenshot(pageId, target, options) {
|
|
278
|
+
const { page } = await this.ensurePage(pageId);
|
|
279
|
+
let buffer;
|
|
280
|
+
if (target) {
|
|
281
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
282
|
+
buffer = await locator.screenshot({ type: 'png' });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
buffer = await page.screenshot({
|
|
286
|
+
type: 'png',
|
|
287
|
+
fullPage: options?.fullPage,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return buffer.toString('base64');
|
|
291
|
+
}
|
|
292
|
+
async snapshot(pageId, target) {
|
|
293
|
+
const { page } = await this.ensurePage(pageId);
|
|
294
|
+
let yaml;
|
|
295
|
+
if (target) {
|
|
296
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
297
|
+
yaml = await locator.ariaSnapshot();
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const privatePage = page;
|
|
301
|
+
const result = await privatePage._snapshotForAI({ track: 'response' });
|
|
302
|
+
yaml = result.full;
|
|
303
|
+
}
|
|
304
|
+
if (!yaml) {
|
|
305
|
+
return { tree: '(empty page)', refCount: 0 };
|
|
306
|
+
}
|
|
307
|
+
const refMatches = yaml.match(/\[ref=e\d+\]/g);
|
|
308
|
+
const refCount = refMatches?.length ?? 0;
|
|
309
|
+
return { tree: yaml, refCount };
|
|
310
|
+
}
|
|
311
|
+
async getText(pageId, target) {
|
|
312
|
+
const { page } = await this.ensurePage(pageId);
|
|
313
|
+
if (target) {
|
|
314
|
+
const locator = await this.resolveLocator(pageId, target);
|
|
315
|
+
return await locator.innerText();
|
|
316
|
+
}
|
|
317
|
+
return await page.innerText('body');
|
|
318
|
+
}
|
|
319
|
+
async evaluate(pageId, script) {
|
|
320
|
+
const { page } = await this.ensurePage(pageId);
|
|
321
|
+
return await page.evaluate(script);
|
|
322
|
+
}
|
|
323
|
+
async getConsole(pageId, level, clear) {
|
|
324
|
+
const state = await this.ensurePage(pageId);
|
|
325
|
+
let entries = [
|
|
326
|
+
...state.consoleBuffer,
|
|
327
|
+
...state.errorBuffer.map((e) => ({
|
|
328
|
+
level: 'error',
|
|
329
|
+
text: e.stack ? `${e.message}\n${e.stack}` : e.message,
|
|
330
|
+
timestamp: e.timestamp,
|
|
331
|
+
})),
|
|
332
|
+
];
|
|
333
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
334
|
+
if (level) {
|
|
335
|
+
entries = entries.filter((e) => e.level === level);
|
|
336
|
+
}
|
|
337
|
+
if (clear) {
|
|
338
|
+
state.consoleBuffer = [];
|
|
339
|
+
state.errorBuffer = [];
|
|
340
|
+
}
|
|
341
|
+
return entries;
|
|
342
|
+
}
|
|
343
|
+
getConsoleSummary(pageId) {
|
|
344
|
+
const state = this.pageStates.get(pageId);
|
|
345
|
+
if (!state)
|
|
346
|
+
return { errors: 0, warnings: 0 };
|
|
347
|
+
const consoleErrors = state.consoleBuffer.filter((e) => e.level === 'error').length;
|
|
348
|
+
const pageErrors = state.errorBuffer.length;
|
|
349
|
+
const warnings = state.consoleBuffer.filter((e) => e.level === 'warning').length;
|
|
350
|
+
return { errors: consoleErrors + pageErrors, warnings };
|
|
351
|
+
}
|
|
352
|
+
async pdf(pageId, options) {
|
|
353
|
+
const { page } = await this.ensurePage(pageId);
|
|
354
|
+
const buffer = await page.pdf({
|
|
355
|
+
format: options?.format ?? 'A4',
|
|
356
|
+
landscape: options?.landscape,
|
|
357
|
+
});
|
|
358
|
+
const data = buffer.toString('base64');
|
|
359
|
+
return { data, pages: 1 };
|
|
360
|
+
}
|
|
361
|
+
async getNetwork(pageId, filter, clear) {
|
|
362
|
+
const state = await this.ensurePage(pageId);
|
|
363
|
+
let entries = [...state.networkBuffer];
|
|
364
|
+
if (filter) {
|
|
365
|
+
const pattern = this.globToRegex(filter);
|
|
366
|
+
entries = entries.filter((e) => pattern.test(e.url));
|
|
367
|
+
}
|
|
368
|
+
if (clear) {
|
|
369
|
+
state.networkBuffer = [];
|
|
370
|
+
}
|
|
371
|
+
return entries;
|
|
372
|
+
}
|
|
373
|
+
async wait(pageId, options) {
|
|
374
|
+
const { page } = await this.ensurePage(pageId);
|
|
375
|
+
const start = Date.now();
|
|
376
|
+
const timeout = options.timeoutMs ?? 30_000;
|
|
377
|
+
const promises = [];
|
|
378
|
+
if (options.selector) {
|
|
379
|
+
promises.push(page.waitForSelector(options.selector, { timeout }));
|
|
380
|
+
}
|
|
381
|
+
if (options.url) {
|
|
382
|
+
promises.push(page.waitForURL(options.url, { timeout }));
|
|
383
|
+
}
|
|
384
|
+
if (options.loadState) {
|
|
385
|
+
promises.push(page.waitForLoadState(options.loadState, { timeout }));
|
|
386
|
+
}
|
|
387
|
+
if (options.predicate) {
|
|
388
|
+
promises.push(page.waitForFunction(options.predicate, undefined, { timeout }));
|
|
389
|
+
}
|
|
390
|
+
if (options.text) {
|
|
391
|
+
promises.push(page.waitForSelector(`text=${options.text}`, { timeout }));
|
|
392
|
+
}
|
|
393
|
+
if (promises.length === 0) {
|
|
394
|
+
await page.waitForTimeout(100);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
await Promise.all(promises);
|
|
398
|
+
}
|
|
399
|
+
return Date.now() - start;
|
|
400
|
+
}
|
|
401
|
+
async getCookies(pageId, url) {
|
|
402
|
+
await this.ensurePage(pageId);
|
|
403
|
+
const context = this.requireContext();
|
|
404
|
+
const cookies = url ? await context.cookies(url) : await context.cookies();
|
|
405
|
+
return cookies.map((c) => ({
|
|
406
|
+
name: c.name,
|
|
407
|
+
value: c.value,
|
|
408
|
+
domain: c.domain,
|
|
409
|
+
path: c.path,
|
|
410
|
+
expires: c.expires,
|
|
411
|
+
httpOnly: c.httpOnly,
|
|
412
|
+
secure: c.secure,
|
|
413
|
+
sameSite: c.sameSite,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
async setCookies(pageId, cookies) {
|
|
417
|
+
await this.ensurePage(pageId);
|
|
418
|
+
const context = this.requireContext();
|
|
419
|
+
await context.addCookies(cookies.map((c) => ({
|
|
420
|
+
name: c.name,
|
|
421
|
+
value: c.value,
|
|
422
|
+
domain: c.domain,
|
|
423
|
+
path: c.path ?? '/',
|
|
424
|
+
expires: c.expires,
|
|
425
|
+
httpOnly: c.httpOnly,
|
|
426
|
+
secure: c.secure,
|
|
427
|
+
sameSite: c.sameSite,
|
|
428
|
+
})));
|
|
429
|
+
}
|
|
430
|
+
async clearCookies(pageId) {
|
|
431
|
+
await this.ensurePage(pageId);
|
|
432
|
+
await this.requireContext().clearCookies();
|
|
433
|
+
}
|
|
434
|
+
async getStorage(pageId, kind) {
|
|
435
|
+
const { page } = await this.ensurePage(pageId);
|
|
436
|
+
const storageObj = kind === 'local' ? 'localStorage' : 'sessionStorage';
|
|
437
|
+
return await page.evaluate((s) => {
|
|
438
|
+
const storage = s === 'localStorage' ? localStorage : sessionStorage;
|
|
439
|
+
const result = {};
|
|
440
|
+
for (let i = 0; i < storage.length; i++) {
|
|
441
|
+
const key = storage.key(i);
|
|
442
|
+
if (key !== null)
|
|
443
|
+
result[key] = storage.getItem(key) ?? '';
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}, storageObj);
|
|
447
|
+
}
|
|
448
|
+
async setStorage(pageId, kind, data) {
|
|
449
|
+
const { page } = await this.ensurePage(pageId);
|
|
450
|
+
await page.evaluate(({ kind: k, data: d }) => {
|
|
451
|
+
const storage = k === 'local' ? localStorage : sessionStorage;
|
|
452
|
+
for (const [key, value] of Object.entries(d)) {
|
|
453
|
+
storage.setItem(key, value);
|
|
454
|
+
}
|
|
455
|
+
}, { kind, data });
|
|
456
|
+
}
|
|
457
|
+
async clearStorage(pageId, kind) {
|
|
458
|
+
const { page } = await this.ensurePage(pageId);
|
|
459
|
+
await page.evaluate((k) => {
|
|
460
|
+
const storage = k === 'local' ? localStorage : sessionStorage;
|
|
461
|
+
storage.clear();
|
|
462
|
+
}, kind);
|
|
463
|
+
}
|
|
464
|
+
async waitForCompletion(pageId, action) {
|
|
465
|
+
if (!this.pageStates.has(pageId) && !this.relay?.hasTab(pageId)) {
|
|
466
|
+
log.debug('waitForCompletion: page not found, running action directly:', pageId);
|
|
467
|
+
return await action();
|
|
468
|
+
}
|
|
469
|
+
const { page } = await this.ensurePage(pageId);
|
|
470
|
+
const requests = [];
|
|
471
|
+
const requestListener = (request) => requests.push(request);
|
|
472
|
+
page.on('request', requestListener);
|
|
473
|
+
let result;
|
|
474
|
+
try {
|
|
475
|
+
result = await action();
|
|
476
|
+
await page.waitForTimeout(500);
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
page.off('request', requestListener);
|
|
480
|
+
}
|
|
481
|
+
if (requests.some((r) => r.isNavigationRequest())) {
|
|
482
|
+
await page
|
|
483
|
+
.mainFrame()
|
|
484
|
+
.waitForLoadState('load', { timeout: 10_000 })
|
|
485
|
+
.catch(() => { });
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
const resourceTypes = new Set(['document', 'stylesheet', 'script', 'xhr', 'fetch']);
|
|
489
|
+
const promises = requests.map(async (r) => {
|
|
490
|
+
if (resourceTypes.has(r.resourceType())) {
|
|
491
|
+
const resp = await r.response().catch(() => undefined);
|
|
492
|
+
await resp?.finished().catch(() => { });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
await r.response().catch(() => { });
|
|
496
|
+
});
|
|
497
|
+
await Promise.race([
|
|
498
|
+
Promise.all(promises),
|
|
499
|
+
new Promise((resolve) => setTimeout(resolve, 5_000)),
|
|
500
|
+
]);
|
|
501
|
+
if (requests.length > 0) {
|
|
502
|
+
await page.waitForTimeout(500);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
getModalStates(pageId) {
|
|
507
|
+
const state = this.pageStates.get(pageId);
|
|
508
|
+
if (!state)
|
|
509
|
+
return [];
|
|
510
|
+
const modals = [];
|
|
511
|
+
if (state.pendingDialog) {
|
|
512
|
+
modals.push({
|
|
513
|
+
type: 'dialog',
|
|
514
|
+
description: `JavaScript ${state.pendingDialog.type()} dialog: "${state.pendingDialog.message()}"`,
|
|
515
|
+
clearedBy: 'browser_dialog',
|
|
516
|
+
dialogType: state.pendingDialog.type(),
|
|
517
|
+
message: state.pendingDialog.message(),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (state.pendingFileChooser) {
|
|
521
|
+
modals.push({
|
|
522
|
+
type: 'filechooser',
|
|
523
|
+
description: 'File chooser is open, waiting for file selection.',
|
|
524
|
+
clearedBy: 'browser_upload',
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return modals;
|
|
528
|
+
}
|
|
529
|
+
async getContent(pageId, selector) {
|
|
530
|
+
const { page } = await this.ensurePage(pageId);
|
|
531
|
+
if (selector) {
|
|
532
|
+
const locator = page.locator(selector);
|
|
533
|
+
const html = await locator.evaluate((el) => el.outerHTML);
|
|
534
|
+
return { html, url: page.url() };
|
|
535
|
+
}
|
|
536
|
+
return { html: await page.content(), url: page.url() };
|
|
537
|
+
}
|
|
538
|
+
async resolveRef(pageId, ref) {
|
|
539
|
+
const { page } = await this.ensurePage(pageId);
|
|
540
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
541
|
+
try {
|
|
542
|
+
const count = await locator.count();
|
|
543
|
+
if (count === 0)
|
|
544
|
+
throw new errors_1.StaleRefError(ref);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error instanceof errors_1.StaleRefError)
|
|
548
|
+
throw error;
|
|
549
|
+
throw new errors_1.StaleRefError(ref);
|
|
550
|
+
}
|
|
551
|
+
return locator;
|
|
552
|
+
}
|
|
553
|
+
requireContext() {
|
|
554
|
+
if (!this.context)
|
|
555
|
+
throw new Error('Browser context not initialized');
|
|
556
|
+
return this.context;
|
|
557
|
+
}
|
|
558
|
+
requirePage(pageId) {
|
|
559
|
+
const state = this.pageStates.get(pageId);
|
|
560
|
+
if (!state)
|
|
561
|
+
throw new errors_1.PageNotFoundError(pageId);
|
|
562
|
+
return state;
|
|
563
|
+
}
|
|
564
|
+
async ensurePage(pageId) {
|
|
565
|
+
const existing = this.pageStates.get(pageId);
|
|
566
|
+
if (existing) {
|
|
567
|
+
log.debug('ensurePage: page already tracked:', pageId);
|
|
568
|
+
return existing;
|
|
569
|
+
}
|
|
570
|
+
if (!this.relay || !this.context)
|
|
571
|
+
throw new errors_1.PageNotFoundError(pageId);
|
|
572
|
+
if (!pageId || !this.relay.hasTab(pageId)) {
|
|
573
|
+
throw new errors_1.PageNotFoundError(pageId);
|
|
574
|
+
}
|
|
575
|
+
log.debug('ensurePage: activating tab', pageId, 'current pages:', [...this.pageStates.keys()]);
|
|
576
|
+
const pagePromise = new Promise((resolve, reject) => {
|
|
577
|
+
const timeout = setTimeout(() => {
|
|
578
|
+
this.pendingActivation = undefined;
|
|
579
|
+
log.error('ensurePage: timed out waiting for page event after activateTab:', pageId);
|
|
580
|
+
reject(new Error(`Timed out waiting for page after activateTab (${pageId})`));
|
|
581
|
+
}, 10_000);
|
|
582
|
+
this.pendingActivation = {
|
|
583
|
+
id: pageId,
|
|
584
|
+
resolve: (page) => {
|
|
585
|
+
clearTimeout(timeout);
|
|
586
|
+
log.debug('ensurePage: page event received for', pageId);
|
|
587
|
+
resolve(page);
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
log.debug('ensurePage: calling activateTab for', pageId);
|
|
592
|
+
await this.relay.activateTab(pageId);
|
|
593
|
+
log.debug('ensurePage: activateTab completed for', pageId, '— waiting for page event');
|
|
594
|
+
const page = await pagePromise;
|
|
595
|
+
log.debug('ensurePage: waiting for domcontentloaded on', pageId);
|
|
596
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => {
|
|
597
|
+
log.debug('ensurePage: domcontentloaded timeout (non-fatal) for', pageId);
|
|
598
|
+
});
|
|
599
|
+
log.debug('ensurePage: page ready:', pageId);
|
|
600
|
+
return this.pageStates.get(pageId) ?? this.trackPage(page, pageId);
|
|
601
|
+
}
|
|
602
|
+
findPageState(page) {
|
|
603
|
+
for (const state of this.pageStates.values()) {
|
|
604
|
+
if (state.page === page)
|
|
605
|
+
return state;
|
|
606
|
+
}
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
trackPage(page, explicitId) {
|
|
610
|
+
const id = explicitId ?? (0, utils_1.generateId)('page');
|
|
611
|
+
log.debug('trackPage: id =', id, 'url =', page.url());
|
|
612
|
+
const state = {
|
|
613
|
+
page,
|
|
614
|
+
info: { id, title: '', url: page.url() },
|
|
615
|
+
consoleBuffer: [],
|
|
616
|
+
errorBuffer: [],
|
|
617
|
+
networkBuffer: [],
|
|
618
|
+
};
|
|
619
|
+
page.on('console', (msg) => {
|
|
620
|
+
state.consoleBuffer.push({
|
|
621
|
+
level: msg.type(),
|
|
622
|
+
text: msg.text(),
|
|
623
|
+
timestamp: Date.now(),
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
page.on('pageerror', (error) => {
|
|
627
|
+
state.errorBuffer.push({
|
|
628
|
+
message: error.message,
|
|
629
|
+
stack: error.stack,
|
|
630
|
+
timestamp: Date.now(),
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
page.on('response', (response) => {
|
|
634
|
+
state.networkBuffer.push({
|
|
635
|
+
url: response.url(),
|
|
636
|
+
method: response.request().method(),
|
|
637
|
+
status: response.status(),
|
|
638
|
+
contentType: response.headers()['content-type'],
|
|
639
|
+
timestamp: Date.now(),
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
page.on('dialog', (dlg) => {
|
|
643
|
+
state.pendingDialog = dlg;
|
|
644
|
+
});
|
|
645
|
+
page.on('filechooser', (chooser) => {
|
|
646
|
+
state.pendingFileChooser = chooser;
|
|
647
|
+
});
|
|
648
|
+
page.on('close', () => {
|
|
649
|
+
log.debug('page closed:', id);
|
|
650
|
+
this.pageStates.delete(id);
|
|
651
|
+
this.relay?.deactivateTab(id);
|
|
652
|
+
});
|
|
653
|
+
this.pageStates.set(id, state);
|
|
654
|
+
return state;
|
|
655
|
+
}
|
|
656
|
+
getPageUrl(pageId) {
|
|
657
|
+
const state = this.pageStates.get(pageId);
|
|
658
|
+
if (!state)
|
|
659
|
+
return undefined;
|
|
660
|
+
try {
|
|
661
|
+
return state.page.url();
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async resolveLocator(pageId, target) {
|
|
668
|
+
if ('ref' in target) {
|
|
669
|
+
return (await this.resolveRef(pageId, target.ref));
|
|
670
|
+
}
|
|
671
|
+
const { page } = this.requirePage(pageId);
|
|
672
|
+
return page.locator(target.selector);
|
|
673
|
+
}
|
|
674
|
+
globToRegex(pattern) {
|
|
675
|
+
const escaped = pattern
|
|
676
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
677
|
+
.replace(/\*\*/g, '.*')
|
|
678
|
+
.replace(/\*/g, '[^/]*')
|
|
679
|
+
.replace(/\?/g, '.');
|
|
680
|
+
return new RegExp(`^${escaped}$`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
exports.PlaywrightAdapter = PlaywrightAdapter;
|
|
684
|
+
//# sourceMappingURL=playwright.js.map
|