@playwright/mcp 0.0.24 → 0.0.26
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 +354 -179
- package/config.d.ts +10 -0
- package/lib/config.js +63 -12
- package/lib/connection.js +2 -2
- package/lib/context.js +69 -50
- package/lib/index.js +3 -1
- package/lib/pageSnapshot.js +6 -2
- package/lib/program.js +34 -15
- package/lib/tab.js +9 -2
- package/lib/tools/common.js +1 -41
- package/lib/tools/screenshot.js +77 -0
- package/lib/tools/snapshot.js +1 -59
- package/lib/tools/utils.js +14 -10
- package/lib/tools/wait.js +59 -0
- package/lib/tools.js +8 -3
- package/lib/transport.js +1 -1
- package/package.json +1 -1
- /package/lib/tools/{screen.js → vision.js} +0 -0
package/config.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type Config = {
|
|
|
28
28
|
*/
|
|
29
29
|
browserName?: 'chromium' | 'firefox' | 'webkit';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Keep the browser profile in memory, do not save it to disk.
|
|
33
|
+
*/
|
|
34
|
+
isolated?: boolean;
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Path to a user data directory for browser profile persistence.
|
|
33
38
|
* Temporary directory is created by default.
|
|
@@ -89,6 +94,11 @@ export type Config = {
|
|
|
89
94
|
*/
|
|
90
95
|
vision?: boolean;
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Whether to save the Playwright trace of the session into the output directory.
|
|
99
|
+
*/
|
|
100
|
+
saveTrace?: boolean;
|
|
101
|
+
|
|
92
102
|
/**
|
|
93
103
|
* The directory to save output files.
|
|
94
104
|
*/
|
package/lib/config.js
CHANGED
|
@@ -25,6 +25,7 @@ const defaultConfig = {
|
|
|
25
25
|
launchOptions: {
|
|
26
26
|
channel: 'chrome',
|
|
27
27
|
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
|
28
|
+
chromiumSandbox: true,
|
|
28
29
|
},
|
|
29
30
|
contextOptions: {
|
|
30
31
|
viewport: null,
|
|
@@ -34,11 +35,19 @@ const defaultConfig = {
|
|
|
34
35
|
allowedOrigins: undefined,
|
|
35
36
|
blockedOrigins: undefined,
|
|
36
37
|
},
|
|
38
|
+
outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
|
|
37
39
|
};
|
|
38
|
-
export async function resolveConfig(
|
|
39
|
-
|
|
40
|
+
export async function resolveConfig(config) {
|
|
41
|
+
return mergeConfig(defaultConfig, config);
|
|
42
|
+
}
|
|
43
|
+
export async function resolveCLIConfig(cliOptions) {
|
|
44
|
+
const configInFile = await loadConfig(cliOptions.config);
|
|
40
45
|
const cliOverrides = await configFromCLIOptions(cliOptions);
|
|
41
|
-
|
|
46
|
+
const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
|
|
47
|
+
// Derive artifact output directory from config.outputDir
|
|
48
|
+
if (result.saveTrace)
|
|
49
|
+
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
|
50
|
+
return result;
|
|
42
51
|
}
|
|
43
52
|
export async function configFromCLIOptions(cliOptions) {
|
|
44
53
|
let browserName;
|
|
@@ -66,17 +75,51 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
66
75
|
browserName = 'chromium';
|
|
67
76
|
channel = 'chrome';
|
|
68
77
|
}
|
|
78
|
+
// Launch options
|
|
69
79
|
const launchOptions = {
|
|
70
80
|
channel,
|
|
71
81
|
executablePath: cliOptions.executablePath,
|
|
72
82
|
headless: cliOptions.headless,
|
|
73
83
|
};
|
|
74
|
-
if (browserName === 'chromium')
|
|
84
|
+
if (browserName === 'chromium') {
|
|
75
85
|
launchOptions.cdpPort = await findFreePort();
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
if (!cliOptions.sandbox) {
|
|
87
|
+
// --no-sandbox was passed, disable the sandbox
|
|
88
|
+
launchOptions.chromiumSandbox = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (cliOptions.proxyServer) {
|
|
92
|
+
launchOptions.proxy = {
|
|
93
|
+
server: cliOptions.proxyServer
|
|
94
|
+
};
|
|
95
|
+
if (cliOptions.proxyBypass)
|
|
96
|
+
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
97
|
+
}
|
|
98
|
+
// Context options
|
|
99
|
+
const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
100
|
+
if (cliOptions.storageState)
|
|
101
|
+
contextOptions.storageState = cliOptions.storageState;
|
|
102
|
+
if (cliOptions.userAgent)
|
|
103
|
+
contextOptions.userAgent = cliOptions.userAgent;
|
|
104
|
+
if (cliOptions.viewportSize) {
|
|
105
|
+
try {
|
|
106
|
+
const [width, height] = cliOptions.viewportSize.split(',').map(n => +n);
|
|
107
|
+
if (isNaN(width) || isNaN(height))
|
|
108
|
+
throw new Error('bad values');
|
|
109
|
+
contextOptions.viewport = { width, height };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (cliOptions.ignoreHttpsErrors)
|
|
116
|
+
contextOptions.ignoreHTTPSErrors = true;
|
|
117
|
+
if (cliOptions.blockServiceWorkers)
|
|
118
|
+
contextOptions.serviceWorkers = 'block';
|
|
119
|
+
const result = {
|
|
78
120
|
browser: {
|
|
79
121
|
browserName,
|
|
122
|
+
isolated: cliOptions.isolated,
|
|
80
123
|
userDataDir: cliOptions.userDataDir,
|
|
81
124
|
launchOptions,
|
|
82
125
|
contextOptions,
|
|
@@ -92,8 +135,14 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
92
135
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
93
136
|
blockedOrigins: cliOptions.blockedOrigins,
|
|
94
137
|
},
|
|
138
|
+
saveTrace: cliOptions.saveTrace,
|
|
95
139
|
outputDir: cliOptions.outputDir,
|
|
96
140
|
};
|
|
141
|
+
if (!cliOptions.imageResponses) {
|
|
142
|
+
// --no-image-responses was passed, disable image responses
|
|
143
|
+
result.noImageResponses = true;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
97
146
|
}
|
|
98
147
|
async function findFreePort() {
|
|
99
148
|
return new Promise((resolve, reject) => {
|
|
@@ -116,18 +165,17 @@ async function loadConfig(configFile) {
|
|
|
116
165
|
}
|
|
117
166
|
}
|
|
118
167
|
export async function outputFile(config, name) {
|
|
119
|
-
|
|
120
|
-
await fs.promises.mkdir(result, { recursive: true });
|
|
168
|
+
await fs.promises.mkdir(config.outputDir, { recursive: true });
|
|
121
169
|
const fileName = sanitizeForFilePath(name);
|
|
122
|
-
return path.join(
|
|
170
|
+
return path.join(config.outputDir, fileName);
|
|
123
171
|
}
|
|
124
172
|
function pickDefined(obj) {
|
|
125
173
|
return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined));
|
|
126
174
|
}
|
|
127
175
|
function mergeConfig(base, overrides) {
|
|
128
176
|
const browser = {
|
|
129
|
-
|
|
130
|
-
|
|
177
|
+
browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium',
|
|
178
|
+
isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
|
|
131
179
|
launchOptions: {
|
|
132
180
|
...pickDefined(base.browser?.launchOptions),
|
|
133
181
|
...pickDefined(overrides.browser?.launchOptions),
|
|
@@ -137,6 +185,9 @@ function mergeConfig(base, overrides) {
|
|
|
137
185
|
...pickDefined(base.browser?.contextOptions),
|
|
138
186
|
...pickDefined(overrides.browser?.contextOptions),
|
|
139
187
|
},
|
|
188
|
+
userDataDir: overrides.browser?.userDataDir ?? base.browser?.userDataDir,
|
|
189
|
+
cdpEndpoint: overrides.browser?.cdpEndpoint ?? base.browser?.cdpEndpoint,
|
|
190
|
+
remoteEndpoint: overrides.browser?.remoteEndpoint ?? base.browser?.remoteEndpoint,
|
|
140
191
|
};
|
|
141
192
|
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
|
142
193
|
delete browser.launchOptions.channel;
|
|
@@ -147,6 +198,6 @@ function mergeConfig(base, overrides) {
|
|
|
147
198
|
network: {
|
|
148
199
|
...pickDefined(base.network),
|
|
149
200
|
...pickDefined(overrides.network),
|
|
150
|
-
}
|
|
201
|
+
}
|
|
151
202
|
};
|
|
152
203
|
}
|
package/lib/connection.js
CHANGED
|
@@ -17,9 +17,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
19
19
|
import { Context, packageJSON } from './context.js';
|
|
20
|
-
import { snapshotTools,
|
|
20
|
+
import { snapshotTools, visionTools } from './tools.js';
|
|
21
21
|
export async function createConnection(config) {
|
|
22
|
-
const allTools = config.vision ?
|
|
22
|
+
const allTools = config.vision ? visionTools : snapshotTools;
|
|
23
23
|
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
24
24
|
const context = new Context(tools, config);
|
|
25
25
|
const server = new Server({ name: 'Playwright', version: packageJSON.version }, {
|
package/lib/context.js
CHANGED
|
@@ -18,16 +18,14 @@ import url from 'node:url';
|
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import * as playwright from 'playwright';
|
|
21
|
-
import { waitForCompletion } from './tools/utils.js';
|
|
21
|
+
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
|
22
22
|
import { ManualPromise } from './manualPromise.js';
|
|
23
23
|
import { Tab } from './tab.js';
|
|
24
24
|
import { outputFile } from './config.js';
|
|
25
25
|
export class Context {
|
|
26
26
|
tools;
|
|
27
27
|
config;
|
|
28
|
-
|
|
29
|
-
_browserContext;
|
|
30
|
-
_createBrowserContextPromise;
|
|
28
|
+
_browserContextPromise;
|
|
31
29
|
_tabs = [];
|
|
32
30
|
_currentTab;
|
|
33
31
|
_modalStates = [];
|
|
@@ -65,7 +63,7 @@ export class Context {
|
|
|
65
63
|
return this._currentTab;
|
|
66
64
|
}
|
|
67
65
|
async newTab() {
|
|
68
|
-
const browserContext = await this._ensureBrowserContext();
|
|
66
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
69
67
|
const page = await browserContext.newPage();
|
|
70
68
|
this._currentTab = this._tabs.find(t => t.page === page);
|
|
71
69
|
return this._currentTab;
|
|
@@ -75,9 +73,9 @@ export class Context {
|
|
|
75
73
|
await this._currentTab.page.bringToFront();
|
|
76
74
|
}
|
|
77
75
|
async ensureTab() {
|
|
78
|
-
const
|
|
76
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
79
77
|
if (!this._currentTab)
|
|
80
|
-
await
|
|
78
|
+
await browserContext.newPage();
|
|
81
79
|
return this._currentTab;
|
|
82
80
|
}
|
|
83
81
|
async listTabsMarkdown() {
|
|
@@ -86,7 +84,7 @@ export class Context {
|
|
|
86
84
|
const lines = ['### Open tabs'];
|
|
87
85
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
88
86
|
const tab = this._tabs[i];
|
|
89
|
-
const title = await tab.
|
|
87
|
+
const title = await tab.title();
|
|
90
88
|
const url = tab.page.url();
|
|
91
89
|
const current = tab === this._currentTab ? ' (current)' : '';
|
|
92
90
|
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
|
@@ -118,7 +116,7 @@ export class Context {
|
|
|
118
116
|
let actionResult;
|
|
119
117
|
try {
|
|
120
118
|
if (waitForNetwork)
|
|
121
|
-
actionResult = await waitForCompletion(this, tab
|
|
119
|
+
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
|
|
122
120
|
else
|
|
123
121
|
actionResult = await racingAction?.() ?? undefined;
|
|
124
122
|
}
|
|
@@ -155,7 +153,7 @@ ${code.join('\n')}
|
|
|
155
153
|
result.push(await this.listTabsMarkdown(), '');
|
|
156
154
|
if (this.tabs().length > 1)
|
|
157
155
|
result.push('### Current tab');
|
|
158
|
-
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.
|
|
156
|
+
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
|
|
159
157
|
if (captureSnapshot && tab.hasSnapshot())
|
|
160
158
|
result.push(tab.snapshotOrDie().text());
|
|
161
159
|
const content = actionResult?.content ?? [];
|
|
@@ -170,10 +168,13 @@ ${code.join('\n')}
|
|
|
170
168
|
};
|
|
171
169
|
}
|
|
172
170
|
async waitForTimeout(time) {
|
|
173
|
-
if (this._currentTab
|
|
174
|
-
await this._currentTab.page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
175
|
-
else
|
|
171
|
+
if (!this._currentTab || this._javaScriptBlocked()) {
|
|
176
172
|
await new Promise(f => setTimeout(f, time));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await callOnPageNoTrace(this._currentTab.page, page => {
|
|
176
|
+
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
177
|
+
});
|
|
177
178
|
}
|
|
178
179
|
async _raceAgainstModalDialogs(action) {
|
|
179
180
|
this._pendingAction = {
|
|
@@ -226,20 +227,21 @@ ${code.join('\n')}
|
|
|
226
227
|
this._tabs.splice(index, 1);
|
|
227
228
|
if (this._currentTab === tab)
|
|
228
229
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
229
|
-
if (
|
|
230
|
+
if (!this._tabs.length)
|
|
230
231
|
void this.close();
|
|
231
232
|
}
|
|
232
233
|
async close() {
|
|
233
|
-
if (!this.
|
|
234
|
+
if (!this._browserContextPromise)
|
|
234
235
|
return;
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
236
|
+
const promise = this._browserContextPromise;
|
|
237
|
+
this._browserContextPromise = undefined;
|
|
238
|
+
await promise.then(async ({ browserContext, browser }) => {
|
|
239
|
+
if (this.config.saveTrace)
|
|
240
|
+
await browserContext.tracing.stop();
|
|
241
|
+
await browserContext.close().then(async () => {
|
|
242
|
+
await browser?.close();
|
|
243
|
+
}).catch(() => { });
|
|
244
|
+
});
|
|
243
245
|
}
|
|
244
246
|
async _setupRequestInterception(context) {
|
|
245
247
|
if (this.config.network?.allowedOrigins?.length) {
|
|
@@ -252,28 +254,32 @@ ${code.join('\n')}
|
|
|
252
254
|
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
|
-
|
|
256
|
-
if (!this.
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
for (const page of this._browserContext.pages())
|
|
262
|
-
this._onPageCreated(page);
|
|
263
|
-
this._browserContext.on('page', page => this._onPageCreated(page));
|
|
257
|
+
_ensureBrowserContext() {
|
|
258
|
+
if (!this._browserContextPromise) {
|
|
259
|
+
this._browserContextPromise = this._setupBrowserContext();
|
|
260
|
+
this._browserContextPromise.catch(() => {
|
|
261
|
+
this._browserContextPromise = undefined;
|
|
262
|
+
});
|
|
264
263
|
}
|
|
265
|
-
return this.
|
|
264
|
+
return this._browserContextPromise;
|
|
266
265
|
}
|
|
267
|
-
async
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
async _setupBrowserContext() {
|
|
267
|
+
const { browser, browserContext } = await this._createBrowserContext();
|
|
268
|
+
await this._setupRequestInterception(browserContext);
|
|
269
|
+
for (const page of browserContext.pages())
|
|
270
|
+
this._onPageCreated(page);
|
|
271
|
+
browserContext.on('page', page => this._onPageCreated(page));
|
|
272
|
+
if (this.config.saveTrace) {
|
|
273
|
+
await browserContext.tracing.start({
|
|
274
|
+
name: 'trace',
|
|
275
|
+
screenshots: false,
|
|
276
|
+
snapshots: true,
|
|
277
|
+
sources: false,
|
|
272
278
|
});
|
|
273
279
|
}
|
|
274
|
-
return
|
|
280
|
+
return { browser, browserContext };
|
|
275
281
|
}
|
|
276
|
-
async
|
|
282
|
+
async _createBrowserContext() {
|
|
277
283
|
if (this.config.browser?.remoteEndpoint) {
|
|
278
284
|
const url = new URL(this.config.browser?.remoteEndpoint);
|
|
279
285
|
if (this.config.browser.browserName)
|
|
@@ -286,19 +292,35 @@ ${code.join('\n')}
|
|
|
286
292
|
}
|
|
287
293
|
if (this.config.browser?.cdpEndpoint) {
|
|
288
294
|
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
289
|
-
const browserContext = browser.contexts()[0];
|
|
295
|
+
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
290
296
|
return { browser, browserContext };
|
|
291
297
|
}
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
return this.config.browser?.isolated ?
|
|
299
|
+
await createIsolatedContext(this.config.browser) :
|
|
300
|
+
await launchPersistentContext(this.config.browser);
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
|
-
async function
|
|
303
|
+
async function createIsolatedContext(browserConfig) {
|
|
297
304
|
try {
|
|
298
305
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
299
|
-
const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
300
306
|
const browserType = playwright[browserName];
|
|
301
|
-
|
|
307
|
+
const browser = await browserType.launch(browserConfig.launchOptions);
|
|
308
|
+
const browserContext = await browser.newContext(browserConfig.contextOptions);
|
|
309
|
+
return { browser, browserContext };
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error.message.includes('Executable doesn\'t exist'))
|
|
313
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function launchPersistentContext(browserConfig) {
|
|
318
|
+
try {
|
|
319
|
+
const browserName = browserConfig.browserName ?? 'chromium';
|
|
320
|
+
const userDataDir = browserConfig.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
321
|
+
const browserType = playwright[browserName];
|
|
322
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig.launchOptions, ...browserConfig.contextOptions });
|
|
323
|
+
return { browserContext };
|
|
302
324
|
}
|
|
303
325
|
catch (error) {
|
|
304
326
|
if (error.message.includes('Executable doesn\'t exist'))
|
|
@@ -316,12 +338,9 @@ async function createUserDataDir(browserConfig) {
|
|
|
316
338
|
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
317
339
|
else
|
|
318
340
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
319
|
-
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig
|
|
341
|
+
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
320
342
|
await fs.promises.mkdir(result, { recursive: true });
|
|
321
343
|
return result;
|
|
322
344
|
}
|
|
323
|
-
export async function generateLocator(locator) {
|
|
324
|
-
return locator._generateLocatorString();
|
|
325
|
-
}
|
|
326
345
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
327
346
|
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { createConnection as createConnectionImpl } from './connection.js';
|
|
17
|
-
|
|
17
|
+
import { resolveConfig } from './config.js';
|
|
18
|
+
export async function createConnection(userConfig = {}) {
|
|
19
|
+
const config = await resolveConfig(userConfig);
|
|
18
20
|
return createConnectionImpl(config);
|
|
19
21
|
}
|
package/lib/pageSnapshot.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { callOnPageNoTrace } from './tools/utils.js';
|
|
16
17
|
export class PageSnapshot {
|
|
17
18
|
_page;
|
|
18
19
|
_text;
|
|
@@ -28,11 +29,14 @@ export class PageSnapshot {
|
|
|
28
29
|
return this._text;
|
|
29
30
|
}
|
|
30
31
|
async _build() {
|
|
31
|
-
|
|
32
|
+
// FIXME: Rountrip evaluate to ensure _snapshotForAI works.
|
|
33
|
+
// This probably broke once we moved off locator snapshots
|
|
34
|
+
await this._page.evaluate(() => 1);
|
|
35
|
+
const snapshot = await callOnPageNoTrace(this._page, page => page._snapshotForAI());
|
|
32
36
|
this._text = [
|
|
33
37
|
`- Page Snapshot`,
|
|
34
38
|
'```yaml',
|
|
35
|
-
|
|
39
|
+
snapshot,
|
|
36
40
|
'```',
|
|
37
41
|
].join('\n');
|
|
38
42
|
}
|
package/lib/program.js
CHANGED
|
@@ -15,34 +15,53 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { program } from 'commander';
|
|
17
17
|
import { startHttpTransport, startStdioTransport } from './transport.js';
|
|
18
|
-
import {
|
|
18
|
+
import { resolveCLIConfig } from './config.js';
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|
19
21
|
import { packageJSON } from './context.js';
|
|
20
22
|
program
|
|
21
23
|
.version('Version ' + packageJSON.version)
|
|
22
24
|
.name(packageJSON.name)
|
|
23
|
-
.option('--
|
|
24
|
-
.option('--
|
|
25
|
+
.option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
|
26
|
+
.option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
|
27
|
+
.option('--block-service-workers', 'block service workers')
|
|
28
|
+
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
29
|
+
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
|
25
30
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
26
|
-
.option('--
|
|
27
|
-
.option('--
|
|
28
|
-
.option('--
|
|
29
|
-
.option('--
|
|
30
|
-
.option('--
|
|
31
|
-
.option('--
|
|
32
|
-
.option('--
|
|
33
|
-
.option('--
|
|
31
|
+
.option('--config <path>', 'path to the configuration file.')
|
|
32
|
+
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
|
|
33
|
+
.option('--executable-path <path>', 'path to the browser executable.')
|
|
34
|
+
.option('--headless', 'run browser in headless mode, headed by default')
|
|
35
|
+
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
36
|
+
.option('--ignore-https-errors', 'ignore https errors')
|
|
37
|
+
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
38
|
+
.option('--no-image-responses', 'do not send image responses to the client.')
|
|
39
|
+
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
40
|
+
.option('--output-dir <path>', 'path to the directory for output files.')
|
|
41
|
+
.option('--port <port>', 'port to listen on for SSE transport.')
|
|
42
|
+
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
|
43
|
+
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
|
44
|
+
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
|
|
45
|
+
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
|
46
|
+
.option('--user-agent <ua string>', 'specify user agent string')
|
|
47
|
+
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
48
|
+
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
34
49
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
35
|
-
.option('--no-image-responses', 'Do not send image responses to the client.')
|
|
36
|
-
.option('--output-dir <path>', 'Path to the directory for output files.')
|
|
37
|
-
.option('--config <path>', 'Path to the configuration file.')
|
|
38
50
|
.action(async (options) => {
|
|
39
|
-
const config = await
|
|
51
|
+
const config = await resolveCLIConfig(options);
|
|
40
52
|
const connectionList = [];
|
|
41
53
|
setupExitWatchdog(connectionList);
|
|
42
54
|
if (options.port)
|
|
43
55
|
startHttpTransport(config, +options.port, options.host, connectionList);
|
|
44
56
|
else
|
|
45
57
|
await startStdioTransport(config, connectionList);
|
|
58
|
+
if (config.saveTrace) {
|
|
59
|
+
const server = await startTraceViewerServer();
|
|
60
|
+
const urlPrefix = server.urlPrefix('human-readable');
|
|
61
|
+
const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.error('\nTrace viewer listening on ' + url);
|
|
64
|
+
}
|
|
46
65
|
});
|
|
47
66
|
function setupExitWatchdog(connectionList) {
|
|
48
67
|
const handleExit = async () => {
|
package/lib/tab.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { PageSnapshot } from './pageSnapshot.js';
|
|
17
|
+
import { callOnPageNoTrace } from './tools/utils.js';
|
|
17
18
|
export class Tab {
|
|
18
19
|
context;
|
|
19
20
|
page;
|
|
@@ -51,9 +52,15 @@ export class Tab {
|
|
|
51
52
|
this._clearCollectedArtifacts();
|
|
52
53
|
this._onPageClose(this);
|
|
53
54
|
}
|
|
55
|
+
async title() {
|
|
56
|
+
return await callOnPageNoTrace(this.page, page => page.title());
|
|
57
|
+
}
|
|
58
|
+
async waitForLoadState(state, options) {
|
|
59
|
+
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(() => { }));
|
|
60
|
+
}
|
|
54
61
|
async navigate(url) {
|
|
55
62
|
this._clearCollectedArtifacts();
|
|
56
|
-
const downloadEvent = this.page.waitForEvent('download').catch(() => { });
|
|
63
|
+
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(() => { }));
|
|
57
64
|
try {
|
|
58
65
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
66
|
}
|
|
@@ -72,7 +79,7 @@ export class Tab {
|
|
|
72
79
|
throw e;
|
|
73
80
|
}
|
|
74
81
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
75
|
-
await this.
|
|
82
|
+
await this.waitForLoadState('load', { timeout: 5000 });
|
|
76
83
|
}
|
|
77
84
|
hasSnapshot() {
|
|
78
85
|
return !!this._snapshot;
|
package/lib/tools/common.js
CHANGED
|
@@ -15,45 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
|
-
const wait = captureSnapshot => defineTool({
|
|
19
|
-
capability: 'wait',
|
|
20
|
-
schema: {
|
|
21
|
-
name: 'browser_wait_for',
|
|
22
|
-
title: 'Wait for',
|
|
23
|
-
description: 'Wait for text to appear or disappear or a specified time to pass',
|
|
24
|
-
inputSchema: z.object({
|
|
25
|
-
time: z.number().optional().describe('The time to wait in seconds'),
|
|
26
|
-
text: z.string().optional().describe('The text to wait for'),
|
|
27
|
-
textGone: z.string().optional().describe('The text to wait for to disappear'),
|
|
28
|
-
}),
|
|
29
|
-
type: 'readOnly',
|
|
30
|
-
},
|
|
31
|
-
handle: async (context, params) => {
|
|
32
|
-
if (!params.text && !params.textGone && !params.time)
|
|
33
|
-
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
-
const code = [];
|
|
35
|
-
if (params.time) {
|
|
36
|
-
code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
|
-
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
38
|
-
}
|
|
39
|
-
const tab = context.currentTabOrDie();
|
|
40
|
-
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
|
-
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
|
-
if (goneLocator) {
|
|
43
|
-
code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
|
-
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
|
-
}
|
|
46
|
-
if (locator) {
|
|
47
|
-
code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
|
-
await locator.waitFor({ state: 'visible' });
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
code,
|
|
52
|
-
captureSnapshot,
|
|
53
|
-
waitForNetwork: false,
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
18
|
const close = defineTool({
|
|
58
19
|
capability: 'core',
|
|
59
20
|
schema: {
|
|
@@ -66,7 +27,7 @@ const close = defineTool({
|
|
|
66
27
|
handle: async (context) => {
|
|
67
28
|
await context.close();
|
|
68
29
|
return {
|
|
69
|
-
code: [
|
|
30
|
+
code: [`await page.close()`],
|
|
70
31
|
captureSnapshot: false,
|
|
71
32
|
waitForNetwork: false,
|
|
72
33
|
};
|
|
@@ -103,6 +64,5 @@ const resize = captureSnapshot => defineTool({
|
|
|
103
64
|
});
|
|
104
65
|
export default (captureSnapshot) => [
|
|
105
66
|
close,
|
|
106
|
-
wait(captureSnapshot),
|
|
107
67
|
resize(captureSnapshot)
|
|
108
68
|
];
|