@monoes/monobrowse 1.0.13 → 1.0.15
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/dist/src/__tests__/bridge-instruments.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-instruments.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-instruments.test.js +132 -0
- package/dist/src/__tests__/bridge-instruments.test.js.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.js +287 -0
- package/dist/src/__tests__/bridge-transport.test.js.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.js +167 -0
- package/dist/src/__tests__/cdp-transport.test.js.map +1 -0
- package/dist/src/browser/bridge.d.ts +119 -0
- package/dist/src/browser/bridge.d.ts.map +1 -0
- package/dist/src/browser/bridge.js +264 -0
- package/dist/src/browser/bridge.js.map +1 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +11 -0
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +11 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +60 -56
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/index.d.ts +2 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +2 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/transport.d.ts +62 -0
- package/dist/src/browser/transport.d.ts.map +1 -0
- package/dist/src/browser/transport.js +65 -0
- package/dist/src/browser/transport.js.map +1 -0
- package/dist/src/cli/commander-adapter.d.ts +16 -0
- package/dist/src/cli/commander-adapter.d.ts.map +1 -0
- package/dist/src/cli/commander-adapter.js +68 -0
- package/dist/src/cli/commander-adapter.js.map +1 -0
- package/dist/src/cli/commands-context.d.ts +14 -0
- package/dist/src/cli/commands-context.d.ts.map +1 -0
- package/dist/src/cli/commands-context.js +351 -0
- package/dist/src/cli/commands-context.js.map +1 -0
- package/dist/src/cli/commands-data.d.ts +12 -0
- package/dist/src/cli/commands-data.d.ts.map +1 -0
- package/dist/src/cli/commands-data.js +246 -0
- package/dist/src/cli/commands-data.js.map +1 -0
- package/dist/src/cli/commands-element.d.ts +15 -0
- package/dist/src/cli/commands-element.d.ts.map +1 -0
- package/dist/src/cli/commands-element.js +262 -0
- package/dist/src/cli/commands-element.js.map +1 -0
- package/dist/src/cli/commands-files.d.ts +9 -0
- package/dist/src/cli/commands-files.d.ts.map +1 -0
- package/dist/src/cli/commands-files.js +207 -0
- package/dist/src/cli/commands-files.js.map +1 -0
- package/dist/src/cli/commands-input.d.ts +26 -0
- package/dist/src/cli/commands-input.d.ts.map +1 -0
- package/dist/src/cli/commands-input.js +399 -0
- package/dist/src/cli/commands-input.js.map +1 -0
- package/dist/src/cli/commands-navigate.d.ts +12 -0
- package/dist/src/cli/commands-navigate.d.ts.map +1 -0
- package/dist/src/cli/commands-navigate.js +389 -0
- package/dist/src/cli/commands-navigate.js.map +1 -0
- package/dist/src/cli/commands-page.d.ts +25 -0
- package/dist/src/cli/commands-page.d.ts.map +1 -0
- package/dist/src/cli/commands-page.js +377 -0
- package/dist/src/cli/commands-page.js.map +1 -0
- package/dist/src/cli/commands-report.d.ts +8 -0
- package/dist/src/cli/commands-report.d.ts.map +1 -0
- package/dist/src/cli/commands-report.js +256 -0
- package/dist/src/cli/commands-report.js.map +1 -0
- package/dist/src/cli/commands-script.d.ts +43 -0
- package/dist/src/cli/commands-script.d.ts.map +1 -0
- package/dist/src/cli/commands-script.js +168 -0
- package/dist/src/cli/commands-script.js.map +1 -0
- package/dist/src/cli/commands-session.d.ts +15 -0
- package/dist/src/cli/commands-session.d.ts.map +1 -0
- package/dist/src/cli/commands-session.js +357 -0
- package/dist/src/cli/commands-session.js.map +1 -0
- package/dist/src/cli/commands-trace.d.ts +12 -0
- package/dist/src/cli/commands-trace.d.ts.map +1 -0
- package/dist/src/cli/commands-trace.js +347 -0
- package/dist/src/cli/commands-trace.js.map +1 -0
- package/dist/src/cli/commands.d.ts +22 -42
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +42 -3648
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/session.d.ts +47 -0
- package/dist/src/cli/session.d.ts.map +1 -0
- package/dist/src/cli/session.js +343 -0
- package/dist/src/cli/session.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/bridge-instruments.test.ts +180 -0
- package/src/__tests__/bridge-transport.test.ts +365 -0
- package/src/__tests__/cdp-transport.test.ts +198 -0
- package/src/browser/bridge.ts +354 -0
- package/src/browser/browser.ts +12 -0
- package/src/browser/cdp.ts +57 -52
- package/src/browser/index.ts +2 -0
- package/src/browser/transport.ts +111 -0
- package/src/cli/commander-adapter.ts +65 -0
- package/src/cli/commands-context.ts +367 -0
- package/src/cli/commands-data.ts +248 -0
- package/src/cli/commands-element.ts +305 -0
- package/src/cli/commands-files.ts +248 -0
- package/src/cli/commands-input.ts +434 -0
- package/src/cli/commands-navigate.ts +442 -0
- package/src/cli/commands-page.ts +420 -0
- package/src/cli/commands-report.ts +275 -0
- package/src/cli/commands-script.ts +183 -0
- package/src/cli/commands-session.ts +376 -0
- package/src/cli/commands-trace.ts +349 -0
- package/src/cli/commands.ts +95 -3943
- package/src/cli/session.ts +412 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving around and configuring the viewport — history navigation, scrolling,
|
|
3
|
+
* SPA pushState, device/viewport/user-agent settings, and waiting for the page
|
|
4
|
+
* to reach a condition.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ElementRef } from '../index.js';
|
|
8
|
+
import { output } from './output.js';
|
|
9
|
+
import { ensureConnected, getBrowser, session } from './session.js';
|
|
10
|
+
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
11
|
+
|
|
12
|
+
export const waitCommand: Command = {
|
|
13
|
+
name: 'wait',
|
|
14
|
+
description: 'Wait for a condition to be met before proceeding',
|
|
15
|
+
options: [
|
|
16
|
+
{ name: 'url', type: 'string', description: 'Wait for URL matching glob pattern' },
|
|
17
|
+
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
18
|
+
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
19
|
+
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
20
|
+
{
|
|
21
|
+
name: 'load',
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Wait for load event: load|networkidle|domcontentloaded',
|
|
24
|
+
},
|
|
25
|
+
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
26
|
+
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
27
|
+
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
28
|
+
{
|
|
29
|
+
name: 'download',
|
|
30
|
+
type: 'string',
|
|
31
|
+
description:
|
|
32
|
+
'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
36
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
37
|
+
const browser = await getBrowser();
|
|
38
|
+
|
|
39
|
+
if (ctx.flags.ms) {
|
|
40
|
+
const rawMs = ctx.flags.ms as number;
|
|
41
|
+
const waitMs = Number.isFinite(rawMs) ? Math.max(0, Math.min(rawMs, 60_000)) : 0; // cap at 60s
|
|
42
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
43
|
+
output.printSuccess(`Waited ${ctx.flags.ms}ms`);
|
|
44
|
+
return { success: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (ctx.flags.fn) {
|
|
48
|
+
const expr = ctx.flags.fn as string;
|
|
49
|
+
const rawTimeout = (ctx.flags.timeout as number) ?? 30000;
|
|
50
|
+
const timeout = Number.isFinite(rawTimeout)
|
|
51
|
+
? Math.max(100, Math.min(rawTimeout, 300_000))
|
|
52
|
+
: 30000; // cap at 5min
|
|
53
|
+
const interval = 200;
|
|
54
|
+
const deadline = Date.now() + timeout;
|
|
55
|
+
while (Date.now() < deadline) {
|
|
56
|
+
const result = await browser.evaluateJs(client, sessionId, expr);
|
|
57
|
+
if (result) {
|
|
58
|
+
output.printSuccess('Wait function returned truthy');
|
|
59
|
+
return { success: true };
|
|
60
|
+
}
|
|
61
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Timeout waiting for --fn: ${expr}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ctx.flags['not-text']) {
|
|
67
|
+
const target = ctx.flags['not-text'] as string;
|
|
68
|
+
const timeout = (ctx.flags.timeout as number) ?? 30000;
|
|
69
|
+
const interval = 200;
|
|
70
|
+
const deadline = Date.now() + timeout;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
const text = (await browser.evaluateJs(
|
|
73
|
+
client,
|
|
74
|
+
sessionId,
|
|
75
|
+
'document.body?.innerText ?? ""',
|
|
76
|
+
)) as string;
|
|
77
|
+
if (!text.includes(target)) {
|
|
78
|
+
output.printSuccess('Text disappeared');
|
|
79
|
+
return { success: true };
|
|
80
|
+
}
|
|
81
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Timeout waiting for text to disappear: "${target}"`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ctx.flags.download) {
|
|
87
|
+
const savePath = ctx.flags.download as string;
|
|
88
|
+
const { mkdir } = await import('node:fs/promises');
|
|
89
|
+
const { dirname, join } = await import('node:path');
|
|
90
|
+
const { tmpdir } = await import('node:os');
|
|
91
|
+
const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
|
|
92
|
+
await mkdir(downloadDir, { recursive: true });
|
|
93
|
+
await client
|
|
94
|
+
.send(
|
|
95
|
+
'Browser.setDownloadBehavior',
|
|
96
|
+
{
|
|
97
|
+
behavior: 'allow',
|
|
98
|
+
downloadPath: downloadDir,
|
|
99
|
+
eventsEnabled: true,
|
|
100
|
+
},
|
|
101
|
+
undefined,
|
|
102
|
+
)
|
|
103
|
+
.catch(() =>
|
|
104
|
+
client
|
|
105
|
+
.send(
|
|
106
|
+
'Page.setDownloadBehavior',
|
|
107
|
+
{ behavior: 'allow', downloadPath: downloadDir },
|
|
108
|
+
sessionId,
|
|
109
|
+
)
|
|
110
|
+
.catch(() => {}),
|
|
111
|
+
);
|
|
112
|
+
const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
|
|
113
|
+
const rawTimeout = Math.min((ctx.flags.timeout as number) ?? 30000, MAX_DOWNLOAD_TIMEOUT);
|
|
114
|
+
const finalPath = await new Promise<string>((resolve, reject) => {
|
|
115
|
+
let guid = '';
|
|
116
|
+
let settled = false;
|
|
117
|
+
// C2: capture off() functions to avoid listener leaks
|
|
118
|
+
const offBegin = client.on(
|
|
119
|
+
'Browser.downloadWillBegin',
|
|
120
|
+
(params: Record<string, unknown>) => {
|
|
121
|
+
guid = params.guid as string;
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
let offProgress: (() => void) | undefined;
|
|
125
|
+
// cleanup defined before setTimeout so the timeout callback can call it
|
|
126
|
+
let tid: ReturnType<typeof setTimeout>;
|
|
127
|
+
let pollTid: ReturnType<typeof setInterval> | undefined;
|
|
128
|
+
const cleanup = () => {
|
|
129
|
+
clearTimeout(tid);
|
|
130
|
+
clearInterval(pollTid);
|
|
131
|
+
offBegin?.();
|
|
132
|
+
offProgress?.();
|
|
133
|
+
};
|
|
134
|
+
const finish = (path: string) => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
cleanup();
|
|
138
|
+
resolve(path);
|
|
139
|
+
};
|
|
140
|
+
const fail = (err: Error) => {
|
|
141
|
+
if (settled) return;
|
|
142
|
+
settled = true;
|
|
143
|
+
cleanup();
|
|
144
|
+
reject(err);
|
|
145
|
+
};
|
|
146
|
+
tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
|
|
147
|
+
offProgress = client.on(
|
|
148
|
+
'Browser.downloadProgress',
|
|
149
|
+
async (params: Record<string, unknown>) => {
|
|
150
|
+
if (params.guid === guid && params.state === 'completed') {
|
|
151
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
152
|
+
const files = await readdir(downloadDir);
|
|
153
|
+
if (files.length > 0) {
|
|
154
|
+
const src = join(downloadDir, files[0]);
|
|
155
|
+
await mkdir(dirname(savePath), { recursive: true });
|
|
156
|
+
await rename(src, savePath);
|
|
157
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
158
|
+
finish(savePath);
|
|
159
|
+
} else {
|
|
160
|
+
await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
|
|
161
|
+
fail(new Error('Download completed but no file found'));
|
|
162
|
+
}
|
|
163
|
+
} else if (params.guid === guid && params.state === 'canceled') {
|
|
164
|
+
fail(new Error('Download was canceled'));
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Fallback for the empty-guid race: this process's listener attaches
|
|
170
|
+
// AFTER a separate `click` process may have already started (and even
|
|
171
|
+
// finished) the download — so downloadWillBegin/downloadProgress for it
|
|
172
|
+
// were never observed here. While guid is still empty, poll the expected
|
|
173
|
+
// output path directly: if it already exists with a stable, non-zero
|
|
174
|
+
// size across two consecutive checks, treat the download as complete.
|
|
175
|
+
let lastSize = -1;
|
|
176
|
+
let stableReads = 0;
|
|
177
|
+
pollTid = setInterval(async () => {
|
|
178
|
+
if (settled || guid) return; // a real CDP event has taken over
|
|
179
|
+
try {
|
|
180
|
+
const { stat } = await import('node:fs/promises');
|
|
181
|
+
const st = await stat(savePath);
|
|
182
|
+
if (st.isFile() && st.size > 0 && st.size === lastSize) {
|
|
183
|
+
stableReads++;
|
|
184
|
+
if (stableReads >= 2) finish(savePath);
|
|
185
|
+
} else {
|
|
186
|
+
stableReads = 0;
|
|
187
|
+
}
|
|
188
|
+
lastSize = st.size;
|
|
189
|
+
} catch {
|
|
190
|
+
// savePath not present yet — keep polling until timeout
|
|
191
|
+
}
|
|
192
|
+
}, 300);
|
|
193
|
+
});
|
|
194
|
+
output.printSuccess(`Download saved: ${finalPath}`);
|
|
195
|
+
return { success: true, data: { path: finalPath } };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await browser.waitFor(client, sessionId, {
|
|
199
|
+
url: ctx.flags.url as string,
|
|
200
|
+
text: ctx.flags.text as string,
|
|
201
|
+
selector: ctx.flags.selector as string,
|
|
202
|
+
load: ctx.flags.load as 'load' | 'networkidle' | 'domcontentloaded',
|
|
203
|
+
timeout: ctx.flags.timeout as number,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
output.printSuccess('Wait condition met');
|
|
207
|
+
return { success: true };
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const scrollCommand: Command = {
|
|
212
|
+
name: 'scroll',
|
|
213
|
+
description:
|
|
214
|
+
'Scroll the page. Usage: monomind browse scroll up|down|left|right [amount] [--selector ".sidebar"]',
|
|
215
|
+
options: [
|
|
216
|
+
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
217
|
+
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
218
|
+
{
|
|
219
|
+
name: 'selector',
|
|
220
|
+
short: 's',
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'CSS selector of element to scroll within',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
226
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
227
|
+
const browser = await getBrowser();
|
|
228
|
+
|
|
229
|
+
const direction = ctx.args[0] as 'up' | 'down' | 'left' | 'right';
|
|
230
|
+
if (!direction) throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
|
|
231
|
+
|
|
232
|
+
// Support positional amount: scroll down 300
|
|
233
|
+
const positionalAmount =
|
|
234
|
+
ctx.args[1] !== undefined ? parseInt(ctx.args[1] as string, 10) : undefined;
|
|
235
|
+
const amount =
|
|
236
|
+
positionalAmount && Number.isFinite(positionalAmount)
|
|
237
|
+
? positionalAmount
|
|
238
|
+
: ((ctx.flags.amount as number) ?? 300);
|
|
239
|
+
|
|
240
|
+
if (ctx.flags.selector) {
|
|
241
|
+
const sel = ctx.flags.selector as string;
|
|
242
|
+
const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
|
|
243
|
+
const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
|
|
244
|
+
const posJson = (await browser.evaluateJs(
|
|
245
|
+
client,
|
|
246
|
+
sessionId,
|
|
247
|
+
`(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`,
|
|
248
|
+
)) as string | null;
|
|
249
|
+
if (!posJson) throw new Error(`Selector not found: ${sel}`);
|
|
250
|
+
const pos = JSON.parse(posJson) as { x: number; y: number };
|
|
251
|
+
await client.send(
|
|
252
|
+
'Input.dispatchMouseEvent',
|
|
253
|
+
{ type: 'mouseWheel', x: pos.x, y: pos.y, deltaX: dx, deltaY: dy },
|
|
254
|
+
sessionId,
|
|
255
|
+
);
|
|
256
|
+
output.printSuccess(`Scrolled ${direction} in ${sel}`);
|
|
257
|
+
return { success: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let ref: ElementRef | undefined;
|
|
261
|
+
if (ctx.flags.ref) {
|
|
262
|
+
const refKey = (ctx.flags.ref as string).startsWith('@')
|
|
263
|
+
? (ctx.flags.ref as string).slice(1)
|
|
264
|
+
: (ctx.flags.ref as string);
|
|
265
|
+
ref = session.refs.get(refKey);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await browser.scrollElement(client, sessionId, direction, amount, ref);
|
|
269
|
+
output.printSuccess(`Scrolled ${direction}`);
|
|
270
|
+
return { success: true };
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const navigateCommand: Command = {
|
|
275
|
+
name: 'navigate',
|
|
276
|
+
description: 'Navigate browser history. Usage: monomind browse navigate back|forward|reload',
|
|
277
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
278
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
279
|
+
const browser = await getBrowser();
|
|
280
|
+
|
|
281
|
+
const direction = ctx.args[0] as string;
|
|
282
|
+
if (!direction) throw new Error('Usage: monomind browse navigate back|forward|reload');
|
|
283
|
+
|
|
284
|
+
if (direction === 'back' || direction === 'forward') {
|
|
285
|
+
// Pre-register frame-start listener BEFORE JS navigation to avoid the race
|
|
286
|
+
// where history.back/forward() returns before the browser issues any requests
|
|
287
|
+
let offFrameStarted: () => void = () => {};
|
|
288
|
+
const frameStartedPromise = new Promise<void>((resolve) => {
|
|
289
|
+
offFrameStarted = client.on('Page.frameStartedLoading', (_params, sid) => {
|
|
290
|
+
if (sid === sessionId) {
|
|
291
|
+
const off = offFrameStarted;
|
|
292
|
+
offFrameStarted = () => {};
|
|
293
|
+
off();
|
|
294
|
+
resolve();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
await client.send(
|
|
300
|
+
'Runtime.evaluate',
|
|
301
|
+
{
|
|
302
|
+
expression: direction === 'back' ? 'history.back()' : 'history.forward()',
|
|
303
|
+
},
|
|
304
|
+
sessionId,
|
|
305
|
+
);
|
|
306
|
+
let fallbackHandle: ReturnType<typeof setTimeout> | undefined;
|
|
307
|
+
const fallbackPromise = new Promise<void>((r) => {
|
|
308
|
+
fallbackHandle = setTimeout(r, 2000);
|
|
309
|
+
});
|
|
310
|
+
await Promise.race([frameStartedPromise, fallbackPromise]);
|
|
311
|
+
if (fallbackHandle !== undefined) clearTimeout(fallbackHandle);
|
|
312
|
+
} finally {
|
|
313
|
+
offFrameStarted();
|
|
314
|
+
}
|
|
315
|
+
await browser.waitForLoad(client, sessionId, 'networkidle');
|
|
316
|
+
} else if (direction === 'reload') {
|
|
317
|
+
await client.send('Page.reload', {}, sessionId);
|
|
318
|
+
await browser.waitForLoad(client, sessionId, 'load');
|
|
319
|
+
} else {
|
|
320
|
+
throw new Error(`Unknown direction: ${direction}. Use: back|forward|reload`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Refs captured before this navigation may now point at different content.
|
|
324
|
+
session.refs = new Map();
|
|
325
|
+
await browser.clearRefCache();
|
|
326
|
+
|
|
327
|
+
output.printSuccess(`Navigated: ${direction}`);
|
|
328
|
+
return { success: true };
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const setCommand: Command = {
|
|
333
|
+
name: 'set',
|
|
334
|
+
description:
|
|
335
|
+
'Configure browser settings. Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
336
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
337
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
338
|
+
const browser = await getBrowser();
|
|
339
|
+
|
|
340
|
+
const setting = ctx.args[0] as string;
|
|
341
|
+
if (!setting)
|
|
342
|
+
throw new Error(
|
|
343
|
+
'Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
switch (setting) {
|
|
347
|
+
case 'viewport': {
|
|
348
|
+
const width = parseInt(ctx.args[1] as string, 10);
|
|
349
|
+
const height = parseInt(ctx.args[2] as string, 10);
|
|
350
|
+
const dpr = parseFloat(ctx.args[3] as string) || undefined;
|
|
351
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
352
|
+
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
353
|
+
await client.send(
|
|
354
|
+
'Emulation.setDeviceMetricsOverride',
|
|
355
|
+
{
|
|
356
|
+
width,
|
|
357
|
+
height,
|
|
358
|
+
deviceScaleFactor: dpr ?? 1,
|
|
359
|
+
mobile: false,
|
|
360
|
+
},
|
|
361
|
+
sessionId,
|
|
362
|
+
);
|
|
363
|
+
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case 'device': {
|
|
367
|
+
const deviceName = ctx.args[1] as string;
|
|
368
|
+
if (!deviceName)
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Usage: set device <name>. Available: ${browser.listDevices().join(', ')}`,
|
|
371
|
+
);
|
|
372
|
+
await browser.emulateDevice(client, sessionId, deviceName);
|
|
373
|
+
output.printSuccess(`Emulating device: ${deviceName}`);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case 'geo': {
|
|
377
|
+
const lat = parseFloat(ctx.args[1] as string);
|
|
378
|
+
const lon = parseFloat(ctx.args[2] as string);
|
|
379
|
+
const acc = parseFloat(ctx.args[3] as string) || 100;
|
|
380
|
+
if (Number.isNaN(lat) || Number.isNaN(lon))
|
|
381
|
+
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
382
|
+
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
383
|
+
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case 'offline': {
|
|
387
|
+
const offlineArg = ctx.args[1];
|
|
388
|
+
if (offlineArg === undefined) throw new Error('Usage: set offline <true|false>');
|
|
389
|
+
const enabled = offlineArg === 'true';
|
|
390
|
+
await browser.setOfflineMode(client, sessionId, enabled);
|
|
391
|
+
output.printSuccess(`Offline mode: ${enabled}`);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'media': {
|
|
395
|
+
const scheme = ctx.args[1] as 'dark' | 'light' | 'no-preference';
|
|
396
|
+
if (!scheme) throw new Error('Usage: set media dark|light|no-preference');
|
|
397
|
+
await browser.setColorScheme(client, sessionId, scheme);
|
|
398
|
+
output.printSuccess(`Color scheme: ${scheme}`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'credentials': {
|
|
402
|
+
const username = ctx.args[1] as string;
|
|
403
|
+
const password = ctx.args[2] as string;
|
|
404
|
+
if (!username || !password) throw new Error('Usage: set credentials <username> <password>');
|
|
405
|
+
await browser.setBasicAuth(client, sessionId, username, password);
|
|
406
|
+
output.printSuccess('Basic auth credentials set');
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case 'useragent': {
|
|
410
|
+
const ua = ctx.args[1] as string;
|
|
411
|
+
if (!ua) throw new Error('Usage: set useragent "<user-agent-string>"');
|
|
412
|
+
await browser.setUserAgent(client, sessionId, ua);
|
|
413
|
+
output.printSuccess('User agent set');
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
default:
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Unknown setting: ${setting}. Use: viewport|device|geo|offline|media|credentials|useragent`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { success: true };
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
export const pushstateCommand: Command = {
|
|
427
|
+
name: 'pushstate',
|
|
428
|
+
description: 'SPA navigation via pushState. Usage: monomind browse pushstate /path',
|
|
429
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
430
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
431
|
+
const browser = await getBrowser();
|
|
432
|
+
const url = ctx.args[0] as string;
|
|
433
|
+
if (!url) throw new Error('Usage: monomind browse pushstate <url>');
|
|
434
|
+
await browser.pushState(client, sessionId, url);
|
|
435
|
+
// SPA navigation changes what's on the page without a full page load —
|
|
436
|
+
// refs captured before this pushState call may now resolve to different content.
|
|
437
|
+
session.refs = new Map();
|
|
438
|
+
await browser.clearRefCache();
|
|
439
|
+
output.printSuccess(`pushState: ${url}`);
|
|
440
|
+
return { success: true };
|
|
441
|
+
},
|
|
442
|
+
};
|