@testmuai/playwright-bindings 0.1.18 → 0.1.19
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/helpers/assertion.d.ts.map +1 -1
- package/dist/helpers/assertion.js +6 -24
- package/dist/helpers/assertion.js.map +1 -1
- package/dist/helpers/devtools.d.ts +13 -1
- package/dist/helpers/devtools.d.ts.map +1 -1
- package/dist/helpers/devtools.js +352 -12
- package/dist/helpers/devtools.js.map +1 -1
- package/dist/helpers/evaluation/colors.d.ts +2 -0
- package/dist/helpers/evaluation/colors.d.ts.map +1 -0
- package/dist/helpers/evaluation/colors.js +151 -0
- package/dist/helpers/evaluation/colors.js.map +1 -0
- package/dist/helpers/evaluation/evaluate.d.ts +8 -0
- package/dist/helpers/evaluation/evaluate.d.ts.map +1 -0
- package/dist/helpers/evaluation/evaluate.js +217 -0
- package/dist/helpers/evaluation/evaluate.js.map +1 -0
- package/dist/helpers/sseParser.d.ts +34 -0
- package/dist/helpers/sseParser.d.ts.map +1 -0
- package/dist/helpers/sseParser.js +128 -0
- package/dist/helpers/sseParser.js.map +1 -0
- package/dist/session.d.ts +8 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +51 -19
- package/dist/session.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../../src/helpers/assertion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../../src/helpers/assertion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AASvC,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,kBAAkB,EAAE,KAAK,GAAG,IAAI,CAAC;IACjC,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;CACzB;AAsBD,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,eAAe,CAAC,CA+C1B;AAWD,wBAAsB,cAAc,CAClC,SAAS,EAAE,QAAQ,EAAE,EACrB,iBAAiB,GAAE,KAAK,GAAG,IAAY,GACtC,OAAO,CAAC,OAAO,CAAC,CAalB"}
|
|
@@ -4,6 +4,7 @@ import { config } from '../config.js';
|
|
|
4
4
|
import { log, preview } from '../log.js';
|
|
5
5
|
import { AssertionFailureError } from '../errors.js';
|
|
6
6
|
import { takeScreenshot } from './screenshot.js';
|
|
7
|
+
import { evaluateSubChecks } from './evaluation/evaluate.js';
|
|
7
8
|
/** Resolve a sub-check's templated fields and cast the string-typed ones
|
|
8
9
|
* (description, expected_value, extracted_value) to strings. The variable
|
|
9
10
|
* store may hold non-string values (numbers, bools) and the server's
|
|
@@ -21,18 +22,6 @@ async function resolveSub(sub) {
|
|
|
21
22
|
transforms: sub.transforms ?? ['strip'],
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
async function callEvaluateApi(claim, compositeOp, subChecks) {
|
|
25
|
-
const resp = await fetchWithAuth(`${config.current.aiApiHost}/api/v1/evaluate`, {
|
|
26
|
-
method: 'POST',
|
|
27
|
-
headers: { 'content-type': 'application/json' },
|
|
28
|
-
body: JSON.stringify({ claim, composite_operator: compositeOp, sub_checks: subChecks }),
|
|
29
|
-
});
|
|
30
|
-
if (!resp.ok) {
|
|
31
|
-
const text = await resp.text();
|
|
32
|
-
throw new Error(`Evaluate API error ${resp.status}: ${text}`);
|
|
33
|
-
}
|
|
34
|
-
return (await resp.json());
|
|
35
|
-
}
|
|
36
25
|
export async function verifyAssertion(page, claim, tree) {
|
|
37
26
|
const subChecks = tree.sub_checks ?? [];
|
|
38
27
|
const compositeOp = tree.composite_operator ?? 'and';
|
|
@@ -43,7 +32,7 @@ export async function verifyAssertion(page, claim, tree) {
|
|
|
43
32
|
if (hasStoreKeys || !config.current.smart) {
|
|
44
33
|
log.helper(`[assertion] deterministic claim=${preview(assertionClaim, 80)}`);
|
|
45
34
|
const resolved = await Promise.all(subChecks.map(resolveSub));
|
|
46
|
-
const result =
|
|
35
|
+
const result = evaluateSubChecks(assertionClaim, compositeOp, resolved);
|
|
47
36
|
log.helper(`[assertion] result status=${result.status ?? 'unknown'}`);
|
|
48
37
|
return _gateAssertionFailure(result, assertionClaim);
|
|
49
38
|
}
|
|
@@ -93,16 +82,9 @@ export async function evaluateBranch(subChecks, compositeOperator = 'and') {
|
|
|
93
82
|
log.test('[if/else] result=False (no valid sub-checks) → taking false branch');
|
|
94
83
|
return false;
|
|
95
84
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
log.test(`[if/else] result=${passed ? 'true' : 'false'} → taking ${passed ? 'true' : 'false'} branch`);
|
|
101
|
-
return passed;
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
log.test('[if/else] result=false → taking false branch');
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
85
|
+
const result = evaluateSubChecks(`Branch condition (${compositeOperator})`, compositeOperator, resolved);
|
|
86
|
+
const passed = result.status === 'passed';
|
|
87
|
+
log.test(`[if/else] result=${passed ? 'true' : 'false'} → taking ${passed ? 'true' : 'false'} branch`);
|
|
88
|
+
return passed;
|
|
107
89
|
}
|
|
108
90
|
//# sourceMappingURL=assertion.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.js","sourceRoot":"","sources":["../../src/helpers/assertion.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"assertion.js","sourceRoot":"","sources":["../../src/helpers/assertion.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,10 +9,22 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Cookies + Storage are snapshot-based (on-demand at query time).
|
|
11
11
|
*/
|
|
12
|
-
import type { Page, BrowserContext } from 'playwright';
|
|
12
|
+
import type { Page, BrowserContext, CDPSession } from 'playwright';
|
|
13
13
|
export declare function startCapture(page: Page, context: BrowserContext): Promise<void>;
|
|
14
14
|
export declare function stopCapture(page: Page): void;
|
|
15
15
|
export declare function resetCapture(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Create a CDP session on `page`, wire SSE handlers, register it in the
|
|
18
|
+
* page->session registry, and return it. Handlers close over per-session request
|
|
19
|
+
* maps (requestId collides across CDP targets).
|
|
20
|
+
*/
|
|
21
|
+
export declare function startSSECapture(page: Page): Promise<CDPSession>;
|
|
22
|
+
/**
|
|
23
|
+
* Async SSE teardown: stop handlers, drain pending tasks, detach every CDP
|
|
24
|
+
* session. The context 'page' listener is removed by the sync stopCapture
|
|
25
|
+
* BEFORE this runs, so no new popup-attach task can be scheduled mid-teardown.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stopSSECapture(): Promise<void>;
|
|
16
28
|
export declare function devtoolsNetworkQuery(codeJs: string): Promise<string>;
|
|
17
29
|
export declare function devtoolsConsoleQuery(codeJs: string): Promise<string>;
|
|
18
30
|
export declare function devtoolsCookiesQuery(codeJs: string): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/helpers/devtools.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAgD,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/helpers/devtools.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAgD,UAAU,EAAE,MAAM,YAAY,CAAC;AAyJjH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCrF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAa5C;AAmDD,wBAAgB,YAAY,IAAI,IAAI,CAiCnC;AAybD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CA8FrE;AAkBD;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAQpD;AA0KD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1E;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU1E;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB1E;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB1E;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9E;AAWD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAGpD;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpD;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5E"}
|
package/dist/helpers/devtools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClipboardStore, installClipboard, pasteViaPage, runClipboardQuery } from './clipboard.js';
|
|
2
2
|
import * as configure from '../configure.js';
|
|
3
3
|
import { log } from '../log.js';
|
|
4
|
+
import { SSEWireParser } from './sseParser.js';
|
|
4
5
|
// ── State ──
|
|
5
6
|
let networkEntries = [];
|
|
6
7
|
let networkSequence = 0;
|
|
@@ -14,6 +15,16 @@ let wsActive = false;
|
|
|
14
15
|
let wsConfigured = false; // true once WS capture was started this run
|
|
15
16
|
let wsSeq = 0; // global monotonic across all connections
|
|
16
17
|
let wsConnCounter = 0; // connection-id allocator
|
|
18
|
+
// SSE capture (CDP-based, Chromium-only). CDP sessions are NOT auto-managed
|
|
19
|
+
// like page.on — keep a page->CDPSession registry and detach each on stop.
|
|
20
|
+
let sseConnections = [];
|
|
21
|
+
let sseActive = false;
|
|
22
|
+
let sseConfigured = false; // true once SSE capture was started this run
|
|
23
|
+
let sseSeq = 0; // global monotonic across all connections
|
|
24
|
+
let sseConnCounter = 0; // connection-id allocator
|
|
25
|
+
const sseMsgCounter = new Map(); // connectionId -> per-conn messageIndex
|
|
26
|
+
const sseSessions = new Map(); // the page->session registry
|
|
27
|
+
const sseTasks = new Set(); // pending scheduled tasks (response/popup)
|
|
17
28
|
let pageRef = null;
|
|
18
29
|
let contextRef = null;
|
|
19
30
|
let pageListenerActive = false; // true once context.on('page') is wired this run
|
|
@@ -24,6 +35,9 @@ const MAX_CONSOLE = 50000;
|
|
|
24
35
|
const MAX_WS_CONN = 256;
|
|
25
36
|
const MAX_WS_FRAMES = 2000;
|
|
26
37
|
const WS_PRESERVE_FIRST_N = 20;
|
|
38
|
+
const MAX_SSE_CONN = 256;
|
|
39
|
+
const MAX_SSE_MSGS = 2000;
|
|
40
|
+
const SSE_PRESERVE_FIRST_N = 20;
|
|
27
41
|
// Raw-byte budget for a binary frame so its base64 stays <= MAX_BODY and decodes cleanly
|
|
28
42
|
// (multiple of 3 → no mid-stream base64 padding). Math.floor(65536/4)*3 = 49152. Matches Python.
|
|
29
43
|
const MAX_WS_RAW_BUDGET = Math.floor(MAX_BODY / 4) * 3;
|
|
@@ -49,12 +63,17 @@ export async function startCapture(page, context) {
|
|
|
49
63
|
startConsoleCapture(page);
|
|
50
64
|
if (devtools.websocket)
|
|
51
65
|
startWebSocketCapture(page);
|
|
52
|
-
|
|
66
|
+
if (devtools.sse) {
|
|
67
|
+
// SSE rides a CDP session (Chromium-only); attach is async. The
|
|
68
|
+
// non-Chromium hard-fail lives in session.ts run() BEFORE this runs.
|
|
69
|
+
await startSSECaptureInitial(page);
|
|
70
|
+
}
|
|
71
|
+
// Page-based captures (network/console/websocket/sse) only listen on the page
|
|
53
72
|
// they were attached to. Re-arm them on tabs opened later (window.open,
|
|
54
73
|
// target=_blank) via the context's 'page' event — otherwise those tabs are
|
|
55
74
|
// silently uncaptured. (performance/clipboard ride context init scripts and
|
|
56
75
|
// already cover every page.)
|
|
57
|
-
if (devtools.network || devtools.console || devtools.websocket) {
|
|
76
|
+
if (devtools.network || devtools.console || devtools.websocket || devtools.sse) {
|
|
58
77
|
attachContextPageListener(context);
|
|
59
78
|
}
|
|
60
79
|
if (devtools.performance)
|
|
@@ -71,6 +90,11 @@ export function stopCapture(page) {
|
|
|
71
90
|
stopConsoleCapture(page);
|
|
72
91
|
if (wsActive)
|
|
73
92
|
stopWebSocketCapture(page);
|
|
93
|
+
// SSE CDP sessions are detached asynchronously by stopSSECapture(); this sync
|
|
94
|
+
// stop only flips sseActive off (so handlers short-circuit). The context
|
|
95
|
+
// 'page' listener is removed above, so no new popup-attach task is scheduled
|
|
96
|
+
// before stopSSECapture() drains and detaches.
|
|
97
|
+
sseActive = false;
|
|
74
98
|
// The per-type stops above only detach the initial page; also detach the
|
|
75
99
|
// tabs opened mid-run so no listener outlives the capture session.
|
|
76
100
|
detachExtraPages();
|
|
@@ -122,6 +146,14 @@ function onNewPage(page) {
|
|
|
122
146
|
}
|
|
123
147
|
catch { /* ignore */ }
|
|
124
148
|
}
|
|
149
|
+
// SSE attach is async (own CDP session) — schedule it as a tracked task.
|
|
150
|
+
// The session is registered in sseSessions and detached by stopSSECapture.
|
|
151
|
+
if (sseActive) {
|
|
152
|
+
try {
|
|
153
|
+
schedule(attachSseToPage(page));
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
}
|
|
125
157
|
if (attached && !extraPages.includes(page))
|
|
126
158
|
extraPages.push(page);
|
|
127
159
|
}
|
|
@@ -144,6 +176,17 @@ export function resetCapture() {
|
|
|
144
176
|
wsConnCounter = 0;
|
|
145
177
|
wsById.clear();
|
|
146
178
|
wsFrameCounter.clear();
|
|
179
|
+
// SSE: best-effort sync reset for test isolation. Live CDP sessions should be
|
|
180
|
+
// detached via stopSSECapture() in the async teardown; here we drop refs so a
|
|
181
|
+
// fresh run starts clean.
|
|
182
|
+
sseConnections = [];
|
|
183
|
+
sseActive = false;
|
|
184
|
+
sseConfigured = false;
|
|
185
|
+
sseSeq = 0;
|
|
186
|
+
sseConnCounter = 0;
|
|
187
|
+
sseMsgCounter.clear();
|
|
188
|
+
sseSessions.clear();
|
|
189
|
+
sseTasks.clear();
|
|
147
190
|
pendingRequests.clear();
|
|
148
191
|
pageRef = null;
|
|
149
192
|
contextRef = null;
|
|
@@ -232,7 +275,14 @@ async function onResponse(response) {
|
|
|
232
275
|
entry.responseStatus = response.status();
|
|
233
276
|
entry.responseHeaders = await response.allHeaders();
|
|
234
277
|
const ct = entry.responseHeaders['content-type'] ?? '';
|
|
235
|
-
|
|
278
|
+
// SSE responses (text/event-stream) are long-lived streams whose body
|
|
279
|
+
// never resolves until the stream closes — reading it here would hang the
|
|
280
|
+
// capture. SSE is captured separately via CDP, so skip the body read.
|
|
281
|
+
// Match before the generic 'text' substring (which catches event-stream).
|
|
282
|
+
if (ct.includes('text/event-stream')) {
|
|
283
|
+
entry.responseBody = null;
|
|
284
|
+
}
|
|
285
|
+
else if (TEXT_TYPES.some(t => ct.includes(t))) {
|
|
236
286
|
try {
|
|
237
287
|
let body = await response.text();
|
|
238
288
|
if (body.length > MAX_BODY)
|
|
@@ -489,6 +539,254 @@ function reindexWsById(removed) {
|
|
|
489
539
|
wsById.set(k, v - 1);
|
|
490
540
|
}
|
|
491
541
|
}
|
|
542
|
+
// ── SSE capture (CDP, Chromium-only) ──
|
|
543
|
+
function schedule(p) {
|
|
544
|
+
// Track a scheduled task so teardown can drain it; remove on completion.
|
|
545
|
+
sseTasks.add(p);
|
|
546
|
+
void p.finally(() => sseTasks.delete(p));
|
|
547
|
+
}
|
|
548
|
+
function normSseEvent(name) {
|
|
549
|
+
// CDP eventName "" or "message" -> null (default event type).
|
|
550
|
+
return !name || name === 'message' ? null : name;
|
|
551
|
+
}
|
|
552
|
+
function isEventStream(resp) {
|
|
553
|
+
const mime = (resp.mimeType ?? '').split(';')[0].trim().toLowerCase();
|
|
554
|
+
if (mime === 'text/event-stream')
|
|
555
|
+
return true;
|
|
556
|
+
const headers = resp.headers ?? {};
|
|
557
|
+
const ct = headers['content-type'] ?? headers['Content-Type'] ?? '';
|
|
558
|
+
return ct.toLowerCase().includes('text/event-stream');
|
|
559
|
+
}
|
|
560
|
+
function allocSseConn(url, transport) {
|
|
561
|
+
if (sseConnections.length >= MAX_SSE_CONN)
|
|
562
|
+
evictOldestClosedSse();
|
|
563
|
+
let domain = '';
|
|
564
|
+
let path = '/';
|
|
565
|
+
try {
|
|
566
|
+
const u = new URL(url || '');
|
|
567
|
+
domain = u.hostname;
|
|
568
|
+
path = u.pathname || '/';
|
|
569
|
+
}
|
|
570
|
+
catch { /* malformed */ }
|
|
571
|
+
const conn = {
|
|
572
|
+
connectionId: `sse_${sseConnCounter}`,
|
|
573
|
+
url: url || '', domain, path,
|
|
574
|
+
openedTsMs: Date.now(),
|
|
575
|
+
closedTsMs: null,
|
|
576
|
+
error: null,
|
|
577
|
+
transport,
|
|
578
|
+
messages: [],
|
|
579
|
+
droppedMessages: 0,
|
|
580
|
+
};
|
|
581
|
+
sseConnCounter++;
|
|
582
|
+
sseConnections.push(conn);
|
|
583
|
+
return conn;
|
|
584
|
+
}
|
|
585
|
+
function appendSseMessage(conn, msg) {
|
|
586
|
+
// per-connection ring with first-N preserved; messageIndex stays monotonic via
|
|
587
|
+
// sseMsgCounter (NOT messages.length, which repeats after eviction).
|
|
588
|
+
if (conn.messages.length >= MAX_SSE_MSGS) {
|
|
589
|
+
if (SSE_PRESERVE_FIRST_N < conn.messages.length) {
|
|
590
|
+
conn.messages.splice(SSE_PRESERVE_FIRST_N, 1);
|
|
591
|
+
conn.droppedMessages++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
conn.messages.push(msg);
|
|
595
|
+
}
|
|
596
|
+
function evictOldestClosedSse() {
|
|
597
|
+
// Session request-maps hold object refs (not indices), so no re-index needed;
|
|
598
|
+
// just drop the evicted conn's message counter.
|
|
599
|
+
for (let i = 0; i < sseConnections.length; i++) {
|
|
600
|
+
if (sseConnections[i].closedTsMs !== null) {
|
|
601
|
+
sseMsgCounter.delete(sseConnections[i].connectionId);
|
|
602
|
+
sseConnections.splice(i, 1);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (sseConnections.length) {
|
|
607
|
+
sseMsgCounter.delete(sseConnections[0].connectionId);
|
|
608
|
+
sseConnections.splice(0, 1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function emitSse(conn, ev, transport) {
|
|
612
|
+
if (!sseActive)
|
|
613
|
+
return;
|
|
614
|
+
try {
|
|
615
|
+
let data = ev.data ?? '';
|
|
616
|
+
const truncated = Boolean(ev.truncated) || data.length > MAX_BODY;
|
|
617
|
+
data = data.slice(0, MAX_BODY);
|
|
618
|
+
const cid = conn.connectionId;
|
|
619
|
+
const midx = sseMsgCounter.get(cid) ?? 0;
|
|
620
|
+
sseMsgCounter.set(cid, midx + 1);
|
|
621
|
+
appendSseMessage(conn, {
|
|
622
|
+
connectionId: cid,
|
|
623
|
+
data,
|
|
624
|
+
event: ev.event,
|
|
625
|
+
id: ev.id,
|
|
626
|
+
retry: ev.retry,
|
|
627
|
+
transport,
|
|
628
|
+
truncated,
|
|
629
|
+
tsMs: Date.now(),
|
|
630
|
+
seq: sseSeq,
|
|
631
|
+
messageIndex: midx,
|
|
632
|
+
});
|
|
633
|
+
sseSeq++;
|
|
634
|
+
}
|
|
635
|
+
catch { /* ignore */ }
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Create a CDP session on `page`, wire SSE handlers, register it in the
|
|
639
|
+
* page->session registry, and return it. Handlers close over per-session request
|
|
640
|
+
* maps (requestId collides across CDP targets).
|
|
641
|
+
*/
|
|
642
|
+
export async function startSSECapture(page) {
|
|
643
|
+
const cdp = await page.context().newCDPSession(page);
|
|
644
|
+
await cdp.send('Network.enable');
|
|
645
|
+
const streams = new Map();
|
|
646
|
+
const native = new Map();
|
|
647
|
+
const getNativeConn = (rid, url = '') => {
|
|
648
|
+
let conn = native.get(rid);
|
|
649
|
+
if (!conn) {
|
|
650
|
+
conn = allocSseConn(url, 'eventsource');
|
|
651
|
+
native.set(rid, conn);
|
|
652
|
+
}
|
|
653
|
+
else if (url && !conn.url) {
|
|
654
|
+
conn.url = url;
|
|
655
|
+
try {
|
|
656
|
+
const u = new URL(url);
|
|
657
|
+
conn.domain = u.hostname;
|
|
658
|
+
conn.path = u.pathname || '/';
|
|
659
|
+
}
|
|
660
|
+
catch { /* malformed */ }
|
|
661
|
+
}
|
|
662
|
+
return conn;
|
|
663
|
+
};
|
|
664
|
+
const decodeB64 = (b64) => Buffer.from(b64, 'base64');
|
|
665
|
+
cdp.on('Network.eventSourceMessageReceived', (p) => {
|
|
666
|
+
if (!sseActive)
|
|
667
|
+
return;
|
|
668
|
+
try {
|
|
669
|
+
const conn = getNativeConn(p.requestId);
|
|
670
|
+
emitSse(conn, {
|
|
671
|
+
event: normSseEvent(p.eventName),
|
|
672
|
+
data: p.data ?? '',
|
|
673
|
+
id: p.eventId || null,
|
|
674
|
+
retry: null,
|
|
675
|
+
}, 'eventsource');
|
|
676
|
+
}
|
|
677
|
+
catch { /* ignore */ }
|
|
678
|
+
});
|
|
679
|
+
const onResponse = async (p) => {
|
|
680
|
+
if (!sseActive)
|
|
681
|
+
return;
|
|
682
|
+
try {
|
|
683
|
+
const resp = p.response ?? {};
|
|
684
|
+
if (!isEventStream(resp))
|
|
685
|
+
return;
|
|
686
|
+
const rid = p.requestId;
|
|
687
|
+
const rtype = p.type ?? 'Other';
|
|
688
|
+
if (rtype === 'EventSource') {
|
|
689
|
+
// Native EventSource is pre-parsed; never stream it (double-count).
|
|
690
|
+
getNativeConn(rid, resp.url ?? '');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const transport = rtype.toLowerCase();
|
|
694
|
+
const conn = allocSseConn(resp.url ?? '', transport);
|
|
695
|
+
// NOT-yet-primed: dataReceived arriving during the streamResourceContent await
|
|
696
|
+
// is queued (not fed) so bufferedData (earlier bytes) parses first — ordering
|
|
697
|
+
// must not rely on event-loop scheduling (a buffered burst on connect would
|
|
698
|
+
// otherwise corrupt the parse).
|
|
699
|
+
const entry = { conn, parser: new SSEWireParser(), dec: new TextDecoder('utf-8'), transport, primed: false, pending: [] };
|
|
700
|
+
streams.set(rid, entry);
|
|
701
|
+
let buffered;
|
|
702
|
+
try {
|
|
703
|
+
const res = await cdp.send('Network.streamResourceContent', { requestId: rid });
|
|
704
|
+
buffered = res?.bufferedData;
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
buffered = undefined;
|
|
708
|
+
}
|
|
709
|
+
if (buffered) {
|
|
710
|
+
for (const ev of entry.parser.feed(entry.dec.decode(decodeB64(buffered), { stream: true })))
|
|
711
|
+
emitSse(conn, ev, transport);
|
|
712
|
+
}
|
|
713
|
+
for (const chunk of entry.pending) {
|
|
714
|
+
for (const ev of entry.parser.feed(entry.dec.decode(decodeB64(chunk), { stream: true })))
|
|
715
|
+
emitSse(conn, ev, transport);
|
|
716
|
+
}
|
|
717
|
+
entry.pending = [];
|
|
718
|
+
entry.primed = true;
|
|
719
|
+
}
|
|
720
|
+
catch { /* ignore */ }
|
|
721
|
+
};
|
|
722
|
+
cdp.on('Network.responseReceived', (p) => schedule(onResponse(p)));
|
|
723
|
+
cdp.on('Network.dataReceived', (p) => {
|
|
724
|
+
if (!sseActive)
|
|
725
|
+
return;
|
|
726
|
+
try {
|
|
727
|
+
const st = streams.get(p.requestId);
|
|
728
|
+
if (!st || !p.data)
|
|
729
|
+
return;
|
|
730
|
+
if (!st.primed) {
|
|
731
|
+
st.pending.push(p.data);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const ev of st.parser.feed(st.dec.decode(decodeB64(p.data), { stream: true })))
|
|
735
|
+
emitSse(st.conn, ev, st.transport);
|
|
736
|
+
}
|
|
737
|
+
catch { /* ignore */ }
|
|
738
|
+
});
|
|
739
|
+
const markClosed = (rid, error) => {
|
|
740
|
+
try {
|
|
741
|
+
const conn = streams.get(rid)?.conn ?? native.get(rid);
|
|
742
|
+
if (!conn)
|
|
743
|
+
return;
|
|
744
|
+
conn.closedTsMs = Date.now();
|
|
745
|
+
if (error)
|
|
746
|
+
conn.error = String(error);
|
|
747
|
+
}
|
|
748
|
+
catch { /* ignore */ }
|
|
749
|
+
};
|
|
750
|
+
cdp.on('Network.loadingFinished', (p) => markClosed(p.requestId));
|
|
751
|
+
cdp.on('Network.loadingFailed', (p) => markClosed(p.requestId, p.errorText));
|
|
752
|
+
sseSessions.set(page, cdp);
|
|
753
|
+
return cdp;
|
|
754
|
+
}
|
|
755
|
+
async function startSSECaptureInitial(page) {
|
|
756
|
+
sseConnections = [];
|
|
757
|
+
sseActive = true;
|
|
758
|
+
sseConfigured = true;
|
|
759
|
+
sseSeq = 0;
|
|
760
|
+
sseConnCounter = 0;
|
|
761
|
+
sseMsgCounter.clear();
|
|
762
|
+
sseSessions.clear();
|
|
763
|
+
await startSSECapture(page);
|
|
764
|
+
}
|
|
765
|
+
async function attachSseToPage(page) {
|
|
766
|
+
// Popup attach: a tab opened after startCapture gets its own CDP session.
|
|
767
|
+
try {
|
|
768
|
+
await startSSECapture(page);
|
|
769
|
+
}
|
|
770
|
+
catch { /* ignore */ }
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Async SSE teardown: stop handlers, drain pending tasks, detach every CDP
|
|
774
|
+
* session. The context 'page' listener is removed by the sync stopCapture
|
|
775
|
+
* BEFORE this runs, so no new popup-attach task can be scheduled mid-teardown.
|
|
776
|
+
*/
|
|
777
|
+
export async function stopSSECapture() {
|
|
778
|
+
sseActive = false;
|
|
779
|
+
const pending = [...sseTasks];
|
|
780
|
+
if (pending.length)
|
|
781
|
+
await Promise.allSettled(pending);
|
|
782
|
+
for (const cdp of [...sseSessions.values()]) {
|
|
783
|
+
try {
|
|
784
|
+
await cdp.detach();
|
|
785
|
+
}
|
|
786
|
+
catch { /* ignore */ }
|
|
787
|
+
}
|
|
788
|
+
sseSessions.clear();
|
|
789
|
+
}
|
|
492
790
|
// ── Performance ──
|
|
493
791
|
const WEB_VITALS_INIT_SCRIPT =
|
|
494
792
|
// Vendored web-vitals v4 IIFE
|
|
@@ -586,6 +884,43 @@ function buildWebSocketAPI() {
|
|
|
586
884
|
get all() { return [...wsConnections]; },
|
|
587
885
|
};
|
|
588
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Build a query API object for SSE — injected as `sse` into the user's code,
|
|
889
|
+
* alongside `network`. Light filters; the query code does the rest. Mirror of
|
|
890
|
+
* the Python _ReadOnlySSELog.
|
|
891
|
+
*/
|
|
892
|
+
function buildSSEAPI() {
|
|
893
|
+
const matchUrl = (c, url, urlGlob) => {
|
|
894
|
+
if (url && c.url !== url)
|
|
895
|
+
return false;
|
|
896
|
+
if (urlGlob && !globToRegExp(urlGlob).test(c.url))
|
|
897
|
+
return false;
|
|
898
|
+
return true;
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
connections(filter) {
|
|
902
|
+
let results = [...sseConnections];
|
|
903
|
+
if (filter)
|
|
904
|
+
results = results.filter(c => matchUrl(c, filter.url, filter.urlGlob));
|
|
905
|
+
return results;
|
|
906
|
+
},
|
|
907
|
+
messages(filter) {
|
|
908
|
+
const out = [];
|
|
909
|
+
for (const c of sseConnections) {
|
|
910
|
+
if (filter && !matchUrl(c, filter.url, filter.urlGlob))
|
|
911
|
+
continue;
|
|
912
|
+
for (const m of c.messages) {
|
|
913
|
+
if (filter?.event !== undefined && m.event !== filter.event)
|
|
914
|
+
continue;
|
|
915
|
+
out.push(m);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
out.sort((a, b) => a.seq - b.seq); // global merge order across connections
|
|
919
|
+
return out;
|
|
920
|
+
},
|
|
921
|
+
get all() { return [...sseConnections]; },
|
|
922
|
+
};
|
|
923
|
+
}
|
|
589
924
|
/**
|
|
590
925
|
* Build a query API object for console — injected as `consoleLog` into the user's code.
|
|
591
926
|
*/
|
|
@@ -631,19 +966,24 @@ function buildStorageAPI(data) {
|
|
|
631
966
|
};
|
|
632
967
|
}
|
|
633
968
|
export async function devtoolsNetworkQuery(codeJs) {
|
|
634
|
-
const network = buildNetworkAPI();
|
|
635
969
|
try {
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
|
|
970
|
+
// Build args dynamically, gated on the PERSISTENT configured flags (never
|
|
971
|
+
// .length — eviction can splice connections to 0 mid-session, which would
|
|
972
|
+
// drop the object and silently pass + regress WS). websocket and sse are
|
|
973
|
+
// independent: a query can use network only, +ws, +sse, or all three. When
|
|
974
|
+
// neither was configured this is the exact single-arg form (byte-identical).
|
|
975
|
+
const names = ['network'];
|
|
976
|
+
const values = [buildNetworkAPI()];
|
|
639
977
|
if (wsConfigured) {
|
|
640
|
-
|
|
641
|
-
|
|
978
|
+
names.push('websocket');
|
|
979
|
+
values.push(buildWebSocketAPI());
|
|
642
980
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
981
|
+
if (sseConfigured) {
|
|
982
|
+
names.push('sse');
|
|
983
|
+
values.push(buildSSEAPI());
|
|
646
984
|
}
|
|
985
|
+
const fn = new Function(...names, `"use strict";\n${codeJs}`);
|
|
986
|
+
const result = fn(...values);
|
|
647
987
|
return result != null ? String(result) : '';
|
|
648
988
|
}
|
|
649
989
|
catch (e) {
|