@monoes/monobrowse 1.0.9 → 1.0.12
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 +1 -1
- package/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +40 -26
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +45 -15
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +16 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +16 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +10 -5
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +453 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +49 -26
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +61 -22
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +16 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +23 -10
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1093 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/adapters/gemini.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const geminiAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'gemini',
|
|
@@ -7,9 +7,13 @@ export const geminiAdapter: PlatformAdapter = {
|
|
|
7
7
|
reservedPaths: ['/app', '/chat'],
|
|
8
8
|
loginURL: () => 'https://accounts.google.com/signin/v2/identifier',
|
|
9
9
|
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
-
return page.evaluate<boolean>(
|
|
10
|
+
return page.evaluate<boolean>(
|
|
11
|
+
"!!(document.querySelector('bard-sidenav') || document.querySelector('.ql-editor'))",
|
|
12
|
+
);
|
|
11
13
|
},
|
|
12
14
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
-
return page.evaluate<string>(
|
|
15
|
+
return page.evaluate<string>(
|
|
16
|
+
"document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''",
|
|
17
|
+
);
|
|
14
18
|
},
|
|
15
19
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/adapters/google.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const googleAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'google',
|
|
@@ -10,11 +10,13 @@ export const googleAdapter: PlatformAdapter = {
|
|
|
10
10
|
const url = await page.url();
|
|
11
11
|
// Logged in if we're on a Google service page (not the sign-in page)
|
|
12
12
|
if (url.includes('accounts.google.com/signin')) return false;
|
|
13
|
-
return page.evaluate<boolean>(
|
|
13
|
+
return page.evaluate<boolean>(
|
|
14
|
+
"!!(document.querySelector('[aria-label*=\"Google Account\"]') || document.querySelector('.gb_A') || document.cookie.includes('SSID'))",
|
|
15
|
+
);
|
|
14
16
|
},
|
|
15
17
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
16
18
|
return page.evaluate<string>(
|
|
17
|
-
"document.querySelector('[data-email]')?.getAttribute('data-email') ?? document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''"
|
|
19
|
+
"document.querySelector('[data-email]')?.getAttribute('data-email') ?? document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''",
|
|
18
20
|
);
|
|
19
21
|
},
|
|
20
22
|
};
|
|
@@ -14,12 +14,12 @@ export interface PlatformAdapter {
|
|
|
14
14
|
extractUsername(page: PageInterface): Promise<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
import { linkedinAdapter } from './linkedin.js';
|
|
18
|
-
import { instagramAdapter } from './instagram.js';
|
|
19
|
-
import { xAdapter } from './x.js';
|
|
20
17
|
import { geminiAdapter } from './gemini.js';
|
|
21
18
|
import { googleAdapter } from './google.js';
|
|
19
|
+
import { instagramAdapter } from './instagram.js';
|
|
20
|
+
import { linkedinAdapter } from './linkedin.js';
|
|
22
21
|
import { microsoftAdapter } from './microsoft.js';
|
|
22
|
+
import { xAdapter } from './x.js';
|
|
23
23
|
|
|
24
24
|
export const adapters: Map<string, PlatformAdapter> = new Map([
|
|
25
25
|
['linkedin', linkedinAdapter],
|
|
@@ -32,6 +32,7 @@ export const adapters: Map<string, PlatformAdapter> = new Map([
|
|
|
32
32
|
|
|
33
33
|
export function getAdapter(platform: string): PlatformAdapter {
|
|
34
34
|
const adapter = adapters.get(platform);
|
|
35
|
-
if (!adapter)
|
|
35
|
+
if (!adapter)
|
|
36
|
+
throw new Error(`Unknown platform: ${platform}. Supported: ${[...adapters.keys()].join(', ')}`);
|
|
36
37
|
return adapter;
|
|
37
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/adapters/instagram.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const instagramAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'instagram',
|
|
@@ -11,6 +11,8 @@ export const instagramAdapter: PlatformAdapter = {
|
|
|
11
11
|
},
|
|
12
12
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
13
|
// Use getAttribute to get the raw path-relative href (not the resolved absolute URL)
|
|
14
|
-
return page.evaluate<string>(
|
|
14
|
+
return page.evaluate<string>(
|
|
15
|
+
"document.querySelector('a[href^=\"/\"]')?.getAttribute('href')?.split('/').filter(Boolean)[0] ?? ''",
|
|
16
|
+
);
|
|
15
17
|
},
|
|
16
18
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/adapters/linkedin.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const linkedinAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'linkedin',
|
|
@@ -10,6 +10,8 @@ export const linkedinAdapter: PlatformAdapter = {
|
|
|
10
10
|
return page.evaluate<boolean>("!!document.querySelector('.global-nav__me-photo')");
|
|
11
11
|
},
|
|
12
12
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
-
return page.evaluate<string>(
|
|
13
|
+
return page.evaluate<string>(
|
|
14
|
+
"document.querySelector('.profile-rail-card__actor-link')?.textContent?.trim() ?? ''",
|
|
15
|
+
);
|
|
14
16
|
},
|
|
15
17
|
};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
// src/browser/adapters/microsoft.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const microsoftAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'microsoft',
|
|
6
6
|
baseURL: 'https://login.microsoftonline.com',
|
|
7
7
|
reservedPaths: ['/oauth2', '/common/oauth2', '/kmsi'],
|
|
8
|
-
loginURL: () =>
|
|
8
|
+
loginURL: () =>
|
|
9
|
+
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=common&response_type=code&scope=openid+profile+email',
|
|
9
10
|
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
11
|
const url = await page.url();
|
|
11
|
-
if (url.includes('login.microsoftonline.com') || url.includes('login.microsoft.com'))
|
|
12
|
-
|
|
12
|
+
if (url.includes('login.microsoftonline.com') || url.includes('login.microsoft.com'))
|
|
13
|
+
return false;
|
|
14
|
+
return page.evaluate<boolean>(
|
|
15
|
+
"!!(document.querySelector('[data-tid=\"user-email\"]') || document.querySelector('.mectrl_currentAccount') || document.cookie.includes('ESTSAUTH'))",
|
|
16
|
+
);
|
|
13
17
|
},
|
|
14
18
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
15
19
|
return page.evaluate<string>(
|
|
16
|
-
"document.querySelector('[data-tid=\"user-email\"]')?.textContent?.trim() ?? document.querySelector('.mectrl_headerEmail')?.textContent?.trim() ?? ''"
|
|
20
|
+
"document.querySelector('[data-tid=\"user-email\"]')?.textContent?.trim() ?? document.querySelector('.mectrl_headerEmail')?.textContent?.trim() ?? ''",
|
|
17
21
|
);
|
|
18
22
|
},
|
|
19
23
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/adapters/x.ts
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageInterface, PlatformAdapter } from './index.js';
|
|
3
3
|
|
|
4
4
|
export const xAdapter: PlatformAdapter = {
|
|
5
5
|
platform: 'x',
|
|
@@ -7,9 +7,13 @@ export const xAdapter: PlatformAdapter = {
|
|
|
7
7
|
reservedPaths: ['/home', '/explore', '/notifications', '/messages'],
|
|
8
8
|
loginURL: () => 'https://x.com/i/flow/login',
|
|
9
9
|
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
-
return page.evaluate<boolean>(
|
|
10
|
+
return page.evaluate<boolean>(
|
|
11
|
+
'!!document.querySelector(\'[data-testid="SideNav_AccountSwitcher_Button"]\')',
|
|
12
|
+
);
|
|
11
13
|
},
|
|
12
14
|
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
-
return page.evaluate<string>(
|
|
15
|
+
return page.evaluate<string>(
|
|
16
|
+
"document.querySelector('[data-testid=\"SideNav_AccountSwitcher_Button\"] span')?.textContent?.trim() ?? ''",
|
|
17
|
+
);
|
|
14
18
|
},
|
|
15
19
|
};
|
package/src/browser/browser.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { CdpClient,
|
|
7
|
-
import type { BrowserConfig, CdpTarget } from './types.js';
|
|
8
|
-
import { CHROME_EXECUTABLES } from './types.js';
|
|
1
|
+
import { execSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { connect } from 'node:net';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { CdpClient, fetchNewTarget, fetchTargets } from './cdp.js';
|
|
9
7
|
import { enableConsoleCapture, setupConsoleCapture } from './console-log.js';
|
|
10
8
|
import { setupDialogAutoHandling } from './dialog.js';
|
|
11
|
-
import { loadActivePortInfo } from './ref-cache.js';
|
|
9
|
+
import { clearActivePort, loadActivePortInfo } from './ref-cache.js';
|
|
10
|
+
import type { BrowserConfig, CdpTarget } from './types.js';
|
|
11
|
+
import { CHROME_EXECUTABLES } from './types.js';
|
|
12
12
|
|
|
13
13
|
const DEFAULT_PORT = 9222;
|
|
14
14
|
const LAUNCH_TIMEOUT = 10_000;
|
|
@@ -22,6 +22,10 @@ const PROCESS_EXIT_POLL_MS = 50;
|
|
|
22
22
|
// A cross-process persisted PID older than this is not trusted for the
|
|
23
23
|
// SIGKILL fallback in closeBrowser() — see the call site for why.
|
|
24
24
|
const PERSISTED_PID_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
// A browser WE launched that has had no targets for longer than this is
|
|
26
|
+
// reaped at the next launch/attach — see reapIdleLaunchedBrowser().
|
|
27
|
+
const IDLE_REAP_AFTER_MS = 30 * 60 * 1000;
|
|
28
|
+
const REAP_CONNECT_TIMEOUT_MS = 3000;
|
|
25
29
|
|
|
26
30
|
// Tracks the PID of Chrome instances *we* spawned, keyed by CDP port, so
|
|
27
31
|
// closeBrowser() has a kill fallback when the graceful `Browser.close` CDP
|
|
@@ -53,14 +57,17 @@ function findChrome(executablePath?: string): string {
|
|
|
53
57
|
}
|
|
54
58
|
// Try PATH
|
|
55
59
|
try {
|
|
56
|
-
const result = execSync(
|
|
60
|
+
const result = execSync(
|
|
61
|
+
'which google-chrome chromium-browser chromium microsoft-edge microsoft-edge-stable 2>/dev/null',
|
|
62
|
+
{ encoding: 'utf8' },
|
|
63
|
+
).trim();
|
|
57
64
|
const first = result.split('\n')[0];
|
|
58
65
|
if (first) return first;
|
|
59
66
|
} catch {
|
|
60
67
|
// ignore
|
|
61
68
|
}
|
|
62
69
|
throw new Error(
|
|
63
|
-
'No supported browser found. Install Google Chrome, Microsoft Edge, or Chromium — or pass executablePath in BrowserConfig.'
|
|
70
|
+
'No supported browser found. Install Google Chrome, Microsoft Edge, or Chromium — or pass executablePath in BrowserConfig.',
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -81,7 +88,9 @@ export async function isPortOpen(port: number): Promise<boolean> {
|
|
|
81
88
|
*/
|
|
82
89
|
async function isChromeIdentity(port: number): Promise<boolean> {
|
|
83
90
|
try {
|
|
84
|
-
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
91
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
92
|
+
signal: AbortSignal.timeout(1000),
|
|
93
|
+
});
|
|
85
94
|
if (!res.ok) return false;
|
|
86
95
|
const info = (await res.json()) as { Browser?: string };
|
|
87
96
|
// Edge's Browser field is "Edg/<version>" with no "chrome"/"chromium"
|
|
@@ -124,6 +133,12 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
|
|
|
124
133
|
throw new Error(`Invalid port: ${rawPort}. Must be an integer between 1024 and 65535.`);
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
// Every launch/attach is this tool's only chance to notice that a previous
|
|
137
|
+
// session abandoned a browser on a CDP port and never gave it back — there
|
|
138
|
+
// is no daemon to do it on a timer. Bounded and non-throwing; a stale
|
|
139
|
+
// instance on the port we are about to use is freed before we probe it.
|
|
140
|
+
await reapIdleLaunchedBrowser();
|
|
141
|
+
|
|
127
142
|
// strictPort: fail fast on the exact requested port, matching the old
|
|
128
143
|
// behavior (Vite has the same escape hatch for the same reason) — for
|
|
129
144
|
// callers that treat the error as a signal ("this port is taken by
|
|
@@ -133,14 +148,15 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
|
|
|
133
148
|
if (await isChromeIdentity(rawPort)) return rawPort;
|
|
134
149
|
throw new Error(
|
|
135
150
|
`Port ${rawPort} is occupied by a process that does not identify as Chrome/Chromium. ` +
|
|
136
|
-
|
|
151
|
+
`Refusing to attach — pass a different port or free port ${rawPort}.`,
|
|
137
152
|
);
|
|
138
153
|
}
|
|
139
154
|
return launchOnFreePort(config, rawPort);
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
const candidates: number[] = [];
|
|
143
|
-
for (let i = 0; i < LAUNCH_PORT_SCAN_TRIES && rawPort + i <= 65535; i++)
|
|
158
|
+
for (let i = 0; i < LAUNCH_PORT_SCAN_TRIES && rawPort + i <= 65535; i++)
|
|
159
|
+
candidates.push(rawPort + i);
|
|
144
160
|
|
|
145
161
|
// Attach-if-already-Chrome only applies to the EXACT requested port — the
|
|
146
162
|
// original, deliberate, single-port risk ("don't silently take over an
|
|
@@ -170,7 +186,7 @@ export async function launchBrowser(config: BrowserConfig = {}): Promise<number>
|
|
|
170
186
|
}
|
|
171
187
|
throw new Error(
|
|
172
188
|
`Ports ${candidates[0]}-${candidates[candidates.length - 1]} are all occupied and port ${candidates[0]} ` +
|
|
173
|
-
|
|
189
|
+
`isn't a Chrome/Chromium instance to attach to. Pass a different --port.`,
|
|
174
190
|
);
|
|
175
191
|
}
|
|
176
192
|
|
|
@@ -223,7 +239,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
|
|
|
223
239
|
if (await isChromeIdentity(port)) return port;
|
|
224
240
|
throw new Error(
|
|
225
241
|
`Port ${port} is occupied by a CDP-speaking process that does not identify as Chrome/Chromium. ` +
|
|
226
|
-
|
|
242
|
+
`Refusing to attach — pass a different port or free port ${port}.`,
|
|
227
243
|
);
|
|
228
244
|
}
|
|
229
245
|
}
|
|
@@ -234,7 +250,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
|
|
|
234
250
|
if (await isTcpPortOpen(port)) {
|
|
235
251
|
throw new Error(
|
|
236
252
|
`Port ${port} is occupied by a non-Chrome process (TCP connection succeeds but no CDP response within ${launchTimeout}ms). ` +
|
|
237
|
-
|
|
253
|
+
`Free the port or pass a different one.`,
|
|
238
254
|
);
|
|
239
255
|
}
|
|
240
256
|
|
|
@@ -254,7 +270,10 @@ export async function enableSessionDomains(client: CdpClient, sessionId: string)
|
|
|
254
270
|
setupDialogAutoHandling(client, sessionId);
|
|
255
271
|
}
|
|
256
272
|
|
|
257
|
-
export async function connectToTarget(
|
|
273
|
+
export async function connectToTarget(
|
|
274
|
+
port: number,
|
|
275
|
+
targetId?: string,
|
|
276
|
+
): Promise<{ client: CdpClient; target: CdpTarget; sessionId: string }> {
|
|
258
277
|
const targets = await fetchTargets(port);
|
|
259
278
|
const pageTargets = targets.filter((t) => t.type === 'page');
|
|
260
279
|
|
|
@@ -306,7 +325,10 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
|
|
|
306
325
|
await Promise.race([
|
|
307
326
|
client.send('Browser.close', {}),
|
|
308
327
|
new Promise<never>((_, reject) => {
|
|
309
|
-
const t = setTimeout(
|
|
328
|
+
const t = setTimeout(
|
|
329
|
+
() => reject(new Error('Browser.close timed out')),
|
|
330
|
+
BROWSER_CLOSE_TIMEOUT_MS,
|
|
331
|
+
);
|
|
310
332
|
t.unref?.();
|
|
311
333
|
}),
|
|
312
334
|
]);
|
|
@@ -351,7 +373,9 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
|
|
|
351
373
|
try {
|
|
352
374
|
process.kill(pid, 0);
|
|
353
375
|
process.kill(pid, 'SIGKILL');
|
|
354
|
-
} catch {
|
|
376
|
+
} catch {
|
|
377
|
+
/* already exited, or never was — nothing to do */
|
|
378
|
+
}
|
|
355
379
|
return;
|
|
356
380
|
}
|
|
357
381
|
|
|
@@ -384,6 +408,90 @@ async function waitForProcessExit(pid: number): Promise<boolean> {
|
|
|
384
408
|
}
|
|
385
409
|
}
|
|
386
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Give back the CDP port of a browser we launched that nothing is using any
|
|
413
|
+
* more. A launched Chrome is deliberately detached and unref'd so it outlives
|
|
414
|
+
* the command that spawned it and later commands can reattach — but nothing
|
|
415
|
+
* reaped it afterwards, so an abandoned session kept listening on its port
|
|
416
|
+
* (9222 by convention, which other local tools want) indefinitely. A foreign
|
|
417
|
+
* CDP server *answers* there rather than refusing the connection, so those
|
|
418
|
+
* tools retry the wrong port and report misleading errors instead of moving on.
|
|
419
|
+
*
|
|
420
|
+
* Nothing long-lived exists to run this on a timer — every CLI command is its
|
|
421
|
+
* own short-lived process — so it runs at the next launch/attach instead
|
|
422
|
+
* (see launchBrowser). A browser is reaped only when ALL of these hold:
|
|
423
|
+
* - the persisted session says WE launched it — `connect` records
|
|
424
|
+
* launched:false for a browser that belongs to the user, never touched;
|
|
425
|
+
* - that session is older than IDLE_REAP_AFTER_MS;
|
|
426
|
+
* - the CDP endpoint still answers and reports ZERO targets. An open tab,
|
|
427
|
+
* or a client attached to one (Chrome drops webSocketDebuggerUrl from an
|
|
428
|
+
* attached target but still lists it), means the session is live and
|
|
429
|
+
* reattach-across-commands must keep working.
|
|
430
|
+
*
|
|
431
|
+
* Shutdown goes through closeBrowser() on the browser-level websocket — the
|
|
432
|
+
* only connection left once every target is gone — so a recycled PID can
|
|
433
|
+
* never be signaled by mistake: graceful `Browser.close` reaches whatever is
|
|
434
|
+
* actually listening on that port, at any age, and the persisted PID is only
|
|
435
|
+
* ever used for closeBrowser's own age-bounded force-kill.
|
|
436
|
+
*
|
|
437
|
+
* Never throws: a failed reap must not stop a launch. Returns the reaped
|
|
438
|
+
* port, or null when there was nothing to reap.
|
|
439
|
+
*/
|
|
440
|
+
export async function reapIdleLaunchedBrowser(): Promise<number | null> {
|
|
441
|
+
try {
|
|
442
|
+
const session = await loadActivePortInfo();
|
|
443
|
+
if (!session?.launched || session.savedAt === undefined) return null;
|
|
444
|
+
if (Date.now() - session.savedAt < IDLE_REAP_AFTER_MS) return null;
|
|
445
|
+
|
|
446
|
+
const wsUrl = await fetchBrowserWebSocketUrl(session.port);
|
|
447
|
+
if (!wsUrl) return null;
|
|
448
|
+
if ((await fetchTargets(session.port)).length > 0) return null;
|
|
449
|
+
|
|
450
|
+
const client = new CdpClient();
|
|
451
|
+
try {
|
|
452
|
+
// CdpClient.connect() has no timeout of its own — a socket that never
|
|
453
|
+
// opens (and never errors) would hang the launch this reap is running
|
|
454
|
+
// inside of, which is strictly worse than leaving the port squatted.
|
|
455
|
+
await Promise.race([
|
|
456
|
+
client.connect(wsUrl),
|
|
457
|
+
new Promise<never>((_, reject) => {
|
|
458
|
+
const t = setTimeout(
|
|
459
|
+
() => reject(new Error('Reap: CDP connect timed out')),
|
|
460
|
+
REAP_CONNECT_TIMEOUT_MS,
|
|
461
|
+
);
|
|
462
|
+
t.unref?.();
|
|
463
|
+
}),
|
|
464
|
+
]);
|
|
465
|
+
await closeBrowser(client, session.port);
|
|
466
|
+
} finally {
|
|
467
|
+
try {
|
|
468
|
+
client.close();
|
|
469
|
+
} catch {
|
|
470
|
+
/* already gone */
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
launchedPids.delete(session.port);
|
|
474
|
+
launchedUserDataDirs.delete(session.port);
|
|
475
|
+
await clearActivePort();
|
|
476
|
+
return session.port;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Chrome's browser-level debugger endpoint, from /json/version — the
|
|
483
|
+
* connection `Browser.close` belongs on. Null if the port isn't answering as
|
|
484
|
+
* a Chrome/Chromium CDP endpoint (same identity check as isChromeIdentity). */
|
|
485
|
+
async function fetchBrowserWebSocketUrl(port: number): Promise<string | null> {
|
|
486
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
487
|
+
signal: AbortSignal.timeout(1000),
|
|
488
|
+
});
|
|
489
|
+
if (!res.ok) return null;
|
|
490
|
+
const info = (await res.json()) as { Browser?: string; webSocketDebuggerUrl?: string };
|
|
491
|
+
if (typeof info.Browser !== 'string' || !/chrom(e|ium)|edg/i.test(info.Browser)) return null;
|
|
492
|
+
return typeof info.webSocketDebuggerUrl === 'string' ? info.webSocketDebuggerUrl : null;
|
|
493
|
+
}
|
|
494
|
+
|
|
387
495
|
export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
|
388
496
|
// Cap to 2 MB to prevent OOM in CDP message serializer (e.g. data: URI attacks)
|
|
389
497
|
if (url.length > 2_097_152) throw new Error('URL exceeds 2 MB limit');
|
|
@@ -400,14 +508,24 @@ export async function waitForLoad(
|
|
|
400
508
|
client: CdpClient,
|
|
401
509
|
sessionId: string,
|
|
402
510
|
condition: 'load' | 'networkidle' | 'domcontentloaded' = 'load',
|
|
403
|
-
timeout = 30_000
|
|
511
|
+
timeout = 30_000,
|
|
404
512
|
): Promise<void> {
|
|
405
513
|
if (condition === 'load' || condition === 'domcontentloaded') {
|
|
406
514
|
// Guard against race where the page loads before the listener is registered
|
|
407
|
-
const readyExpr =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
515
|
+
const readyExpr =
|
|
516
|
+
condition === 'load'
|
|
517
|
+
? 'document.readyState === "complete"'
|
|
518
|
+
: 'document.readyState !== "loading"';
|
|
519
|
+
const readyCheck = await client
|
|
520
|
+
.send<{ result: { value?: boolean } }>(
|
|
521
|
+
'Runtime.evaluate',
|
|
522
|
+
{
|
|
523
|
+
expression: readyExpr,
|
|
524
|
+
returnByValue: true,
|
|
525
|
+
},
|
|
526
|
+
sessionId,
|
|
527
|
+
)
|
|
528
|
+
.catch(() => ({ result: { value: false } }));
|
|
411
529
|
if (readyCheck.result?.value) return;
|
|
412
530
|
|
|
413
531
|
const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
|
|
@@ -415,7 +533,10 @@ export async function waitForLoad(
|
|
|
415
533
|
let timedOut = false;
|
|
416
534
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
417
535
|
const timeoutPromise = new Promise<void>((resolve) => {
|
|
418
|
-
timeoutHandle = setTimeout(() => {
|
|
536
|
+
timeoutHandle = setTimeout(() => {
|
|
537
|
+
timedOut = true;
|
|
538
|
+
resolve();
|
|
539
|
+
}, timeout);
|
|
419
540
|
});
|
|
420
541
|
try {
|
|
421
542
|
await Promise.race([eventPromise, timeoutPromise]);
|
|
@@ -435,7 +556,7 @@ async function waitForNetworkIdle(
|
|
|
435
556
|
client: CdpClient,
|
|
436
557
|
sessionId: string,
|
|
437
558
|
idleMs: number,
|
|
438
|
-
timeout: number
|
|
559
|
+
timeout: number,
|
|
439
560
|
): Promise<void> {
|
|
440
561
|
return new Promise((resolve, reject) => {
|
|
441
562
|
let pending = 0;
|
|
@@ -447,9 +568,16 @@ async function waitForNetworkIdle(
|
|
|
447
568
|
}, timeout);
|
|
448
569
|
|
|
449
570
|
const cleanup = () => {
|
|
450
|
-
if (idleTimer) {
|
|
571
|
+
if (idleTimer) {
|
|
572
|
+
clearTimeout(idleTimer);
|
|
573
|
+
idleTimer = null;
|
|
574
|
+
}
|
|
451
575
|
clearTimeout(killTimer);
|
|
452
|
-
offReq();
|
|
576
|
+
offReq();
|
|
577
|
+
offResp();
|
|
578
|
+
offFail();
|
|
579
|
+
offCache();
|
|
580
|
+
offResp2();
|
|
453
581
|
};
|
|
454
582
|
|
|
455
583
|
const settle = () => {
|
|
@@ -462,24 +590,34 @@ async function waitForNetworkIdle(
|
|
|
462
590
|
if (idleTimer) clearTimeout(idleTimer);
|
|
463
591
|
idleTimer = setTimeout(settle, idleMs);
|
|
464
592
|
} else {
|
|
465
|
-
if (idleTimer) {
|
|
593
|
+
if (idleTimer) {
|
|
594
|
+
clearTimeout(idleTimer);
|
|
595
|
+
idleTimer = null;
|
|
596
|
+
}
|
|
466
597
|
}
|
|
467
598
|
};
|
|
468
599
|
|
|
469
600
|
const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
|
|
470
601
|
if (sid !== sessionId) return;
|
|
471
602
|
const id = params.requestId as string;
|
|
472
|
-
if (!inflight.has(id)) {
|
|
603
|
+
if (!inflight.has(id)) {
|
|
604
|
+
inflight.add(id);
|
|
605
|
+
pending++;
|
|
606
|
+
check();
|
|
607
|
+
}
|
|
473
608
|
});
|
|
474
609
|
|
|
475
610
|
const decrement = (params: Record<string, unknown>, sid?: string) => {
|
|
476
611
|
if (sid !== sessionId) return;
|
|
477
612
|
const id = params.requestId as string;
|
|
478
|
-
if (inflight.delete(id)) {
|
|
613
|
+
if (inflight.delete(id)) {
|
|
614
|
+
pending = Math.max(0, pending - 1);
|
|
615
|
+
check();
|
|
616
|
+
}
|
|
479
617
|
};
|
|
480
618
|
|
|
481
|
-
const offResp
|
|
482
|
-
const offFail
|
|
619
|
+
const offResp = client.on('Network.loadingFinished', decrement);
|
|
620
|
+
const offFail = client.on('Network.loadingFailed', decrement);
|
|
483
621
|
const offCache = client.on('Network.requestServedFromCache', decrement);
|
|
484
622
|
// Guard against requests that never fire loadingFinished/loadingFailed (e.g. data: URLs)
|
|
485
623
|
// Skip 3xx redirect responses — the request continues under the same requestId
|
|
@@ -494,18 +632,26 @@ async function waitForNetworkIdle(
|
|
|
494
632
|
}
|
|
495
633
|
|
|
496
634
|
export async function getCurrentUrl(client: CdpClient, sessionId: string): Promise<string> {
|
|
497
|
-
const result = await client.send<{ result: { value: string } }>(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
635
|
+
const result = await client.send<{ result: { value: string } }>(
|
|
636
|
+
'Runtime.evaluate',
|
|
637
|
+
{
|
|
638
|
+
expression: 'location.href',
|
|
639
|
+
returnByValue: true,
|
|
640
|
+
},
|
|
641
|
+
sessionId,
|
|
642
|
+
);
|
|
501
643
|
return result.result?.value ?? '';
|
|
502
644
|
}
|
|
503
645
|
|
|
504
646
|
export async function getCurrentTitle(client: CdpClient, sessionId: string): Promise<string> {
|
|
505
|
-
const result = await client.send<{ result: { value: string } }>(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
647
|
+
const result = await client.send<{ result: { value: string } }>(
|
|
648
|
+
'Runtime.evaluate',
|
|
649
|
+
{
|
|
650
|
+
expression: 'document.title',
|
|
651
|
+
returnByValue: true,
|
|
652
|
+
},
|
|
653
|
+
sessionId,
|
|
654
|
+
);
|
|
509
655
|
return result.result?.value ?? '';
|
|
510
656
|
}
|
|
511
657
|
|
package/src/browser/cdp.ts
CHANGED
|
@@ -13,8 +13,18 @@ export const DEFAULT_CDP_SEND_TIMEOUT_MS = 30_000;
|
|
|
13
13
|
|
|
14
14
|
export class CdpClient {
|
|
15
15
|
private ws: WebSocket | null = null;
|
|
16
|
-
private pendingCommands = new Map<
|
|
17
|
-
|
|
16
|
+
private pendingCommands = new Map<
|
|
17
|
+
number,
|
|
18
|
+
{
|
|
19
|
+
resolve: (r: CdpResponse) => void;
|
|
20
|
+
reject: (e: Error) => void;
|
|
21
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
22
|
+
}
|
|
23
|
+
>();
|
|
24
|
+
private eventListeners = new Map<
|
|
25
|
+
string,
|
|
26
|
+
Set<(params: Record<string, unknown>, sessionId?: string) => void>
|
|
27
|
+
>();
|
|
18
28
|
private nextId = 1;
|
|
19
29
|
private connected = false;
|
|
20
30
|
|
|
@@ -73,7 +83,11 @@ export class CdpClient {
|
|
|
73
83
|
const listeners = this.eventListeners.get(msg.method);
|
|
74
84
|
if (listeners) {
|
|
75
85
|
for (const fn of listeners) {
|
|
76
|
-
try {
|
|
86
|
+
try {
|
|
87
|
+
fn(msg.params ?? {}, msg.sessionId);
|
|
88
|
+
} catch {
|
|
89
|
+
/* isolate per-listener errors */
|
|
90
|
+
}
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
93
|
}
|
|
@@ -88,7 +102,7 @@ export class CdpClient {
|
|
|
88
102
|
method: string,
|
|
89
103
|
params?: Record<string, unknown>,
|
|
90
104
|
sessionId?: string,
|
|
91
|
-
timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS
|
|
105
|
+
timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS,
|
|
92
106
|
): Promise<T> {
|
|
93
107
|
return new Promise((resolve, reject) => {
|
|
94
108
|
if (!this.ws || !this.connected) {
|
|
@@ -114,8 +128,14 @@ export class CdpClient {
|
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
this.pendingCommands.set(id, {
|
|
117
|
-
resolve: (r) => {
|
|
118
|
-
|
|
131
|
+
resolve: (r) => {
|
|
132
|
+
if (timer) clearTimeout(timer);
|
|
133
|
+
resolve((r.result ?? {}) as T);
|
|
134
|
+
},
|
|
135
|
+
reject: (e) => {
|
|
136
|
+
if (timer) clearTimeout(timer);
|
|
137
|
+
reject(e);
|
|
138
|
+
},
|
|
119
139
|
timer,
|
|
120
140
|
});
|
|
121
141
|
this.ws.send(JSON.stringify(cmd), (err) => {
|
|
@@ -188,7 +208,9 @@ export async function fetchTargets(port: number): Promise<CdpTarget[]> {
|
|
|
188
208
|
|
|
189
209
|
export async function fetchNewTarget(port: number, url: string): Promise<CdpTarget> {
|
|
190
210
|
// Chrome v92+ requires PUT for /json/new; GET returns 405. URL must be encoded.
|
|
191
|
-
const res = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`, {
|
|
211
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`, {
|
|
212
|
+
method: 'PUT',
|
|
213
|
+
});
|
|
192
214
|
if (!res.ok) throw new Error(`Failed to create target: ${res.statusText}`);
|
|
193
215
|
return readCdpJson(res) as Promise<CdpTarget>;
|
|
194
216
|
}
|
|
@@ -203,7 +225,10 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
|
|
|
203
225
|
const target = await fetchNewTarget(port, url);
|
|
204
226
|
const wsDebuggerUrl = target.webSocketDebuggerUrl;
|
|
205
227
|
if (!wsDebuggerUrl) {
|
|
206
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
'Chrome did not return a WebSocket debugger URL. Ensure Chrome is running with --remote-debugging-port=' +
|
|
230
|
+
port,
|
|
231
|
+
);
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
const client = new CdpClient();
|
|
@@ -218,7 +243,9 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
|
|
|
218
243
|
await Promise.race([
|
|
219
244
|
loadPromise,
|
|
220
245
|
new Promise<void>((_, reject) =>
|
|
221
|
-
AbortSignal.timeout(30_000).addEventListener('abort', () =>
|
|
246
|
+
AbortSignal.timeout(30_000).addEventListener('abort', () =>
|
|
247
|
+
reject(new Error('Page load timeout after 30s')),
|
|
248
|
+
),
|
|
222
249
|
),
|
|
223
250
|
]);
|
|
224
251
|
} catch (err) {
|
|
@@ -228,7 +255,9 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
|
|
|
228
255
|
|
|
229
256
|
const closeTab = async (): Promise<void> => {
|
|
230
257
|
try {
|
|
231
|
-
await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(target.id)}`, {
|
|
258
|
+
await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(target.id)}`, {
|
|
259
|
+
method: 'GET',
|
|
260
|
+
});
|
|
232
261
|
} catch {
|
|
233
262
|
// best-effort: tab may already be gone
|
|
234
263
|
}
|
|
@@ -243,7 +272,10 @@ export async function createBrowserPage(url: string, port = 9222): Promise<Brows
|
|
|
243
272
|
return result.result.value;
|
|
244
273
|
},
|
|
245
274
|
evaluate: async <T>(expression: string): Promise<T> => {
|
|
246
|
-
const result = await client.send<{
|
|
275
|
+
const result = await client.send<{
|
|
276
|
+
result: { value: T };
|
|
277
|
+
exceptionDetails?: { text: string };
|
|
278
|
+
}>('Runtime.evaluate', {
|
|
247
279
|
expression,
|
|
248
280
|
returnByValue: true,
|
|
249
281
|
});
|