@ai-ecoverse/slicc-browsers 1.22.0 → 1.23.0
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 +4 -4
- package/close-protected-shape.mjs +563 -0
- package/fixture-server.mjs +52 -0
- package/guard-runner.mjs +653 -125
- package/guard-vectors.json +15040 -12929
- package/package.json +6 -4
- package/protected-worker.mjs +511 -0
- package/schema/guard-vectors.schema.json +95 -0
package/guard-runner.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { fixtureClient, prepareFixture } from './fixture-client.mjs';
|
|
|
3
3
|
export { fixtureClient };
|
|
4
4
|
|
|
5
5
|
import { bfcacheRestore } from './bfcache-restore.mjs';
|
|
6
|
+
import { closeProtectedShape } from './close-protected-shape.mjs';
|
|
6
7
|
import { extensionFrame } from './extension-frame.mjs';
|
|
7
8
|
import { DEFAULT_EXTENSION_WORLD_ID, extensionWorld } from './extension-world.mjs';
|
|
8
9
|
import { frameRead } from './frame-read.mjs';
|
|
@@ -31,12 +32,48 @@ import {
|
|
|
31
32
|
} from './guard-driver.mjs';
|
|
32
33
|
import { createObserverTarget, discoveryViolation } from './observer-target.mjs';
|
|
33
34
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
35
|
+
import {
|
|
36
|
+
leftoverPersistentWorker,
|
|
37
|
+
persistentWorkerTypes,
|
|
38
|
+
probeHarnessIncomplete,
|
|
39
|
+
probeWorkerSessions,
|
|
40
|
+
protectedWorker,
|
|
41
|
+
releasePersistentWorkerSessions,
|
|
42
|
+
workerTypeForKind,
|
|
43
|
+
} from './protected-worker.mjs';
|
|
34
44
|
import { redirectSecret } from './redirect-secret.mjs';
|
|
35
45
|
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
36
46
|
|
|
47
|
+
export { leftoverPersistentWorker, releasePersistentWorkerSessions, workerTypeForKind };
|
|
48
|
+
|
|
37
49
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
50
|
const namedResultMs = 10000;
|
|
39
51
|
const observeMs = 5000;
|
|
52
|
+
|
|
53
|
+
function autoAttachSession(events, start, targetId) {
|
|
54
|
+
return events
|
|
55
|
+
.slice(start)
|
|
56
|
+
.filter(
|
|
57
|
+
(event) =>
|
|
58
|
+
event.method === 'Target.attachedToTarget' &&
|
|
59
|
+
event.params?.targetInfo?.targetId === targetId &&
|
|
60
|
+
event.params?.sessionId
|
|
61
|
+
)
|
|
62
|
+
.at(-1)?.params?.sessionId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function reusableAttachSession(step, out, params, events, start) {
|
|
66
|
+
if (!step?.reuseSession || step.cdp !== 'Target.attachToTarget' || !params?.targetId)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (out?.ok && out.result?.sessionId) return undefined;
|
|
69
|
+
const message = String(out?.error?.message ?? '');
|
|
70
|
+
if (/refused|not allowed/i.test(message)) return undefined;
|
|
71
|
+
const already =
|
|
72
|
+
(out?.ok === true && !out.result?.sessionId) ||
|
|
73
|
+
/already attached|attached already|Target is attaching or attached/i.test(message);
|
|
74
|
+
if (!already) return undefined;
|
|
75
|
+
return autoAttachSession(events, start, params.targetId);
|
|
76
|
+
}
|
|
40
77
|
const browserUiAttempts = 3;
|
|
41
78
|
const browserUiMs = 5000;
|
|
42
79
|
const q = JSON.stringify;
|
|
@@ -158,6 +195,7 @@ class Execution {
|
|
|
158
195
|
this.unsupported = undefined;
|
|
159
196
|
this.preSent = [];
|
|
160
197
|
this.notes = [];
|
|
198
|
+
this.sharedNotes = Array.isArray(options.notes) ? options.notes : undefined;
|
|
161
199
|
this.answers = [];
|
|
162
200
|
this.internalSessions = new Set();
|
|
163
201
|
this.attachSettings = new Map();
|
|
@@ -171,12 +209,75 @@ class Execution {
|
|
|
171
209
|
this.refusalScale = options.refusalScale ?? 1;
|
|
172
210
|
this.childRuns = new Map();
|
|
173
211
|
this.named = new Map();
|
|
212
|
+
this.eventMarks = new Map();
|
|
174
213
|
this.processed = new Set();
|
|
175
214
|
this.eventStart = this.client.events.length;
|
|
176
215
|
this.actionEventStart = this.eventStart;
|
|
177
216
|
this.requestStart = undefined;
|
|
178
217
|
this.trace = [];
|
|
179
218
|
this.stopped = false;
|
|
219
|
+
this.cdpTrace = [];
|
|
220
|
+
this.cdpSessionTypes = new Map();
|
|
221
|
+
this.cdpTraceInstalled = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
note(message) {
|
|
225
|
+
if (this.notes.length >= 80) this.notes.shift();
|
|
226
|
+
Array.prototype.push.call(this.notes, message);
|
|
227
|
+
if (this.sharedNotes && this.sharedNotes !== this.notes) {
|
|
228
|
+
if (this.sharedNotes.length >= 80) this.sharedNotes.shift();
|
|
229
|
+
Array.prototype.push.call(this.sharedNotes, message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
installClientTrace() {
|
|
234
|
+
if (!this.spawnWorkerKind() || this.cdpTraceInstalled) return;
|
|
235
|
+
this.cdpTraceInstalled = true;
|
|
236
|
+
const prev = this.client.__sliccOnSend;
|
|
237
|
+
this.client.__sliccBoundGetFrameTree = true;
|
|
238
|
+
this.client.__sliccOnSend = (entry) => {
|
|
239
|
+
prev?.(entry);
|
|
240
|
+
this.recordCdpTrace(entry);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
recordCdpTrace(entry) {
|
|
245
|
+
let type = entry.sessionId ? this.cdpSessionTypes.get(entry.sessionId) : 'browser';
|
|
246
|
+
if (entry.sessionId && type === undefined) {
|
|
247
|
+
type = '';
|
|
248
|
+
const events = this.client.events;
|
|
249
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
250
|
+
const event = events[i];
|
|
251
|
+
if (
|
|
252
|
+
event.method === 'Target.attachedToTarget' &&
|
|
253
|
+
event.params?.sessionId === entry.sessionId
|
|
254
|
+
) {
|
|
255
|
+
type = event.params.targetInfo?.type ?? '';
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.cdpSessionTypes.set(entry.sessionId, type);
|
|
260
|
+
}
|
|
261
|
+
if (this.cdpTrace.length >= 40) this.cdpTrace.shift();
|
|
262
|
+
this.cdpTrace.push({
|
|
263
|
+
method: entry.method,
|
|
264
|
+
sessionId: entry.sessionId ?? '',
|
|
265
|
+
type: type || (entry.sessionId ? '?' : 'browser'),
|
|
266
|
+
ms: entry.ms,
|
|
267
|
+
status: String(entry.status).slice(0, 200),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
noteClientTrace() {
|
|
272
|
+
if (!this.cdpTrace.length) return;
|
|
273
|
+
this.note(
|
|
274
|
+
`client cdp: ${this.cdpTrace
|
|
275
|
+
.map(
|
|
276
|
+
(call) =>
|
|
277
|
+
`${call.method}@${call.sessionId || 'browser'}:${call.type || '?'} ${call.ms}ms ${call.status}`
|
|
278
|
+
)
|
|
279
|
+
.join(' | ')}`
|
|
280
|
+
);
|
|
180
281
|
}
|
|
181
282
|
|
|
182
283
|
actor() {
|
|
@@ -199,8 +300,7 @@ class Execution {
|
|
|
199
300
|
);
|
|
200
301
|
return read.ok && read.result.targetInfo?.attached === false;
|
|
201
302
|
}, 5000);
|
|
202
|
-
if (!released)
|
|
203
|
-
this.notes.push(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
303
|
+
if (!released) this.note(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
204
304
|
const { sessionId } = await this.reader.send('Target.attachToTarget', {
|
|
205
305
|
targetId: this.pageTarget,
|
|
206
306
|
flatten: true,
|
|
@@ -407,53 +507,55 @@ class Execution {
|
|
|
407
507
|
);
|
|
408
508
|
}
|
|
409
509
|
|
|
510
|
+
async readerFrameTree(targetId, label) {
|
|
511
|
+
const attached = await outcome(
|
|
512
|
+
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
513
|
+
);
|
|
514
|
+
if (!attached.ok) return { ok: false, sessionId: undefined, frameTree: undefined };
|
|
515
|
+
const sessionId = attached.result.sessionId;
|
|
516
|
+
await outcome(this.reader.send('Runtime.runIfWaitingForDebugger', {}, sessionId));
|
|
517
|
+
const read = await outcome(this.reader.send('Page.getFrameTree', {}, sessionId));
|
|
518
|
+
if (!read.ok) {
|
|
519
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Page.getFrameTree timed out on ${label} target ${targetId} session ${sessionId}: ${read.error?.message ?? 'unknown'}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return { ok: true, sessionId, frameTree: read.result.frameTree };
|
|
525
|
+
}
|
|
526
|
+
|
|
410
527
|
async blocked(spec, observer) {
|
|
411
528
|
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
412
529
|
if (observer === 'client') return this.clientBlocked(spec, urls);
|
|
413
530
|
const targetId = spec.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
414
531
|
if (!targetId) return false;
|
|
415
|
-
const attached = await
|
|
416
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
417
|
-
);
|
|
532
|
+
const attached = await this.readerFrameTree(targetId, 'blocked page');
|
|
418
533
|
if (!attached.ok) return false;
|
|
419
534
|
try {
|
|
420
|
-
const read = await outcome(
|
|
421
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
422
|
-
);
|
|
423
535
|
const blockedFrame = (frame) =>
|
|
424
536
|
Boolean(
|
|
425
537
|
frame && urls.includes(frame.unreachableUrl) && frame.url.startsWith('chrome-error://')
|
|
426
538
|
);
|
|
427
539
|
const visit = (tree) =>
|
|
428
540
|
Boolean(tree && (blockedFrame(tree.frame) || (tree.childFrames ?? []).some(visit)));
|
|
429
|
-
if (!read.ok) return false;
|
|
430
541
|
if (spec.frame) {
|
|
431
|
-
if ((
|
|
542
|
+
if ((attached.frameTree.childFrames ?? []).some(visit)) return true;
|
|
432
543
|
return Boolean(await this.blockedFrameTarget(urls, targetId));
|
|
433
544
|
}
|
|
434
|
-
return blockedFrame(
|
|
545
|
+
return blockedFrame(attached.frameTree.frame);
|
|
435
546
|
} finally {
|
|
436
|
-
await outcome(
|
|
437
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
438
|
-
);
|
|
547
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
439
548
|
}
|
|
440
549
|
}
|
|
441
550
|
|
|
442
551
|
async frameIds(targetId) {
|
|
443
|
-
const attached = await
|
|
444
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
445
|
-
);
|
|
552
|
+
const attached = await this.readerFrameTree(targetId, 'frameIds page');
|
|
446
553
|
if (!attached.ok) return [];
|
|
447
554
|
try {
|
|
448
|
-
const read = await outcome(
|
|
449
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
450
|
-
);
|
|
451
555
|
const ids = (tree) => [tree.frame.id, ...(tree.childFrames ?? []).flatMap(ids)];
|
|
452
|
-
return
|
|
556
|
+
return ids(attached.frameTree);
|
|
453
557
|
} finally {
|
|
454
|
-
await outcome(
|
|
455
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
456
|
-
);
|
|
558
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
457
559
|
}
|
|
458
560
|
}
|
|
459
561
|
|
|
@@ -463,21 +565,16 @@ class Execution {
|
|
|
463
565
|
if (!listed.ok) return undefined;
|
|
464
566
|
for (const info of listed.result.targetInfos) {
|
|
465
567
|
if (info.type !== 'iframe' || !parents.includes(info.parentFrameId)) continue;
|
|
466
|
-
|
|
467
|
-
this.reader.send('Target.attachToTarget', { targetId: info.targetId, flatten: true })
|
|
468
|
-
);
|
|
469
|
-
if (!attached.ok) continue;
|
|
568
|
+
let sessionId;
|
|
470
569
|
try {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const frame =
|
|
570
|
+
const attached = await this.readerFrameTree(info.targetId, `iframe ${info.type}`);
|
|
571
|
+
if (!attached.ok) continue;
|
|
572
|
+
sessionId = attached.sessionId;
|
|
573
|
+
const frame = attached.frameTree?.frame;
|
|
475
574
|
if (frame?.url.startsWith('chrome-error://') && urls.includes(frame.unreachableUrl))
|
|
476
575
|
return info.targetId;
|
|
477
576
|
} finally {
|
|
478
|
-
await outcome(
|
|
479
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
480
|
-
);
|
|
577
|
+
if (sessionId) await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
481
578
|
}
|
|
482
579
|
}
|
|
483
580
|
return undefined;
|
|
@@ -531,7 +628,7 @@ class Execution {
|
|
|
531
628
|
} catch (error) {
|
|
532
629
|
const message = String(error?.message ?? error);
|
|
533
630
|
if (!/timed? ?out/i.test(message)) throw error;
|
|
534
|
-
this.
|
|
631
|
+
this.note(`Target.createBrowserContext retried once after: ${message}`);
|
|
535
632
|
return (await client.send('Target.createBrowserContext', {})).browserContextId;
|
|
536
633
|
}
|
|
537
634
|
}
|
|
@@ -593,17 +690,38 @@ class Execution {
|
|
|
593
690
|
}
|
|
594
691
|
}
|
|
595
692
|
|
|
693
|
+
async resumeWaiting(from = this.eventStart) {
|
|
694
|
+
for (const event of this.client.events.slice(from)) {
|
|
695
|
+
if (
|
|
696
|
+
event.method === 'Target.attachedToTarget' &&
|
|
697
|
+
event.params?.waitingForDebugger &&
|
|
698
|
+
event.params?.sessionId
|
|
699
|
+
)
|
|
700
|
+
await outcome(
|
|
701
|
+
this.client.send('Runtime.runIfWaitingForDebugger', {}, event.params.sessionId)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
596
706
|
adopt(event) {
|
|
597
707
|
const info = event.params.targetInfo;
|
|
598
708
|
const child = event.params.sessionId;
|
|
599
709
|
if (info?.type === 'browser_ui') return;
|
|
600
710
|
const paused = event.params.waitingForDebugger || this.tabSessions.has(event.sessionId);
|
|
711
|
+
const workerRow = Boolean(this.spawnWorkerKind());
|
|
601
712
|
if (this.expectWhere && !this.childWhere) {
|
|
602
713
|
this.deferred.push(event);
|
|
603
714
|
return;
|
|
604
715
|
}
|
|
605
|
-
if (
|
|
716
|
+
if (
|
|
717
|
+
info?.browserContextId &&
|
|
718
|
+
this.vars.context &&
|
|
719
|
+
info.browserContextId !== this.vars.context
|
|
720
|
+
) {
|
|
721
|
+
if (paused && workerRow)
|
|
722
|
+
this.pending.push(outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child)));
|
|
606
723
|
return;
|
|
724
|
+
}
|
|
607
725
|
const matched = !this.childWhere || this.childMatches(info);
|
|
608
726
|
const isTab = info?.type === 'tab';
|
|
609
727
|
if (!matched && !isTab) {
|
|
@@ -620,6 +738,27 @@ class Execution {
|
|
|
620
738
|
? this.childSteps
|
|
621
739
|
: this.childSteps.filter((step) => step.cdp === 'Target.setAutoAttach');
|
|
622
740
|
const started = (async () => {
|
|
741
|
+
if (workerRow) {
|
|
742
|
+
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
743
|
+
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
744
|
+
if (reload < 0) await this.execute(nested, child);
|
|
745
|
+
else {
|
|
746
|
+
await this.execute(nested.slice(0, reload), child);
|
|
747
|
+
const start = this.client.events.length;
|
|
748
|
+
await this.poll(
|
|
749
|
+
() =>
|
|
750
|
+
this.client.events
|
|
751
|
+
.slice(start)
|
|
752
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === child),
|
|
753
|
+
1500
|
|
754
|
+
);
|
|
755
|
+
await this.execute(nested.slice(reload), child);
|
|
756
|
+
}
|
|
757
|
+
const resumed = this.client.events.length;
|
|
758
|
+
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
759
|
+
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
623
762
|
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
624
763
|
if (reload < 0) await this.execute(nested, child);
|
|
625
764
|
else {
|
|
@@ -639,7 +778,13 @@ class Execution {
|
|
|
639
778
|
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
640
779
|
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
641
780
|
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
642
|
-
})().catch((error) =>
|
|
781
|
+
})().catch((error) =>
|
|
782
|
+
this.errors.push(
|
|
783
|
+
workerRow
|
|
784
|
+
? `adopt ${info?.type ?? 'target'} ${child}: ${String(error.message ?? error)}`
|
|
785
|
+
: String(error.message ?? error)
|
|
786
|
+
)
|
|
787
|
+
);
|
|
643
788
|
this.childRuns.set(child, started);
|
|
644
789
|
this.pending.push(started);
|
|
645
790
|
}
|
|
@@ -704,7 +849,11 @@ class Execution {
|
|
|
704
849
|
}
|
|
705
850
|
|
|
706
851
|
childMatches(info) {
|
|
707
|
-
if (this.childWhere.
|
|
852
|
+
if (this.childWhere.types !== undefined) {
|
|
853
|
+
if (!this.childWhere.types.includes(info?.type)) return false;
|
|
854
|
+
} else if (this.childWhere.type !== undefined) {
|
|
855
|
+
if (this.childWhere.type !== info?.type) return false;
|
|
856
|
+
}
|
|
708
857
|
if (this.childWhere.openerId === 'page') return info?.openerId === this.pageTarget;
|
|
709
858
|
if (this.childWhere.urlPrefix !== undefined)
|
|
710
859
|
return String(info?.url ?? '').startsWith(this.childWhere.urlPrefix);
|
|
@@ -751,6 +900,7 @@ class Execution {
|
|
|
751
900
|
'redirectSecret',
|
|
752
901
|
'unparseable',
|
|
753
902
|
'errorPage',
|
|
903
|
+
'closeProtectedShape',
|
|
754
904
|
'extensionWorld',
|
|
755
905
|
'extensionFrame',
|
|
756
906
|
'frameRead',
|
|
@@ -759,9 +909,14 @@ class Execution {
|
|
|
759
909
|
if (probe && !out.ok) this.errors.push(`${probe}: ${out.error?.message ?? 'probe failed'}`);
|
|
760
910
|
if (step.client && !out.ok) this.errors.push(`client exit: ${out.error.message}`);
|
|
761
911
|
if (
|
|
762
|
-
[
|
|
763
|
-
|
|
764
|
-
|
|
912
|
+
[
|
|
913
|
+
'errorPage',
|
|
914
|
+
'closeProtectedShape',
|
|
915
|
+
'redirectSecret',
|
|
916
|
+
'extensionFrame',
|
|
917
|
+
'frameRead',
|
|
918
|
+
'bfcacheRestore',
|
|
919
|
+
].includes(probe) &&
|
|
765
920
|
out.ok &&
|
|
766
921
|
out.result?.incomplete
|
|
767
922
|
)
|
|
@@ -791,7 +946,7 @@ class Execution {
|
|
|
791
946
|
Boolean(childSession) && !out.ok && this.detached(childSession)
|
|
792
947
|
);
|
|
793
948
|
if ((step.isolatedWorld || step.observer) && !out.ok)
|
|
794
|
-
this.
|
|
949
|
+
this.note(
|
|
795
950
|
`${step.isolatedWorld ? `isolatedWorld ${step.isolatedWorld}` : `observer ${step.observer}`}: ${out.error?.message ?? 'failed'}`
|
|
796
951
|
);
|
|
797
952
|
this.options.debug?.({ step, out, side: this.side });
|
|
@@ -826,7 +981,7 @@ class Execution {
|
|
|
826
981
|
return Boolean(next);
|
|
827
982
|
}, browserUiMs);
|
|
828
983
|
if (!next) return undefined;
|
|
829
|
-
this.
|
|
984
|
+
this.note(
|
|
830
985
|
`{browserUi} ${vanished} was gone when ${step.cdp} reached it; retried with ${next.targetId}`
|
|
831
986
|
);
|
|
832
987
|
this.vars.browserUi = next.targetId;
|
|
@@ -857,48 +1012,14 @@ class Execution {
|
|
|
857
1012
|
return;
|
|
858
1013
|
}
|
|
859
1014
|
if (step.client) return outcome(this.exit());
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
targetId: this.pageTarget,
|
|
869
|
-
ordinary: this.vars.ordinary,
|
|
870
|
-
tag: this.vars.tag,
|
|
871
|
-
fixture: this.options.fixture,
|
|
872
|
-
mode: step.redirectSecret,
|
|
873
|
-
side: this.side,
|
|
874
|
-
settleScale: this.scale,
|
|
875
|
-
controls: (this.clients.redirectSecretControls ??= {}),
|
|
876
|
-
internalSessions: this.internalSessions,
|
|
877
|
-
})
|
|
878
|
-
);
|
|
879
|
-
} finally {
|
|
880
|
-
this.internalSessions.delete(session);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
if (step.errorPage)
|
|
884
|
-
return outcome(
|
|
885
|
-
protectedErrorPage({
|
|
886
|
-
client: this.client,
|
|
887
|
-
reader: this.reader,
|
|
888
|
-
session,
|
|
889
|
-
targetId: this.pageTarget,
|
|
890
|
-
ordinary: this.vars.ordinary,
|
|
891
|
-
protectedOrigin: this.vars.seven,
|
|
892
|
-
tag: this.vars.tag,
|
|
893
|
-
internalSessions: this.internalSessions,
|
|
894
|
-
mode: step.errorPage,
|
|
895
|
-
side: this.side,
|
|
896
|
-
secret: step.secret,
|
|
897
|
-
discovery: supported(['discovery'], this.options.capabilities),
|
|
898
|
-
notes: this.notes,
|
|
899
|
-
budget: this.bound(5000),
|
|
900
|
-
})
|
|
901
|
-
);
|
|
1015
|
+
if (
|
|
1016
|
+
step.redirectSecret ||
|
|
1017
|
+
step.errorPage ||
|
|
1018
|
+
step.closeProtectedShape ||
|
|
1019
|
+
step.spawnWorker ||
|
|
1020
|
+
step.probeSessions
|
|
1021
|
+
)
|
|
1022
|
+
return this.stepProbe(step, session);
|
|
902
1023
|
if (step.bfcacheRestore)
|
|
903
1024
|
return outcome(
|
|
904
1025
|
bfcacheRestore({
|
|
@@ -1009,12 +1130,10 @@ class Execution {
|
|
|
1009
1130
|
|
|
1010
1131
|
async isolatedWorld(step) {
|
|
1011
1132
|
const targetId = step.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
});
|
|
1133
|
+
const attached = await this.readerFrameTree(targetId, 'isolatedWorld page');
|
|
1134
|
+
if (!attached.ok) throw new Error('isolatedWorld attach failed');
|
|
1135
|
+
const { sessionId, frameTree } = attached;
|
|
1016
1136
|
try {
|
|
1017
|
-
const { frameTree } = await this.reader.send('Page.getFrameTree', {}, sessionId);
|
|
1018
1137
|
const { executionContextId } = await this.reader.send(
|
|
1019
1138
|
'Page.createIsolatedWorld',
|
|
1020
1139
|
{ frameId: frameTree.frame.id, worldName: step.isolatedWorld },
|
|
@@ -1041,6 +1160,248 @@ class Execution {
|
|
|
1041
1160
|
}
|
|
1042
1161
|
}
|
|
1043
1162
|
|
|
1163
|
+
stepProbe(step, session) {
|
|
1164
|
+
if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
|
|
1165
|
+
if (step.closeProtectedShape) return this.runCloseProtectedShape(step);
|
|
1166
|
+
if (step.spawnWorker) return this.spawnWorker(step);
|
|
1167
|
+
return this.probeSessions(step);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async originProbe(step, session) {
|
|
1171
|
+
if (step.redirectSecret) {
|
|
1172
|
+
this.internalSessions.add(session);
|
|
1173
|
+
try {
|
|
1174
|
+
return await outcome(
|
|
1175
|
+
redirectSecret({
|
|
1176
|
+
client: this.client,
|
|
1177
|
+
reader: this.reader,
|
|
1178
|
+
session,
|
|
1179
|
+
targetId: this.pageTarget,
|
|
1180
|
+
ordinary: this.vars.ordinary,
|
|
1181
|
+
tag: this.vars.tag,
|
|
1182
|
+
fixture: this.options.fixture,
|
|
1183
|
+
mode: step.redirectSecret,
|
|
1184
|
+
side: this.side,
|
|
1185
|
+
settleScale: this.scale,
|
|
1186
|
+
controls: (this.clients.redirectSecretControls ??= {}),
|
|
1187
|
+
internalSessions: this.internalSessions,
|
|
1188
|
+
})
|
|
1189
|
+
);
|
|
1190
|
+
} finally {
|
|
1191
|
+
this.internalSessions.delete(session);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return outcome(
|
|
1195
|
+
protectedErrorPage({
|
|
1196
|
+
client: this.client,
|
|
1197
|
+
reader: this.reader,
|
|
1198
|
+
session,
|
|
1199
|
+
targetId: this.pageTarget,
|
|
1200
|
+
ordinary: this.vars.ordinary,
|
|
1201
|
+
protectedOrigin: this.vars.seven,
|
|
1202
|
+
tag: this.vars.tag,
|
|
1203
|
+
internalSessions: this.internalSessions,
|
|
1204
|
+
mode: step.errorPage,
|
|
1205
|
+
side: this.side,
|
|
1206
|
+
secret: step.secret,
|
|
1207
|
+
discovery: supported(['discovery'], this.options.capabilities),
|
|
1208
|
+
notes: this.notes,
|
|
1209
|
+
budget: this.bound(5000),
|
|
1210
|
+
})
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
runCloseProtectedShape(step) {
|
|
1215
|
+
return outcome(
|
|
1216
|
+
closeProtectedShape({
|
|
1217
|
+
client: this.client,
|
|
1218
|
+
reader: this.reader,
|
|
1219
|
+
ordinary: this.vars.ordinary,
|
|
1220
|
+
protectedOrigin: this.vars.target,
|
|
1221
|
+
shape: step.closeProtectedShape,
|
|
1222
|
+
tag: this.vars.tag,
|
|
1223
|
+
internalSessions: this.internalSessions,
|
|
1224
|
+
notes: this.notes,
|
|
1225
|
+
budget: this.bound(5000),
|
|
1226
|
+
releaseSlow: this.options.fixture.releaseSlow,
|
|
1227
|
+
})
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
spawnWorkerKind() {
|
|
1232
|
+
return allSteps([...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])]).find(
|
|
1233
|
+
(step) => step.spawnWorker
|
|
1234
|
+
)?.spawnWorker;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async ensurePriorWorkersGone(kind) {
|
|
1238
|
+
if (this.side !== 'guarded') return;
|
|
1239
|
+
const type = workerTypeForKind(kind);
|
|
1240
|
+
if (!persistentWorkerTypes.has(type)) return;
|
|
1241
|
+
const prior = this.clients.priorWorker;
|
|
1242
|
+
const leftover = await leftoverPersistentWorker(this.reader ?? this.client, {
|
|
1243
|
+
type,
|
|
1244
|
+
...(prior?.url || prior?.targetId ? { url: prior?.url, targetId: prior?.targetId } : {}),
|
|
1245
|
+
});
|
|
1246
|
+
if (leftover)
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`leftover ${type} still present before guarded spawn: ${leftover.url ?? leftover.targetId}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async spawnWorker(step) {
|
|
1253
|
+
if (step.as) this.eventMarks.set(step.as, this.client.events.length);
|
|
1254
|
+
const pageUrl = expand(step.url ?? '{target}/doc?n=host-{tag}', this.vars);
|
|
1255
|
+
const hostUrl = step.frame ? expand(step.frame, this.vars) : undefined;
|
|
1256
|
+
await this.ensurePriorWorkersGone(step.spawnWorker);
|
|
1257
|
+
const out = await outcome(
|
|
1258
|
+
protectedWorker({
|
|
1259
|
+
reader: this.reader,
|
|
1260
|
+
kind: step.spawnWorker,
|
|
1261
|
+
pageUrl,
|
|
1262
|
+
hostUrl,
|
|
1263
|
+
tag: this.vars.tag,
|
|
1264
|
+
context: this.vars.context,
|
|
1265
|
+
notes: this.notes,
|
|
1266
|
+
budget: this.bound(5000),
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
if (out.ok && out.result?.browserContextId) {
|
|
1270
|
+
this.contexts.push(out.result.browserContextId);
|
|
1271
|
+
this.vars.context = out.result.browserContextId;
|
|
1272
|
+
}
|
|
1273
|
+
if (out.ok && out.result?.pageTargetId) this.targets.push(out.result.pageTargetId);
|
|
1274
|
+
if (out.ok && out.result?.workerTargetId) {
|
|
1275
|
+
this.vars.workerTarget = out.result.workerTargetId;
|
|
1276
|
+
this.targets.push(out.result.workerTargetId);
|
|
1277
|
+
}
|
|
1278
|
+
if (out.ok && out.result?.workerUrl && this.side === 'control')
|
|
1279
|
+
this.clients.priorWorker = {
|
|
1280
|
+
type: out.result.type,
|
|
1281
|
+
url: out.result.workerUrl,
|
|
1282
|
+
targetId: out.result.workerTargetId,
|
|
1283
|
+
};
|
|
1284
|
+
if (!out.ok) this.errors.push(`spawnWorker: ${out.error?.message ?? 'probe failed'}`);
|
|
1285
|
+
if (out.ok && out.result?.incomplete)
|
|
1286
|
+
this.incomplete.push(`spawnWorker: ${out.result.incomplete}`);
|
|
1287
|
+
return out;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
probeSessionIds(step) {
|
|
1291
|
+
const mark = this.eventMarks.get(step.probeSessions.since) ?? this.eventStart;
|
|
1292
|
+
const sessions = [];
|
|
1293
|
+
for (const event of this.client.events.slice(mark)) {
|
|
1294
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId)
|
|
1295
|
+
sessions.push(event.params.sessionId);
|
|
1296
|
+
}
|
|
1297
|
+
for (const name of step.probeSessions.attach ?? ['attach']) {
|
|
1298
|
+
const sessionId = this.calls[name]?.result?.sessionId;
|
|
1299
|
+
if (sessionId) sessions.push(sessionId);
|
|
1300
|
+
}
|
|
1301
|
+
return sessions;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async probeSessions(step) {
|
|
1305
|
+
const out = await outcome(
|
|
1306
|
+
probeWorkerSessions({
|
|
1307
|
+
client: this.client,
|
|
1308
|
+
sessions: this.probeSessionIds(step),
|
|
1309
|
+
path: expand(step.probeSessions.path, this.vars),
|
|
1310
|
+
tag: this.vars.tag,
|
|
1311
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
this.probedSessionIds ??= new Set();
|
|
1315
|
+
for (const sessionId of out.result?.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1316
|
+
if (!out.ok) {
|
|
1317
|
+
this.errors.push(`probeSessions: ${out.error?.message ?? 'probe failed'}`);
|
|
1318
|
+
return out;
|
|
1319
|
+
}
|
|
1320
|
+
if (out.result?.incomplete) {
|
|
1321
|
+
const reason = probeHarnessIncomplete(this.side, out.result.incomplete);
|
|
1322
|
+
if (reason) this.incomplete.push(`probeSessions: ${reason}`);
|
|
1323
|
+
const { incomplete: _drop, ...result } = out.result;
|
|
1324
|
+
return { ...out, result };
|
|
1325
|
+
}
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
probeStep() {
|
|
1330
|
+
return [...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])].find(
|
|
1331
|
+
(entry) => entry.probeSessions
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async probeFreshSessions() {
|
|
1336
|
+
const step = this.probeStep();
|
|
1337
|
+
if (!step?.probeSessions) return false;
|
|
1338
|
+
const expanded = expand(step, this.vars);
|
|
1339
|
+
this.probedSessionIds ??= new Set();
|
|
1340
|
+
const fresh = this.probeSessionIds(expanded).filter(
|
|
1341
|
+
(sessionId) => sessionId && !this.probedSessionIds.has(sessionId)
|
|
1342
|
+
);
|
|
1343
|
+
if (!fresh.length) return this.calls.probe?.result?.reachable === true;
|
|
1344
|
+
const out = await probeWorkerSessions({
|
|
1345
|
+
client: this.client,
|
|
1346
|
+
sessions: fresh,
|
|
1347
|
+
path: expand(expanded.probeSessions.path, this.vars),
|
|
1348
|
+
tag: this.vars.tag,
|
|
1349
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1350
|
+
});
|
|
1351
|
+
for (const sessionId of out.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1352
|
+
const sessions = [
|
|
1353
|
+
...new Set([...(this.calls.probe?.result?.sessions ?? []), ...(out.probedIds ?? fresh)]),
|
|
1354
|
+
];
|
|
1355
|
+
if (out.incomplete) {
|
|
1356
|
+
this.calls.probe = {
|
|
1357
|
+
ok: true,
|
|
1358
|
+
result: { ...(this.calls.probe?.result ?? {}), ...out, sessions },
|
|
1359
|
+
};
|
|
1360
|
+
const reason = probeHarnessIncomplete(this.side, out.incomplete);
|
|
1361
|
+
if (reason) {
|
|
1362
|
+
this.incomplete.push(`probeSessions: ${reason}`);
|
|
1363
|
+
return 'incomplete';
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const reachable = out.reachable || this.calls.probe?.result?.reachable === true;
|
|
1367
|
+
this.calls.probe = {
|
|
1368
|
+
ok: true,
|
|
1369
|
+
result: {
|
|
1370
|
+
...(this.calls.probe?.result ?? {}),
|
|
1371
|
+
reachable,
|
|
1372
|
+
probed: (this.calls.probe?.result?.probed ?? 0) + (out.probed ?? 0),
|
|
1373
|
+
sessions,
|
|
1374
|
+
},
|
|
1375
|
+
};
|
|
1376
|
+
return reachable;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async observeProbeReachable(want, check) {
|
|
1380
|
+
if (want !== true && want !== false) return this.check(check);
|
|
1381
|
+
let seen = await this.probeFreshSessions();
|
|
1382
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1383
|
+
if (seen === true) return true;
|
|
1384
|
+
const met = await this.poll(
|
|
1385
|
+
async () => {
|
|
1386
|
+
if (this.clients.flight?.abandoned) return true;
|
|
1387
|
+
seen = await this.probeFreshSessions();
|
|
1388
|
+
if (seen === 'incomplete') return true;
|
|
1389
|
+
return seen === true;
|
|
1390
|
+
},
|
|
1391
|
+
observeMs,
|
|
1392
|
+
false
|
|
1393
|
+
);
|
|
1394
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1395
|
+
if (want === true && !met && !this.clients.flight?.abandoned) {
|
|
1396
|
+
if (this.side === 'control') return false;
|
|
1397
|
+
this.incomplete.push(
|
|
1398
|
+
`workerReachable: probe evidence not observed within ${this.bound(observeMs)}ms`
|
|
1399
|
+
);
|
|
1400
|
+
return 'incomplete';
|
|
1401
|
+
}
|
|
1402
|
+
return seen === true;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1044
1405
|
async watchPage(step) {
|
|
1045
1406
|
if (!this.watch) {
|
|
1046
1407
|
const start = this.reader.events.length;
|
|
@@ -1108,7 +1469,7 @@ class Execution {
|
|
|
1108
1469
|
async settle(settle) {
|
|
1109
1470
|
const refused = () => (settle.dependsOn ?? []).some((name) => this.refused[name]);
|
|
1110
1471
|
const skipped = () => {
|
|
1111
|
-
this.
|
|
1472
|
+
this.note(
|
|
1112
1473
|
`settle:skipped-refused ${settle.beacon ? `beacon ${settle.beacon}` : settle.served ? `request ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'}`
|
|
1113
1474
|
);
|
|
1114
1475
|
};
|
|
@@ -1224,6 +1585,14 @@ class Execution {
|
|
|
1224
1585
|
this.contexts.push(this.vars.context);
|
|
1225
1586
|
await this.seedContext(this.vars.context);
|
|
1226
1587
|
}
|
|
1588
|
+
if (step.reuseSession) {
|
|
1589
|
+
const reused = reusableAttachSession(step, out, params, this.client.events, this.eventStart);
|
|
1590
|
+
if (reused) {
|
|
1591
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, reused));
|
|
1592
|
+
this.note(`Target.attachToTarget reused auto-attach session for ${params.targetId}`);
|
|
1593
|
+
return { ok: true, result: { sessionId: reused, reused: true } };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1227
1596
|
return out;
|
|
1228
1597
|
}
|
|
1229
1598
|
|
|
@@ -1234,6 +1603,27 @@ class Execution {
|
|
|
1234
1603
|
});
|
|
1235
1604
|
}
|
|
1236
1605
|
|
|
1606
|
+
namedCallSummaries() {
|
|
1607
|
+
return Object.fromEntries(
|
|
1608
|
+
['attach', 'spawn', 'targets', 'probe', 'tabs']
|
|
1609
|
+
.filter((name) => this.calls[name])
|
|
1610
|
+
.map((name) => {
|
|
1611
|
+
const out = this.calls[name];
|
|
1612
|
+
return [
|
|
1613
|
+
name,
|
|
1614
|
+
{
|
|
1615
|
+
ok: out.ok,
|
|
1616
|
+
...(out.result?.sessionId ? { sessionId: true } : {}),
|
|
1617
|
+
...(out.result?.exists !== undefined ? { exists: out.result.exists } : {}),
|
|
1618
|
+
...(out.result?.reachable !== undefined ? { reachable: out.result.reachable } : {}),
|
|
1619
|
+
...(out.result?.incomplete ? { incomplete: out.result.incomplete } : {}),
|
|
1620
|
+
...(out.error ? { error: String(out.error?.message ?? out.error) } : {}),
|
|
1621
|
+
},
|
|
1622
|
+
];
|
|
1623
|
+
})
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1237
1627
|
async page(step, session, phase) {
|
|
1238
1628
|
const client = this.actor();
|
|
1239
1629
|
this.actionEventStart = client.events.length;
|
|
@@ -1345,20 +1735,30 @@ class Execution {
|
|
|
1345
1735
|
return !!late;
|
|
1346
1736
|
}
|
|
1347
1737
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
];
|
|
1738
|
+
observeEarly(name, check, want) {
|
|
1739
|
+
if (name === 'workerReachable' && this.probeStep())
|
|
1740
|
+
return this.observeProbeReachable(want, check);
|
|
1352
1741
|
if (check?.forwarded) return this.forwardedCheck(name, check, want);
|
|
1353
1742
|
if (
|
|
1354
1743
|
check?.result &&
|
|
1355
1744
|
(at(this.calls[check.result], check.path) === 'incomplete' || this.unmeasured(check))
|
|
1356
1745
|
)
|
|
1357
1746
|
return 'incomplete';
|
|
1747
|
+
return undefined;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
async observe(name, check) {
|
|
1751
|
+
const want = (this.side === 'control' ? this.vector.control.expected : this.vector.expected)?.[
|
|
1752
|
+
name
|
|
1753
|
+
];
|
|
1754
|
+
const early = this.observeEarly(name, check, want);
|
|
1755
|
+
if (early !== undefined) return early;
|
|
1358
1756
|
const plan = steps(this.vector, this.side, this.vars);
|
|
1359
1757
|
const names = resultNames(check);
|
|
1758
|
+
const absencePoll = this.vector.observe?.[name]?.absencePoll === true;
|
|
1360
1759
|
const snapshots =
|
|
1361
|
-
|
|
1760
|
+
!this.clients.flight?.abandoned &&
|
|
1761
|
+
((this.side === 'control' && want === true) || (want === false && absencePoll))
|
|
1362
1762
|
? [...plan.setup, ...plan.trigger].filter(
|
|
1363
1763
|
(step) =>
|
|
1364
1764
|
names.includes(step.as) &&
|
|
@@ -1367,17 +1767,28 @@ class Execution {
|
|
|
1367
1767
|
supported(step.requires, this.options.capabilities)
|
|
1368
1768
|
)
|
|
1369
1769
|
: [];
|
|
1770
|
+
const hasEvent = (node) =>
|
|
1771
|
+
Boolean(node?.event) ||
|
|
1772
|
+
[...(node?.all ?? []), ...(node?.any ?? []), ...(node?.not ? [node.not] : [])].some(hasEvent);
|
|
1370
1773
|
const polled =
|
|
1371
1774
|
check?.observer === 'fixture' || check?.world !== undefined || check?.script !== undefined;
|
|
1372
|
-
|
|
1775
|
+
const shouldPoll =
|
|
1776
|
+
(want === true && (polled || snapshots.length > 0)) ||
|
|
1777
|
+
(want === false && absencePoll && snapshots.length > 0);
|
|
1778
|
+
if (
|
|
1779
|
+
want === false &&
|
|
1780
|
+
!shouldPoll &&
|
|
1781
|
+
(check?.world !== undefined || check?.script !== undefined || hasEvent(check))
|
|
1782
|
+
)
|
|
1373
1783
|
return this.absenceCheck(check);
|
|
1374
|
-
if (
|
|
1784
|
+
if (!shouldPoll) return this.check(check);
|
|
1375
1785
|
let seen = false;
|
|
1376
1786
|
const met = await this.poll(
|
|
1377
1787
|
async () => {
|
|
1378
1788
|
if (this.clients.flight?.abandoned) return true;
|
|
1379
1789
|
seen = await this.check(check);
|
|
1380
1790
|
if (seen === 'unwatched' || seen === 'incomplete') return true;
|
|
1791
|
+
if (want === false && seen === true) return true;
|
|
1381
1792
|
if (seen !== true && snapshots.length) {
|
|
1382
1793
|
await this.execute(snapshots, undefined, 'observation');
|
|
1383
1794
|
if (this.clients.flight?.abandoned) return true;
|
|
@@ -1390,7 +1801,7 @@ class Execution {
|
|
|
1390
1801
|
snapshots.length > 0
|
|
1391
1802
|
);
|
|
1392
1803
|
if (seen === 'unwatched' || seen === 'incomplete') return seen;
|
|
1393
|
-
if ((!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1804
|
+
if (want === true && (!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1394
1805
|
this.incomplete.push(
|
|
1395
1806
|
`${name}: snapshot evidence not observed within ${this.bound(observeMs)}ms`
|
|
1396
1807
|
);
|
|
@@ -1675,6 +2086,7 @@ class Execution {
|
|
|
1675
2086
|
|
|
1676
2087
|
async run() {
|
|
1677
2088
|
this.requestStart = (await this.options.fixture.log()).length;
|
|
2089
|
+
this.installClientTrace();
|
|
1678
2090
|
let pumping;
|
|
1679
2091
|
try {
|
|
1680
2092
|
await this.seed();
|
|
@@ -1697,6 +2109,7 @@ class Execution {
|
|
|
1697
2109
|
side: this.side,
|
|
1698
2110
|
});
|
|
1699
2111
|
const { live, liveWhy } = await this.liveness(unseen, incomplete);
|
|
2112
|
+
if (this.errors.length) this.noteClientTrace();
|
|
1700
2113
|
return {
|
|
1701
2114
|
observed,
|
|
1702
2115
|
live,
|
|
@@ -1705,6 +2118,7 @@ class Execution {
|
|
|
1705
2118
|
notes: this.notes,
|
|
1706
2119
|
incomplete: this.incomplete,
|
|
1707
2120
|
unsupported: this.unsupported,
|
|
2121
|
+
calls: this.namedCallSummaries(),
|
|
1708
2122
|
evidence: Object.fromEntries(
|
|
1709
2123
|
Object.entries(this.calls)
|
|
1710
2124
|
.filter(
|
|
@@ -1713,13 +2127,25 @@ class Execution {
|
|
|
1713
2127
|
.map(([name, out]) => [name, out.result.diagnostics ?? out.result])
|
|
1714
2128
|
),
|
|
1715
2129
|
};
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
this.noteClientTrace();
|
|
2132
|
+
throw error;
|
|
1716
2133
|
} finally {
|
|
1717
2134
|
this.stopped = true;
|
|
1718
2135
|
await pumping;
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2136
|
+
await this.cleanupLeg();
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
async cleanupLeg() {
|
|
2141
|
+
if (this.watch)
|
|
2142
|
+
await outcome(
|
|
2143
|
+
this.reader.send('Target.detachFromTarget', { sessionId: this.watch.sessionId })
|
|
2144
|
+
);
|
|
2145
|
+
if (this.spawnWorkerKind()) {
|
|
2146
|
+
await Promise.allSettled(this.pending);
|
|
2147
|
+
await this.cleanupSpawnedWorkerLeg();
|
|
2148
|
+
} else {
|
|
1723
2149
|
for (const session of this.attachSettings.keys())
|
|
1724
2150
|
await outcome(
|
|
1725
2151
|
this.client.send(
|
|
@@ -1730,22 +2156,48 @@ class Execution {
|
|
|
1730
2156
|
);
|
|
1731
2157
|
for (const targetId of new Set(this.targets))
|
|
1732
2158
|
await outcome((this.reader ?? this.client).send('Target.closeTarget', { targetId }));
|
|
1733
|
-
for (const browserContextId of new Set(this.contexts))
|
|
1734
|
-
await outcome(
|
|
1735
|
-
(this.reader ?? this.client).send('Target.disposeBrowserContext', { browserContextId })
|
|
1736
|
-
);
|
|
1737
|
-
await Promise.allSettled(this.pending);
|
|
1738
|
-
if (
|
|
1739
|
-
this.side === 'guarded' &&
|
|
1740
|
-
this.vector.observe?.eventsRedacted?.requires?.includes('discovery')
|
|
1741
|
-
) {
|
|
1742
|
-
const violation = discoveryViolation(
|
|
1743
|
-
this.client.events.slice(this.eventStart),
|
|
1744
|
-
supported(['discovery'], this.options.capabilities)
|
|
1745
|
-
);
|
|
1746
|
-
if (violation) this.errors.push(violation);
|
|
1747
|
-
}
|
|
1748
2159
|
}
|
|
2160
|
+
for (const browserContextId of new Set(this.contexts))
|
|
2161
|
+
await outcome(
|
|
2162
|
+
(this.reader ?? this.client).send('Target.disposeBrowserContext', { browserContextId })
|
|
2163
|
+
);
|
|
2164
|
+
this.contexts = [];
|
|
2165
|
+
if (!this.spawnWorkerKind()) await Promise.allSettled(this.pending);
|
|
2166
|
+
if (
|
|
2167
|
+
this.side === 'guarded' &&
|
|
2168
|
+
this.vector.observe?.eventsRedacted?.requires?.includes('discovery')
|
|
2169
|
+
) {
|
|
2170
|
+
const violation = discoveryViolation(
|
|
2171
|
+
this.client.events.slice(this.eventStart),
|
|
2172
|
+
supported(['discovery'], this.options.capabilities)
|
|
2173
|
+
);
|
|
2174
|
+
if (violation) this.errors.push(violation);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
async cleanupSpawnedWorkerLeg() {
|
|
2179
|
+
await this.resumeWaiting();
|
|
2180
|
+
await releasePersistentWorkerSessions({
|
|
2181
|
+
client: this.client,
|
|
2182
|
+
reader: this.reader,
|
|
2183
|
+
events: this.client.events,
|
|
2184
|
+
from: this.eventStart,
|
|
2185
|
+
tag: this.vars.tag,
|
|
2186
|
+
targets: this.targets,
|
|
2187
|
+
attachSessions: [...this.attachSettings.keys()],
|
|
2188
|
+
});
|
|
2189
|
+
if (this.reader && this.reader !== this.client)
|
|
2190
|
+
await releasePersistentWorkerSessions({
|
|
2191
|
+
client: this.reader,
|
|
2192
|
+
reader: this.reader,
|
|
2193
|
+
events: this.reader.events,
|
|
2194
|
+
from: 0,
|
|
2195
|
+
tag: this.vars.tag,
|
|
2196
|
+
targets: this.targets,
|
|
2197
|
+
attachSessions: [],
|
|
2198
|
+
});
|
|
2199
|
+
this.targets = [];
|
|
2200
|
+
this.attachSettings.clear();
|
|
1749
2201
|
}
|
|
1750
2202
|
}
|
|
1751
2203
|
|
|
@@ -1764,7 +2216,7 @@ const observerRequired = (check) => {
|
|
|
1764
2216
|
const needsObserver = (vector) =>
|
|
1765
2217
|
vector.control.via === 'unguarded' ||
|
|
1766
2218
|
allSteps([...(vector.setup ?? []), ...(vector.trigger ?? [])]).some(
|
|
1767
|
-
(step) => step.isolatedWorld || step.observer
|
|
2219
|
+
(step) => step.isolatedWorld || step.observer || step.spawnWorker || step.closeProtectedShape
|
|
1768
2220
|
) ||
|
|
1769
2221
|
observerRequired(vector.liveCheck) ||
|
|
1770
2222
|
Object.values(vector.observe ?? {}).some(
|
|
@@ -1870,14 +2322,75 @@ function variables(targets, target, side, slot, index) {
|
|
|
1870
2322
|
return vars;
|
|
1871
2323
|
}
|
|
1872
2324
|
|
|
2325
|
+
function sessionTypeOf(client, sessionId) {
|
|
2326
|
+
if (!sessionId) return 'browser';
|
|
2327
|
+
const events = client.events ?? [];
|
|
2328
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2329
|
+
const event = events[i];
|
|
2330
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId === sessionId)
|
|
2331
|
+
return event.params.targetInfo?.type || 'unknown';
|
|
2332
|
+
}
|
|
2333
|
+
return 'unknown';
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function boundFrameTree(rawSend, client, params, sessionId) {
|
|
2337
|
+
let timer;
|
|
2338
|
+
const type = sessionTypeOf(client, sessionId);
|
|
2339
|
+
return Promise.race([
|
|
2340
|
+
rawSend('Page.getFrameTree', params, sessionId),
|
|
2341
|
+
new Promise((_, reject) => {
|
|
2342
|
+
timer = setTimeout(
|
|
2343
|
+
() =>
|
|
2344
|
+
reject(
|
|
2345
|
+
new Error(
|
|
2346
|
+
`Page.getFrameTree timed out on ${type} session ${sessionId ?? 'browser'} after 3000ms`
|
|
2347
|
+
)
|
|
2348
|
+
),
|
|
2349
|
+
3000
|
|
2350
|
+
);
|
|
2351
|
+
}),
|
|
2352
|
+
]).finally(() => clearTimeout(timer));
|
|
2353
|
+
}
|
|
2354
|
+
|
|
1873
2355
|
function tracked(client, calls, name) {
|
|
1874
2356
|
if (!client || typeof client.send !== 'function') return client;
|
|
1875
2357
|
let next = 0;
|
|
2358
|
+
const rawSend = client.send.bind(client);
|
|
1876
2359
|
const send = (method, params, sessionId) => {
|
|
1877
2360
|
const id = ++next;
|
|
1878
2361
|
calls.set(id, { name, method, session: sessionId, since: Date.now() });
|
|
2362
|
+
if (!client.__sliccOnSend && !client.__sliccBoundGetFrameTree) {
|
|
2363
|
+
return Promise.resolve()
|
|
2364
|
+
.then(() => client.send(method, params, sessionId))
|
|
2365
|
+
.finally(() => calls.delete(id));
|
|
2366
|
+
}
|
|
2367
|
+
const started = Date.now();
|
|
2368
|
+
const run =
|
|
2369
|
+
method === 'Page.getFrameTree' && client.__sliccBoundGetFrameTree
|
|
2370
|
+
? () => boundFrameTree(rawSend, client, params, sessionId)
|
|
2371
|
+
: () => rawSend(method, params, sessionId);
|
|
1879
2372
|
return Promise.resolve()
|
|
1880
|
-
.then(
|
|
2373
|
+
.then(run)
|
|
2374
|
+
.then(
|
|
2375
|
+
(result) => {
|
|
2376
|
+
client.__sliccOnSend?.({
|
|
2377
|
+
method,
|
|
2378
|
+
sessionId,
|
|
2379
|
+
ms: Date.now() - started,
|
|
2380
|
+
status: 'ok',
|
|
2381
|
+
});
|
|
2382
|
+
return result;
|
|
2383
|
+
},
|
|
2384
|
+
(error) => {
|
|
2385
|
+
client.__sliccOnSend?.({
|
|
2386
|
+
method,
|
|
2387
|
+
sessionId,
|
|
2388
|
+
ms: Date.now() - started,
|
|
2389
|
+
status: String(error?.message ?? error).slice(0, 200),
|
|
2390
|
+
});
|
|
2391
|
+
throw error;
|
|
2392
|
+
}
|
|
2393
|
+
)
|
|
1881
2394
|
.finally(() => calls.delete(id));
|
|
1882
2395
|
};
|
|
1883
2396
|
return new Proxy(client, {
|
|
@@ -2025,12 +2538,27 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2025
2538
|
}
|
|
2026
2539
|
if (legs.control.unsupported === 'trustedEmulation')
|
|
2027
2540
|
return skipRow(base, 'not-supported:trustedEmulation');
|
|
2541
|
+
const row = finishRow(vector, base, legs, opts);
|
|
2542
|
+
opts.log?.(row);
|
|
2543
|
+
return row;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function finishRow(vector, base, legs, opts) {
|
|
2547
|
+
const caps = Array.isArray(opts.capabilities)
|
|
2548
|
+
? opts.capabilities
|
|
2549
|
+
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true);
|
|
2550
|
+
const calls = {
|
|
2551
|
+
...(legs.control.calls && Object.keys(legs.control.calls).length
|
|
2552
|
+
? { control: legs.control.calls }
|
|
2553
|
+
: {}),
|
|
2554
|
+
...(legs.guarded.calls && Object.keys(legs.guarded.calls).length
|
|
2555
|
+
? { guarded: legs.guarded.calls }
|
|
2556
|
+
: {}),
|
|
2557
|
+
};
|
|
2028
2558
|
const row = {
|
|
2029
2559
|
...result(vector, {
|
|
2030
2560
|
...base,
|
|
2031
|
-
capabilities:
|
|
2032
|
-
? opts.capabilities
|
|
2033
|
-
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true),
|
|
2561
|
+
capabilities: caps,
|
|
2034
2562
|
control: legs.control.observed,
|
|
2035
2563
|
guarded: legs.guarded.observed,
|
|
2036
2564
|
live: legs.control.live && legs.guarded.live,
|
|
@@ -2040,6 +2568,7 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2040
2568
|
observations: { control: legs.control.observed, guarded: legs.guarded.observed },
|
|
2041
2569
|
errors: [...legs.control.errors, ...legs.guarded.errors],
|
|
2042
2570
|
designDependent: vector.designDependent ?? [],
|
|
2571
|
+
...(Object.keys(calls).length ? { calls } : {}),
|
|
2043
2572
|
...probeEvidence(legs),
|
|
2044
2573
|
};
|
|
2045
2574
|
const notes = [
|
|
@@ -2052,7 +2581,6 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2052
2581
|
row.control = 'fail';
|
|
2053
2582
|
row.guarded = 'fail';
|
|
2054
2583
|
}
|
|
2055
|
-
opts.log?.(row);
|
|
2056
2584
|
return row;
|
|
2057
2585
|
}
|
|
2058
2586
|
|