@ai-ecoverse/slicc-browsers 1.21.1 → 1.22.1
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 +7 -6
- package/fixture-server.mjs +7 -0
- package/guard-driver.mjs +18 -2
- package/guard-runner.mjs +617 -123
- package/guard-vectors.json +1773 -13
- package/package.json +6 -5
- package/protected-error-page.mjs +10 -3
- package/protected-worker.mjs +511 -0
- package/schema/guard-vectors.schema.json +88 -1
package/guard-runner.mjs
CHANGED
|
@@ -31,12 +31,48 @@ import {
|
|
|
31
31
|
} from './guard-driver.mjs';
|
|
32
32
|
import { createObserverTarget, discoveryViolation } from './observer-target.mjs';
|
|
33
33
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
34
|
+
import {
|
|
35
|
+
leftoverPersistentWorker,
|
|
36
|
+
persistentWorkerTypes,
|
|
37
|
+
probeHarnessIncomplete,
|
|
38
|
+
probeWorkerSessions,
|
|
39
|
+
protectedWorker,
|
|
40
|
+
releasePersistentWorkerSessions,
|
|
41
|
+
workerTypeForKind,
|
|
42
|
+
} from './protected-worker.mjs';
|
|
34
43
|
import { redirectSecret } from './redirect-secret.mjs';
|
|
35
44
|
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
36
45
|
|
|
46
|
+
export { leftoverPersistentWorker, releasePersistentWorkerSessions, workerTypeForKind };
|
|
47
|
+
|
|
37
48
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
49
|
const namedResultMs = 10000;
|
|
39
50
|
const observeMs = 5000;
|
|
51
|
+
|
|
52
|
+
function autoAttachSession(events, start, targetId) {
|
|
53
|
+
return events
|
|
54
|
+
.slice(start)
|
|
55
|
+
.filter(
|
|
56
|
+
(event) =>
|
|
57
|
+
event.method === 'Target.attachedToTarget' &&
|
|
58
|
+
event.params?.targetInfo?.targetId === targetId &&
|
|
59
|
+
event.params?.sessionId
|
|
60
|
+
)
|
|
61
|
+
.at(-1)?.params?.sessionId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function reusableAttachSession(step, out, params, events, start) {
|
|
65
|
+
if (!step?.reuseSession || step.cdp !== 'Target.attachToTarget' || !params?.targetId)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (out?.ok && out.result?.sessionId) return undefined;
|
|
68
|
+
const message = String(out?.error?.message ?? '');
|
|
69
|
+
if (/refused|not allowed/i.test(message)) return undefined;
|
|
70
|
+
const already =
|
|
71
|
+
(out?.ok === true && !out.result?.sessionId) ||
|
|
72
|
+
/already attached|attached already|Target is attaching or attached/i.test(message);
|
|
73
|
+
if (!already) return undefined;
|
|
74
|
+
return autoAttachSession(events, start, params.targetId);
|
|
75
|
+
}
|
|
40
76
|
const browserUiAttempts = 3;
|
|
41
77
|
const browserUiMs = 5000;
|
|
42
78
|
const q = JSON.stringify;
|
|
@@ -50,6 +86,13 @@ const supported = (requires, capabilities) =>
|
|
|
50
86
|
(requires ?? []).every((c) =>
|
|
51
87
|
Array.isArray(capabilities) ? capabilities.includes(c) : capabilities[c] === true
|
|
52
88
|
);
|
|
89
|
+
const declared = (capability, capabilities) =>
|
|
90
|
+
Array.isArray(capabilities)
|
|
91
|
+
? capabilities.includes(capability)
|
|
92
|
+
: capabilities[capability] === true;
|
|
93
|
+
const gatedOut = (observation, capabilities) =>
|
|
94
|
+
!supported(observation?.requires, capabilities) ||
|
|
95
|
+
(observation?.unless ?? []).some((capability) => declared(capability, capabilities));
|
|
53
96
|
const matches = (value, predicate) => {
|
|
54
97
|
if (predicate === null || typeof predicate !== 'object') return value === predicate;
|
|
55
98
|
return Object.entries(predicate).every(([key, want]) => {
|
|
@@ -164,12 +207,71 @@ class Execution {
|
|
|
164
207
|
this.refusalScale = options.refusalScale ?? 1;
|
|
165
208
|
this.childRuns = new Map();
|
|
166
209
|
this.named = new Map();
|
|
210
|
+
this.eventMarks = new Map();
|
|
167
211
|
this.processed = new Set();
|
|
168
212
|
this.eventStart = this.client.events.length;
|
|
169
213
|
this.actionEventStart = this.eventStart;
|
|
170
214
|
this.requestStart = undefined;
|
|
171
215
|
this.trace = [];
|
|
172
216
|
this.stopped = false;
|
|
217
|
+
this.cdpTrace = [];
|
|
218
|
+
this.cdpSessionTypes = new Map();
|
|
219
|
+
this.cdpTraceInstalled = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
note(message) {
|
|
223
|
+
if (this.notes.length >= 80) this.notes.shift();
|
|
224
|
+
Array.prototype.push.call(this.notes, message);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
installClientTrace() {
|
|
228
|
+
if (!this.spawnWorkerKind() || this.cdpTraceInstalled) return;
|
|
229
|
+
this.cdpTraceInstalled = true;
|
|
230
|
+
const prev = this.client.__sliccOnSend;
|
|
231
|
+
this.client.__sliccBoundGetFrameTree = true;
|
|
232
|
+
this.client.__sliccOnSend = (entry) => {
|
|
233
|
+
prev?.(entry);
|
|
234
|
+
this.recordCdpTrace(entry);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
recordCdpTrace(entry) {
|
|
239
|
+
let type = entry.sessionId ? this.cdpSessionTypes.get(entry.sessionId) : 'browser';
|
|
240
|
+
if (entry.sessionId && type === undefined) {
|
|
241
|
+
type = '';
|
|
242
|
+
const events = this.client.events;
|
|
243
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
244
|
+
const event = events[i];
|
|
245
|
+
if (
|
|
246
|
+
event.method === 'Target.attachedToTarget' &&
|
|
247
|
+
event.params?.sessionId === entry.sessionId
|
|
248
|
+
) {
|
|
249
|
+
type = event.params.targetInfo?.type ?? '';
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.cdpSessionTypes.set(entry.sessionId, type);
|
|
254
|
+
}
|
|
255
|
+
if (this.cdpTrace.length >= 40) this.cdpTrace.shift();
|
|
256
|
+
this.cdpTrace.push({
|
|
257
|
+
method: entry.method,
|
|
258
|
+
sessionId: entry.sessionId ?? '',
|
|
259
|
+
type: type || (entry.sessionId ? '?' : 'browser'),
|
|
260
|
+
ms: entry.ms,
|
|
261
|
+
status: String(entry.status).slice(0, 200),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
noteClientTrace() {
|
|
266
|
+
if (!this.cdpTrace.length) return;
|
|
267
|
+
this.note(
|
|
268
|
+
`client cdp: ${this.cdpTrace
|
|
269
|
+
.map(
|
|
270
|
+
(call) =>
|
|
271
|
+
`${call.method}@${call.sessionId || 'browser'}:${call.type || '?'} ${call.ms}ms ${call.status}`
|
|
272
|
+
)
|
|
273
|
+
.join(' | ')}`
|
|
274
|
+
);
|
|
173
275
|
}
|
|
174
276
|
|
|
175
277
|
actor() {
|
|
@@ -192,8 +294,7 @@ class Execution {
|
|
|
192
294
|
);
|
|
193
295
|
return read.ok && read.result.targetInfo?.attached === false;
|
|
194
296
|
}, 5000);
|
|
195
|
-
if (!released)
|
|
196
|
-
this.notes.push(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
297
|
+
if (!released) this.note(`client exit: the page stayed attached for ${this.bound(5000)}ms`);
|
|
197
298
|
const { sessionId } = await this.reader.send('Target.attachToTarget', {
|
|
198
299
|
targetId: this.pageTarget,
|
|
199
300
|
flatten: true,
|
|
@@ -400,53 +501,55 @@ class Execution {
|
|
|
400
501
|
);
|
|
401
502
|
}
|
|
402
503
|
|
|
504
|
+
async readerFrameTree(targetId, label) {
|
|
505
|
+
const attached = await outcome(
|
|
506
|
+
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
507
|
+
);
|
|
508
|
+
if (!attached.ok) return { ok: false, sessionId: undefined, frameTree: undefined };
|
|
509
|
+
const sessionId = attached.result.sessionId;
|
|
510
|
+
await outcome(this.reader.send('Runtime.runIfWaitingForDebugger', {}, sessionId));
|
|
511
|
+
const read = await outcome(this.reader.send('Page.getFrameTree', {}, sessionId));
|
|
512
|
+
if (!read.ok) {
|
|
513
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Page.getFrameTree timed out on ${label} target ${targetId} session ${sessionId}: ${read.error?.message ?? 'unknown'}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return { ok: true, sessionId, frameTree: read.result.frameTree };
|
|
519
|
+
}
|
|
520
|
+
|
|
403
521
|
async blocked(spec, observer) {
|
|
404
522
|
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
405
523
|
if (observer === 'client') return this.clientBlocked(spec, urls);
|
|
406
524
|
const targetId = spec.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
407
525
|
if (!targetId) return false;
|
|
408
|
-
const attached = await
|
|
409
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
410
|
-
);
|
|
526
|
+
const attached = await this.readerFrameTree(targetId, 'blocked page');
|
|
411
527
|
if (!attached.ok) return false;
|
|
412
528
|
try {
|
|
413
|
-
const read = await outcome(
|
|
414
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
415
|
-
);
|
|
416
529
|
const blockedFrame = (frame) =>
|
|
417
530
|
Boolean(
|
|
418
531
|
frame && urls.includes(frame.unreachableUrl) && frame.url.startsWith('chrome-error://')
|
|
419
532
|
);
|
|
420
533
|
const visit = (tree) =>
|
|
421
534
|
Boolean(tree && (blockedFrame(tree.frame) || (tree.childFrames ?? []).some(visit)));
|
|
422
|
-
if (!read.ok) return false;
|
|
423
535
|
if (spec.frame) {
|
|
424
|
-
if ((
|
|
536
|
+
if ((attached.frameTree.childFrames ?? []).some(visit)) return true;
|
|
425
537
|
return Boolean(await this.blockedFrameTarget(urls, targetId));
|
|
426
538
|
}
|
|
427
|
-
return blockedFrame(
|
|
539
|
+
return blockedFrame(attached.frameTree.frame);
|
|
428
540
|
} finally {
|
|
429
|
-
await outcome(
|
|
430
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
431
|
-
);
|
|
541
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
432
542
|
}
|
|
433
543
|
}
|
|
434
544
|
|
|
435
545
|
async frameIds(targetId) {
|
|
436
|
-
const attached = await
|
|
437
|
-
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
438
|
-
);
|
|
546
|
+
const attached = await this.readerFrameTree(targetId, 'frameIds page');
|
|
439
547
|
if (!attached.ok) return [];
|
|
440
548
|
try {
|
|
441
|
-
const read = await outcome(
|
|
442
|
-
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
443
|
-
);
|
|
444
549
|
const ids = (tree) => [tree.frame.id, ...(tree.childFrames ?? []).flatMap(ids)];
|
|
445
|
-
return
|
|
550
|
+
return ids(attached.frameTree);
|
|
446
551
|
} finally {
|
|
447
|
-
await outcome(
|
|
448
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
449
|
-
);
|
|
552
|
+
await outcome(this.reader.send('Target.detachFromTarget', { sessionId: attached.sessionId }));
|
|
450
553
|
}
|
|
451
554
|
}
|
|
452
555
|
|
|
@@ -456,21 +559,16 @@ class Execution {
|
|
|
456
559
|
if (!listed.ok) return undefined;
|
|
457
560
|
for (const info of listed.result.targetInfos) {
|
|
458
561
|
if (info.type !== 'iframe' || !parents.includes(info.parentFrameId)) continue;
|
|
459
|
-
|
|
460
|
-
this.reader.send('Target.attachToTarget', { targetId: info.targetId, flatten: true })
|
|
461
|
-
);
|
|
462
|
-
if (!attached.ok) continue;
|
|
562
|
+
let sessionId;
|
|
463
563
|
try {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const frame =
|
|
564
|
+
const attached = await this.readerFrameTree(info.targetId, `iframe ${info.type}`);
|
|
565
|
+
if (!attached.ok) continue;
|
|
566
|
+
sessionId = attached.sessionId;
|
|
567
|
+
const frame = attached.frameTree?.frame;
|
|
468
568
|
if (frame?.url.startsWith('chrome-error://') && urls.includes(frame.unreachableUrl))
|
|
469
569
|
return info.targetId;
|
|
470
570
|
} finally {
|
|
471
|
-
await outcome(
|
|
472
|
-
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
473
|
-
);
|
|
571
|
+
if (sessionId) await outcome(this.reader.send('Target.detachFromTarget', { sessionId }));
|
|
474
572
|
}
|
|
475
573
|
}
|
|
476
574
|
return undefined;
|
|
@@ -524,7 +622,7 @@ class Execution {
|
|
|
524
622
|
} catch (error) {
|
|
525
623
|
const message = String(error?.message ?? error);
|
|
526
624
|
if (!/timed? ?out/i.test(message)) throw error;
|
|
527
|
-
this.
|
|
625
|
+
this.note(`Target.createBrowserContext retried once after: ${message}`);
|
|
528
626
|
return (await client.send('Target.createBrowserContext', {})).browserContextId;
|
|
529
627
|
}
|
|
530
628
|
}
|
|
@@ -586,17 +684,38 @@ class Execution {
|
|
|
586
684
|
}
|
|
587
685
|
}
|
|
588
686
|
|
|
687
|
+
async resumeWaiting(from = this.eventStart) {
|
|
688
|
+
for (const event of this.client.events.slice(from)) {
|
|
689
|
+
if (
|
|
690
|
+
event.method === 'Target.attachedToTarget' &&
|
|
691
|
+
event.params?.waitingForDebugger &&
|
|
692
|
+
event.params?.sessionId
|
|
693
|
+
)
|
|
694
|
+
await outcome(
|
|
695
|
+
this.client.send('Runtime.runIfWaitingForDebugger', {}, event.params.sessionId)
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
589
700
|
adopt(event) {
|
|
590
701
|
const info = event.params.targetInfo;
|
|
591
702
|
const child = event.params.sessionId;
|
|
592
703
|
if (info?.type === 'browser_ui') return;
|
|
593
704
|
const paused = event.params.waitingForDebugger || this.tabSessions.has(event.sessionId);
|
|
705
|
+
const workerRow = Boolean(this.spawnWorkerKind());
|
|
594
706
|
if (this.expectWhere && !this.childWhere) {
|
|
595
707
|
this.deferred.push(event);
|
|
596
708
|
return;
|
|
597
709
|
}
|
|
598
|
-
if (
|
|
710
|
+
if (
|
|
711
|
+
info?.browserContextId &&
|
|
712
|
+
this.vars.context &&
|
|
713
|
+
info.browserContextId !== this.vars.context
|
|
714
|
+
) {
|
|
715
|
+
if (paused && workerRow)
|
|
716
|
+
this.pending.push(outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child)));
|
|
599
717
|
return;
|
|
718
|
+
}
|
|
600
719
|
const matched = !this.childWhere || this.childMatches(info);
|
|
601
720
|
const isTab = info?.type === 'tab';
|
|
602
721
|
if (!matched && !isTab) {
|
|
@@ -613,6 +732,27 @@ class Execution {
|
|
|
613
732
|
? this.childSteps
|
|
614
733
|
: this.childSteps.filter((step) => step.cdp === 'Target.setAutoAttach');
|
|
615
734
|
const started = (async () => {
|
|
735
|
+
if (workerRow) {
|
|
736
|
+
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
737
|
+
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
738
|
+
if (reload < 0) await this.execute(nested, child);
|
|
739
|
+
else {
|
|
740
|
+
await this.execute(nested.slice(0, reload), child);
|
|
741
|
+
const start = this.client.events.length;
|
|
742
|
+
await this.poll(
|
|
743
|
+
() =>
|
|
744
|
+
this.client.events
|
|
745
|
+
.slice(start)
|
|
746
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === child),
|
|
747
|
+
1500
|
|
748
|
+
);
|
|
749
|
+
await this.execute(nested.slice(reload), child);
|
|
750
|
+
}
|
|
751
|
+
const resumed = this.client.events.length;
|
|
752
|
+
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
753
|
+
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
616
756
|
const reload = nested.findIndex((step) => step.cdp === 'Page.reload');
|
|
617
757
|
if (reload < 0) await this.execute(nested, child);
|
|
618
758
|
else {
|
|
@@ -632,7 +772,13 @@ class Execution {
|
|
|
632
772
|
if (paused) await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
633
773
|
if (nested.some((step) => step.cdp === 'Page.enable'))
|
|
634
774
|
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
635
|
-
})().catch((error) =>
|
|
775
|
+
})().catch((error) =>
|
|
776
|
+
this.errors.push(
|
|
777
|
+
workerRow
|
|
778
|
+
? `adopt ${info?.type ?? 'target'} ${child}: ${String(error.message ?? error)}`
|
|
779
|
+
: String(error.message ?? error)
|
|
780
|
+
)
|
|
781
|
+
);
|
|
636
782
|
this.childRuns.set(child, started);
|
|
637
783
|
this.pending.push(started);
|
|
638
784
|
}
|
|
@@ -697,7 +843,11 @@ class Execution {
|
|
|
697
843
|
}
|
|
698
844
|
|
|
699
845
|
childMatches(info) {
|
|
700
|
-
if (this.childWhere.
|
|
846
|
+
if (this.childWhere.types !== undefined) {
|
|
847
|
+
if (!this.childWhere.types.includes(info?.type)) return false;
|
|
848
|
+
} else if (this.childWhere.type !== undefined) {
|
|
849
|
+
if (this.childWhere.type !== info?.type) return false;
|
|
850
|
+
}
|
|
701
851
|
if (this.childWhere.openerId === 'page') return info?.openerId === this.pageTarget;
|
|
702
852
|
if (this.childWhere.urlPrefix !== undefined)
|
|
703
853
|
return String(info?.url ?? '').startsWith(this.childWhere.urlPrefix);
|
|
@@ -784,7 +934,7 @@ class Execution {
|
|
|
784
934
|
Boolean(childSession) && !out.ok && this.detached(childSession)
|
|
785
935
|
);
|
|
786
936
|
if ((step.isolatedWorld || step.observer) && !out.ok)
|
|
787
|
-
this.
|
|
937
|
+
this.note(
|
|
788
938
|
`${step.isolatedWorld ? `isolatedWorld ${step.isolatedWorld}` : `observer ${step.observer}`}: ${out.error?.message ?? 'failed'}`
|
|
789
939
|
);
|
|
790
940
|
this.options.debug?.({ step, out, side: this.side });
|
|
@@ -819,7 +969,7 @@ class Execution {
|
|
|
819
969
|
return Boolean(next);
|
|
820
970
|
}, browserUiMs);
|
|
821
971
|
if (!next) return undefined;
|
|
822
|
-
this.
|
|
972
|
+
this.note(
|
|
823
973
|
`{browserUi} ${vanished} was gone when ${step.cdp} reached it; retried with ${next.targetId}`
|
|
824
974
|
);
|
|
825
975
|
this.vars.browserUi = next.targetId;
|
|
@@ -850,48 +1000,9 @@ class Execution {
|
|
|
850
1000
|
return;
|
|
851
1001
|
}
|
|
852
1002
|
if (step.client) return outcome(this.exit());
|
|
853
|
-
if (step.redirectSecret)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
return await outcome(
|
|
857
|
-
redirectSecret({
|
|
858
|
-
client: this.client,
|
|
859
|
-
reader: this.reader,
|
|
860
|
-
session,
|
|
861
|
-
targetId: this.pageTarget,
|
|
862
|
-
ordinary: this.vars.ordinary,
|
|
863
|
-
tag: this.vars.tag,
|
|
864
|
-
fixture: this.options.fixture,
|
|
865
|
-
mode: step.redirectSecret,
|
|
866
|
-
side: this.side,
|
|
867
|
-
settleScale: this.scale,
|
|
868
|
-
controls: (this.clients.redirectSecretControls ??= {}),
|
|
869
|
-
internalSessions: this.internalSessions,
|
|
870
|
-
})
|
|
871
|
-
);
|
|
872
|
-
} finally {
|
|
873
|
-
this.internalSessions.delete(session);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
if (step.errorPage)
|
|
877
|
-
return outcome(
|
|
878
|
-
protectedErrorPage({
|
|
879
|
-
client: this.client,
|
|
880
|
-
reader: this.reader,
|
|
881
|
-
session,
|
|
882
|
-
targetId: this.pageTarget,
|
|
883
|
-
ordinary: this.vars.ordinary,
|
|
884
|
-
protectedOrigin: this.vars.seven,
|
|
885
|
-
tag: this.vars.tag,
|
|
886
|
-
internalSessions: this.internalSessions,
|
|
887
|
-
mode: step.errorPage,
|
|
888
|
-
side: this.side,
|
|
889
|
-
secret: step.secret,
|
|
890
|
-
discovery: supported(['discovery'], this.options.capabilities),
|
|
891
|
-
notes: this.notes,
|
|
892
|
-
budget: this.bound(5000),
|
|
893
|
-
})
|
|
894
|
-
);
|
|
1003
|
+
if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
|
|
1004
|
+
if (step.spawnWorker) return this.spawnWorker(step);
|
|
1005
|
+
if (step.probeSessions) return this.probeSessions(step);
|
|
895
1006
|
if (step.bfcacheRestore)
|
|
896
1007
|
return outcome(
|
|
897
1008
|
bfcacheRestore({
|
|
@@ -1002,12 +1113,10 @@ class Execution {
|
|
|
1002
1113
|
|
|
1003
1114
|
async isolatedWorld(step) {
|
|
1004
1115
|
const targetId = step.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
});
|
|
1116
|
+
const attached = await this.readerFrameTree(targetId, 'isolatedWorld page');
|
|
1117
|
+
if (!attached.ok) throw new Error('isolatedWorld attach failed');
|
|
1118
|
+
const { sessionId, frameTree } = attached;
|
|
1009
1119
|
try {
|
|
1010
|
-
const { frameTree } = await this.reader.send('Page.getFrameTree', {}, sessionId);
|
|
1011
1120
|
const { executionContextId } = await this.reader.send(
|
|
1012
1121
|
'Page.createIsolatedWorld',
|
|
1013
1122
|
{ frameId: frameTree.frame.id, worldName: step.isolatedWorld },
|
|
@@ -1034,6 +1143,224 @@ class Execution {
|
|
|
1034
1143
|
}
|
|
1035
1144
|
}
|
|
1036
1145
|
|
|
1146
|
+
async originProbe(step, session) {
|
|
1147
|
+
if (step.redirectSecret) {
|
|
1148
|
+
this.internalSessions.add(session);
|
|
1149
|
+
try {
|
|
1150
|
+
return await outcome(
|
|
1151
|
+
redirectSecret({
|
|
1152
|
+
client: this.client,
|
|
1153
|
+
reader: this.reader,
|
|
1154
|
+
session,
|
|
1155
|
+
targetId: this.pageTarget,
|
|
1156
|
+
ordinary: this.vars.ordinary,
|
|
1157
|
+
tag: this.vars.tag,
|
|
1158
|
+
fixture: this.options.fixture,
|
|
1159
|
+
mode: step.redirectSecret,
|
|
1160
|
+
side: this.side,
|
|
1161
|
+
settleScale: this.scale,
|
|
1162
|
+
controls: (this.clients.redirectSecretControls ??= {}),
|
|
1163
|
+
internalSessions: this.internalSessions,
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
} finally {
|
|
1167
|
+
this.internalSessions.delete(session);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return outcome(
|
|
1171
|
+
protectedErrorPage({
|
|
1172
|
+
client: this.client,
|
|
1173
|
+
reader: this.reader,
|
|
1174
|
+
session,
|
|
1175
|
+
targetId: this.pageTarget,
|
|
1176
|
+
ordinary: this.vars.ordinary,
|
|
1177
|
+
protectedOrigin: this.vars.seven,
|
|
1178
|
+
tag: this.vars.tag,
|
|
1179
|
+
internalSessions: this.internalSessions,
|
|
1180
|
+
mode: step.errorPage,
|
|
1181
|
+
side: this.side,
|
|
1182
|
+
secret: step.secret,
|
|
1183
|
+
discovery: supported(['discovery'], this.options.capabilities),
|
|
1184
|
+
notes: this.notes,
|
|
1185
|
+
budget: this.bound(5000),
|
|
1186
|
+
})
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
spawnWorkerKind() {
|
|
1191
|
+
return allSteps([...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])]).find(
|
|
1192
|
+
(step) => step.spawnWorker
|
|
1193
|
+
)?.spawnWorker;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async ensurePriorWorkersGone(kind) {
|
|
1197
|
+
if (this.side !== 'guarded') return;
|
|
1198
|
+
const type = workerTypeForKind(kind);
|
|
1199
|
+
if (!persistentWorkerTypes.has(type)) return;
|
|
1200
|
+
const prior = this.clients.priorWorker;
|
|
1201
|
+
const leftover = await leftoverPersistentWorker(this.reader ?? this.client, {
|
|
1202
|
+
type,
|
|
1203
|
+
...(prior?.url || prior?.targetId ? { url: prior?.url, targetId: prior?.targetId } : {}),
|
|
1204
|
+
});
|
|
1205
|
+
if (leftover)
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`leftover ${type} still present before guarded spawn: ${leftover.url ?? leftover.targetId}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async spawnWorker(step) {
|
|
1212
|
+
if (step.as) this.eventMarks.set(step.as, this.client.events.length);
|
|
1213
|
+
const pageUrl = expand(step.url ?? '{target}/doc?n=host-{tag}', this.vars);
|
|
1214
|
+
const hostUrl = step.frame ? expand(step.frame, this.vars) : undefined;
|
|
1215
|
+
await this.ensurePriorWorkersGone(step.spawnWorker);
|
|
1216
|
+
const out = await outcome(
|
|
1217
|
+
protectedWorker({
|
|
1218
|
+
reader: this.reader,
|
|
1219
|
+
kind: step.spawnWorker,
|
|
1220
|
+
pageUrl,
|
|
1221
|
+
hostUrl,
|
|
1222
|
+
tag: this.vars.tag,
|
|
1223
|
+
context: this.vars.context,
|
|
1224
|
+
notes: this.notes,
|
|
1225
|
+
budget: this.bound(5000),
|
|
1226
|
+
})
|
|
1227
|
+
);
|
|
1228
|
+
if (out.ok && out.result?.browserContextId) {
|
|
1229
|
+
this.contexts.push(out.result.browserContextId);
|
|
1230
|
+
this.vars.context = out.result.browserContextId;
|
|
1231
|
+
}
|
|
1232
|
+
if (out.ok && out.result?.pageTargetId) this.targets.push(out.result.pageTargetId);
|
|
1233
|
+
if (out.ok && out.result?.workerTargetId) {
|
|
1234
|
+
this.vars.workerTarget = out.result.workerTargetId;
|
|
1235
|
+
this.targets.push(out.result.workerTargetId);
|
|
1236
|
+
}
|
|
1237
|
+
if (out.ok && out.result?.workerUrl && this.side === 'control')
|
|
1238
|
+
this.clients.priorWorker = {
|
|
1239
|
+
type: out.result.type,
|
|
1240
|
+
url: out.result.workerUrl,
|
|
1241
|
+
targetId: out.result.workerTargetId,
|
|
1242
|
+
};
|
|
1243
|
+
if (!out.ok) this.errors.push(`spawnWorker: ${out.error?.message ?? 'probe failed'}`);
|
|
1244
|
+
if (out.ok && out.result?.incomplete)
|
|
1245
|
+
this.incomplete.push(`spawnWorker: ${out.result.incomplete}`);
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
probeSessionIds(step) {
|
|
1250
|
+
const mark = this.eventMarks.get(step.probeSessions.since) ?? this.eventStart;
|
|
1251
|
+
const sessions = [];
|
|
1252
|
+
for (const event of this.client.events.slice(mark)) {
|
|
1253
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId)
|
|
1254
|
+
sessions.push(event.params.sessionId);
|
|
1255
|
+
}
|
|
1256
|
+
for (const name of step.probeSessions.attach ?? ['attach']) {
|
|
1257
|
+
const sessionId = this.calls[name]?.result?.sessionId;
|
|
1258
|
+
if (sessionId) sessions.push(sessionId);
|
|
1259
|
+
}
|
|
1260
|
+
return sessions;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async probeSessions(step) {
|
|
1264
|
+
const out = await outcome(
|
|
1265
|
+
probeWorkerSessions({
|
|
1266
|
+
client: this.client,
|
|
1267
|
+
sessions: this.probeSessionIds(step),
|
|
1268
|
+
path: expand(step.probeSessions.path, this.vars),
|
|
1269
|
+
tag: this.vars.tag,
|
|
1270
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1271
|
+
})
|
|
1272
|
+
);
|
|
1273
|
+
this.probedSessionIds ??= new Set();
|
|
1274
|
+
for (const sessionId of out.result?.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1275
|
+
if (!out.ok) {
|
|
1276
|
+
this.errors.push(`probeSessions: ${out.error?.message ?? 'probe failed'}`);
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
if (out.result?.incomplete) {
|
|
1280
|
+
const reason = probeHarnessIncomplete(this.side, out.result.incomplete);
|
|
1281
|
+
if (reason) this.incomplete.push(`probeSessions: ${reason}`);
|
|
1282
|
+
const { incomplete: _drop, ...result } = out.result;
|
|
1283
|
+
return { ...out, result };
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
probeStep() {
|
|
1289
|
+
return [...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])].find(
|
|
1290
|
+
(entry) => entry.probeSessions
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async probeFreshSessions() {
|
|
1295
|
+
const step = this.probeStep();
|
|
1296
|
+
if (!step?.probeSessions) return false;
|
|
1297
|
+
const expanded = expand(step, this.vars);
|
|
1298
|
+
this.probedSessionIds ??= new Set();
|
|
1299
|
+
const fresh = this.probeSessionIds(expanded).filter(
|
|
1300
|
+
(sessionId) => sessionId && !this.probedSessionIds.has(sessionId)
|
|
1301
|
+
);
|
|
1302
|
+
if (!fresh.length) return this.calls.probe?.result?.reachable === true;
|
|
1303
|
+
const out = await probeWorkerSessions({
|
|
1304
|
+
client: this.client,
|
|
1305
|
+
sessions: fresh,
|
|
1306
|
+
path: expand(expanded.probeSessions.path, this.vars),
|
|
1307
|
+
tag: this.vars.tag,
|
|
1308
|
+
detached: (sessionId) => this.detached(sessionId),
|
|
1309
|
+
});
|
|
1310
|
+
for (const sessionId of out.probedIds ?? []) this.probedSessionIds.add(sessionId);
|
|
1311
|
+
const sessions = [
|
|
1312
|
+
...new Set([...(this.calls.probe?.result?.sessions ?? []), ...(out.probedIds ?? fresh)]),
|
|
1313
|
+
];
|
|
1314
|
+
if (out.incomplete) {
|
|
1315
|
+
this.calls.probe = {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
result: { ...(this.calls.probe?.result ?? {}), ...out, sessions },
|
|
1318
|
+
};
|
|
1319
|
+
const reason = probeHarnessIncomplete(this.side, out.incomplete);
|
|
1320
|
+
if (reason) {
|
|
1321
|
+
this.incomplete.push(`probeSessions: ${reason}`);
|
|
1322
|
+
return 'incomplete';
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const reachable = out.reachable || this.calls.probe?.result?.reachable === true;
|
|
1326
|
+
this.calls.probe = {
|
|
1327
|
+
ok: true,
|
|
1328
|
+
result: {
|
|
1329
|
+
...(this.calls.probe?.result ?? {}),
|
|
1330
|
+
reachable,
|
|
1331
|
+
probed: (this.calls.probe?.result?.probed ?? 0) + (out.probed ?? 0),
|
|
1332
|
+
sessions,
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
return reachable;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async observeProbeReachable(want, check) {
|
|
1339
|
+
if (want !== true && want !== false) return this.check(check);
|
|
1340
|
+
let seen = await this.probeFreshSessions();
|
|
1341
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1342
|
+
if (seen === true) return true;
|
|
1343
|
+
const met = await this.poll(
|
|
1344
|
+
async () => {
|
|
1345
|
+
if (this.clients.flight?.abandoned) return true;
|
|
1346
|
+
seen = await this.probeFreshSessions();
|
|
1347
|
+
if (seen === 'incomplete') return true;
|
|
1348
|
+
return seen === true;
|
|
1349
|
+
},
|
|
1350
|
+
observeMs,
|
|
1351
|
+
false
|
|
1352
|
+
);
|
|
1353
|
+
if (seen === 'incomplete') return this.side === 'control' ? false : 'incomplete';
|
|
1354
|
+
if (want === true && !met && !this.clients.flight?.abandoned) {
|
|
1355
|
+
if (this.side === 'control') return false;
|
|
1356
|
+
this.incomplete.push(
|
|
1357
|
+
`workerReachable: probe evidence not observed within ${this.bound(observeMs)}ms`
|
|
1358
|
+
);
|
|
1359
|
+
return 'incomplete';
|
|
1360
|
+
}
|
|
1361
|
+
return seen === true;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1037
1364
|
async watchPage(step) {
|
|
1038
1365
|
if (!this.watch) {
|
|
1039
1366
|
const start = this.reader.events.length;
|
|
@@ -1101,7 +1428,7 @@ class Execution {
|
|
|
1101
1428
|
async settle(settle) {
|
|
1102
1429
|
const refused = () => (settle.dependsOn ?? []).some((name) => this.refused[name]);
|
|
1103
1430
|
const skipped = () => {
|
|
1104
|
-
this.
|
|
1431
|
+
this.note(
|
|
1105
1432
|
`settle:skipped-refused ${settle.beacon ? `beacon ${settle.beacon}` : settle.served ? `request ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'}`
|
|
1106
1433
|
);
|
|
1107
1434
|
};
|
|
@@ -1217,6 +1544,14 @@ class Execution {
|
|
|
1217
1544
|
this.contexts.push(this.vars.context);
|
|
1218
1545
|
await this.seedContext(this.vars.context);
|
|
1219
1546
|
}
|
|
1547
|
+
if (step.reuseSession) {
|
|
1548
|
+
const reused = reusableAttachSession(step, out, params, this.client.events, this.eventStart);
|
|
1549
|
+
if (reused) {
|
|
1550
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, reused));
|
|
1551
|
+
this.note(`Target.attachToTarget reused auto-attach session for ${params.targetId}`);
|
|
1552
|
+
return { ok: true, result: { sessionId: reused, reused: true } };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1220
1555
|
return out;
|
|
1221
1556
|
}
|
|
1222
1557
|
|
|
@@ -1227,6 +1562,27 @@ class Execution {
|
|
|
1227
1562
|
});
|
|
1228
1563
|
}
|
|
1229
1564
|
|
|
1565
|
+
namedCallSummaries() {
|
|
1566
|
+
return Object.fromEntries(
|
|
1567
|
+
['attach', 'spawn', 'targets', 'probe', 'tabs']
|
|
1568
|
+
.filter((name) => this.calls[name])
|
|
1569
|
+
.map((name) => {
|
|
1570
|
+
const out = this.calls[name];
|
|
1571
|
+
return [
|
|
1572
|
+
name,
|
|
1573
|
+
{
|
|
1574
|
+
ok: out.ok,
|
|
1575
|
+
...(out.result?.sessionId ? { sessionId: true } : {}),
|
|
1576
|
+
...(out.result?.exists !== undefined ? { exists: out.result.exists } : {}),
|
|
1577
|
+
...(out.result?.reachable !== undefined ? { reachable: out.result.reachable } : {}),
|
|
1578
|
+
...(out.result?.incomplete ? { incomplete: out.result.incomplete } : {}),
|
|
1579
|
+
...(out.error ? { error: String(out.error?.message ?? out.error) } : {}),
|
|
1580
|
+
},
|
|
1581
|
+
];
|
|
1582
|
+
})
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1230
1586
|
async page(step, session, phase) {
|
|
1231
1587
|
const client = this.actor();
|
|
1232
1588
|
this.actionEventStart = client.events.length;
|
|
@@ -1338,20 +1694,30 @@ class Execution {
|
|
|
1338
1694
|
return !!late;
|
|
1339
1695
|
}
|
|
1340
1696
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
];
|
|
1697
|
+
observeEarly(name, check, want) {
|
|
1698
|
+
if (name === 'workerReachable' && this.probeStep())
|
|
1699
|
+
return this.observeProbeReachable(want, check);
|
|
1345
1700
|
if (check?.forwarded) return this.forwardedCheck(name, check, want);
|
|
1346
1701
|
if (
|
|
1347
1702
|
check?.result &&
|
|
1348
1703
|
(at(this.calls[check.result], check.path) === 'incomplete' || this.unmeasured(check))
|
|
1349
1704
|
)
|
|
1350
1705
|
return 'incomplete';
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async observe(name, check) {
|
|
1710
|
+
const want = (this.side === 'control' ? this.vector.control.expected : this.vector.expected)?.[
|
|
1711
|
+
name
|
|
1712
|
+
];
|
|
1713
|
+
const early = this.observeEarly(name, check, want);
|
|
1714
|
+
if (early !== undefined) return early;
|
|
1351
1715
|
const plan = steps(this.vector, this.side, this.vars);
|
|
1352
1716
|
const names = resultNames(check);
|
|
1717
|
+
const absencePoll = this.vector.observe?.[name]?.absencePoll === true;
|
|
1353
1718
|
const snapshots =
|
|
1354
|
-
|
|
1719
|
+
!this.clients.flight?.abandoned &&
|
|
1720
|
+
((this.side === 'control' && want === true) || (want === false && absencePoll))
|
|
1355
1721
|
? [...plan.setup, ...plan.trigger].filter(
|
|
1356
1722
|
(step) =>
|
|
1357
1723
|
names.includes(step.as) &&
|
|
@@ -1360,17 +1726,28 @@ class Execution {
|
|
|
1360
1726
|
supported(step.requires, this.options.capabilities)
|
|
1361
1727
|
)
|
|
1362
1728
|
: [];
|
|
1729
|
+
const hasEvent = (node) =>
|
|
1730
|
+
Boolean(node?.event) ||
|
|
1731
|
+
[...(node?.all ?? []), ...(node?.any ?? []), ...(node?.not ? [node.not] : [])].some(hasEvent);
|
|
1363
1732
|
const polled =
|
|
1364
1733
|
check?.observer === 'fixture' || check?.world !== undefined || check?.script !== undefined;
|
|
1365
|
-
|
|
1734
|
+
const shouldPoll =
|
|
1735
|
+
(want === true && (polled || snapshots.length > 0)) ||
|
|
1736
|
+
(want === false && absencePoll && snapshots.length > 0);
|
|
1737
|
+
if (
|
|
1738
|
+
want === false &&
|
|
1739
|
+
!shouldPoll &&
|
|
1740
|
+
(check?.world !== undefined || check?.script !== undefined || hasEvent(check))
|
|
1741
|
+
)
|
|
1366
1742
|
return this.absenceCheck(check);
|
|
1367
|
-
if (
|
|
1743
|
+
if (!shouldPoll) return this.check(check);
|
|
1368
1744
|
let seen = false;
|
|
1369
1745
|
const met = await this.poll(
|
|
1370
1746
|
async () => {
|
|
1371
1747
|
if (this.clients.flight?.abandoned) return true;
|
|
1372
1748
|
seen = await this.check(check);
|
|
1373
1749
|
if (seen === 'unwatched' || seen === 'incomplete') return true;
|
|
1750
|
+
if (want === false && seen === true) return true;
|
|
1374
1751
|
if (seen !== true && snapshots.length) {
|
|
1375
1752
|
await this.execute(snapshots, undefined, 'observation');
|
|
1376
1753
|
if (this.clients.flight?.abandoned) return true;
|
|
@@ -1383,7 +1760,7 @@ class Execution {
|
|
|
1383
1760
|
snapshots.length > 0
|
|
1384
1761
|
);
|
|
1385
1762
|
if (seen === 'unwatched' || seen === 'incomplete') return seen;
|
|
1386
|
-
if ((!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1763
|
+
if (want === true && (!met || this.clients.flight?.abandoned) && snapshots.length) {
|
|
1387
1764
|
this.incomplete.push(
|
|
1388
1765
|
`${name}: snapshot evidence not observed within ${this.bound(observeMs)}ms`
|
|
1389
1766
|
);
|
|
@@ -1616,7 +1993,7 @@ class Execution {
|
|
|
1616
1993
|
const observed = { ...this.refused };
|
|
1617
1994
|
const incomplete = [];
|
|
1618
1995
|
for (const [name, observation] of Object.entries(this.vector.observe ?? {})) {
|
|
1619
|
-
if (
|
|
1996
|
+
if (gatedOut(observation, this.options.capabilities)) {
|
|
1620
1997
|
observed[name] = 'not-supported';
|
|
1621
1998
|
continue;
|
|
1622
1999
|
}
|
|
@@ -1668,6 +2045,7 @@ class Execution {
|
|
|
1668
2045
|
|
|
1669
2046
|
async run() {
|
|
1670
2047
|
this.requestStart = (await this.options.fixture.log()).length;
|
|
2048
|
+
this.installClientTrace();
|
|
1671
2049
|
let pumping;
|
|
1672
2050
|
try {
|
|
1673
2051
|
await this.seed();
|
|
@@ -1690,6 +2068,7 @@ class Execution {
|
|
|
1690
2068
|
side: this.side,
|
|
1691
2069
|
});
|
|
1692
2070
|
const { live, liveWhy } = await this.liveness(unseen, incomplete);
|
|
2071
|
+
if (this.errors.length) this.noteClientTrace();
|
|
1693
2072
|
return {
|
|
1694
2073
|
observed,
|
|
1695
2074
|
live,
|
|
@@ -1698,6 +2077,7 @@ class Execution {
|
|
|
1698
2077
|
notes: this.notes,
|
|
1699
2078
|
incomplete: this.incomplete,
|
|
1700
2079
|
unsupported: this.unsupported,
|
|
2080
|
+
calls: this.namedCallSummaries(),
|
|
1701
2081
|
evidence: Object.fromEntries(
|
|
1702
2082
|
Object.entries(this.calls)
|
|
1703
2083
|
.filter(
|
|
@@ -1706,13 +2086,25 @@ class Execution {
|
|
|
1706
2086
|
.map(([name, out]) => [name, out.result.diagnostics ?? out.result])
|
|
1707
2087
|
),
|
|
1708
2088
|
};
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
this.noteClientTrace();
|
|
2091
|
+
throw error;
|
|
1709
2092
|
} finally {
|
|
1710
2093
|
this.stopped = true;
|
|
1711
2094
|
await pumping;
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2095
|
+
await this.cleanupLeg();
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
async cleanupLeg() {
|
|
2100
|
+
if (this.watch)
|
|
2101
|
+
await outcome(
|
|
2102
|
+
this.reader.send('Target.detachFromTarget', { sessionId: this.watch.sessionId })
|
|
2103
|
+
);
|
|
2104
|
+
if (this.spawnWorkerKind()) {
|
|
2105
|
+
await Promise.allSettled(this.pending);
|
|
2106
|
+
await this.cleanupSpawnedWorkerLeg();
|
|
2107
|
+
} else {
|
|
1716
2108
|
for (const session of this.attachSettings.keys())
|
|
1717
2109
|
await outcome(
|
|
1718
2110
|
this.client.send(
|
|
@@ -1723,23 +2115,49 @@ class Execution {
|
|
|
1723
2115
|
);
|
|
1724
2116
|
for (const targetId of new Set(this.targets))
|
|
1725
2117
|
await outcome((this.reader ?? this.client).send('Target.closeTarget', { targetId }));
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
)
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
)
|
|
1739
|
-
|
|
1740
|
-
|
|
2118
|
+
}
|
|
2119
|
+
for (const browserContextId of new Set(this.contexts))
|
|
2120
|
+
await outcome(
|
|
2121
|
+
(this.reader ?? this.client).send('Target.disposeBrowserContext', { browserContextId })
|
|
2122
|
+
);
|
|
2123
|
+
this.contexts = [];
|
|
2124
|
+
if (!this.spawnWorkerKind()) await Promise.allSettled(this.pending);
|
|
2125
|
+
if (
|
|
2126
|
+
this.side === 'guarded' &&
|
|
2127
|
+
this.vector.observe?.eventsRedacted?.requires?.includes('discovery')
|
|
2128
|
+
) {
|
|
2129
|
+
const violation = discoveryViolation(
|
|
2130
|
+
this.client.events.slice(this.eventStart),
|
|
2131
|
+
supported(['discovery'], this.options.capabilities)
|
|
2132
|
+
);
|
|
2133
|
+
if (violation) this.errors.push(violation);
|
|
1741
2134
|
}
|
|
1742
2135
|
}
|
|
2136
|
+
|
|
2137
|
+
async cleanupSpawnedWorkerLeg() {
|
|
2138
|
+
await this.resumeWaiting();
|
|
2139
|
+
await releasePersistentWorkerSessions({
|
|
2140
|
+
client: this.client,
|
|
2141
|
+
reader: this.reader,
|
|
2142
|
+
events: this.client.events,
|
|
2143
|
+
from: this.eventStart,
|
|
2144
|
+
tag: this.vars.tag,
|
|
2145
|
+
targets: this.targets,
|
|
2146
|
+
attachSessions: [...this.attachSettings.keys()],
|
|
2147
|
+
});
|
|
2148
|
+
if (this.reader && this.reader !== this.client)
|
|
2149
|
+
await releasePersistentWorkerSessions({
|
|
2150
|
+
client: this.reader,
|
|
2151
|
+
reader: this.reader,
|
|
2152
|
+
events: this.reader.events,
|
|
2153
|
+
from: 0,
|
|
2154
|
+
tag: this.vars.tag,
|
|
2155
|
+
targets: this.targets,
|
|
2156
|
+
attachSessions: [],
|
|
2157
|
+
});
|
|
2158
|
+
this.targets = [];
|
|
2159
|
+
this.attachSettings.clear();
|
|
2160
|
+
}
|
|
1743
2161
|
}
|
|
1744
2162
|
|
|
1745
2163
|
const usesPre = (value) =>
|
|
@@ -1757,7 +2175,7 @@ const observerRequired = (check) => {
|
|
|
1757
2175
|
const needsObserver = (vector) =>
|
|
1758
2176
|
vector.control.via === 'unguarded' ||
|
|
1759
2177
|
allSteps([...(vector.setup ?? []), ...(vector.trigger ?? [])]).some(
|
|
1760
|
-
(step) => step.isolatedWorld || step.observer
|
|
2178
|
+
(step) => step.isolatedWorld || step.observer || step.spawnWorker
|
|
1761
2179
|
) ||
|
|
1762
2180
|
observerRequired(vector.liveCheck) ||
|
|
1763
2181
|
Object.values(vector.observe ?? {}).some(
|
|
@@ -1863,14 +2281,75 @@ function variables(targets, target, side, slot, index) {
|
|
|
1863
2281
|
return vars;
|
|
1864
2282
|
}
|
|
1865
2283
|
|
|
2284
|
+
function sessionTypeOf(client, sessionId) {
|
|
2285
|
+
if (!sessionId) return 'browser';
|
|
2286
|
+
const events = client.events ?? [];
|
|
2287
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2288
|
+
const event = events[i];
|
|
2289
|
+
if (event.method === 'Target.attachedToTarget' && event.params?.sessionId === sessionId)
|
|
2290
|
+
return event.params.targetInfo?.type || 'unknown';
|
|
2291
|
+
}
|
|
2292
|
+
return 'unknown';
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function boundFrameTree(rawSend, client, params, sessionId) {
|
|
2296
|
+
let timer;
|
|
2297
|
+
const type = sessionTypeOf(client, sessionId);
|
|
2298
|
+
return Promise.race([
|
|
2299
|
+
rawSend('Page.getFrameTree', params, sessionId),
|
|
2300
|
+
new Promise((_, reject) => {
|
|
2301
|
+
timer = setTimeout(
|
|
2302
|
+
() =>
|
|
2303
|
+
reject(
|
|
2304
|
+
new Error(
|
|
2305
|
+
`Page.getFrameTree timed out on ${type} session ${sessionId ?? 'browser'} after 3000ms`
|
|
2306
|
+
)
|
|
2307
|
+
),
|
|
2308
|
+
3000
|
|
2309
|
+
);
|
|
2310
|
+
}),
|
|
2311
|
+
]).finally(() => clearTimeout(timer));
|
|
2312
|
+
}
|
|
2313
|
+
|
|
1866
2314
|
function tracked(client, calls, name) {
|
|
1867
2315
|
if (!client || typeof client.send !== 'function') return client;
|
|
1868
2316
|
let next = 0;
|
|
2317
|
+
const rawSend = client.send.bind(client);
|
|
1869
2318
|
const send = (method, params, sessionId) => {
|
|
1870
2319
|
const id = ++next;
|
|
1871
2320
|
calls.set(id, { name, method, session: sessionId, since: Date.now() });
|
|
2321
|
+
if (!client.__sliccOnSend && !client.__sliccBoundGetFrameTree) {
|
|
2322
|
+
return Promise.resolve()
|
|
2323
|
+
.then(() => client.send(method, params, sessionId))
|
|
2324
|
+
.finally(() => calls.delete(id));
|
|
2325
|
+
}
|
|
2326
|
+
const started = Date.now();
|
|
2327
|
+
const run =
|
|
2328
|
+
method === 'Page.getFrameTree' && client.__sliccBoundGetFrameTree
|
|
2329
|
+
? () => boundFrameTree(rawSend, client, params, sessionId)
|
|
2330
|
+
: () => rawSend(method, params, sessionId);
|
|
1872
2331
|
return Promise.resolve()
|
|
1873
|
-
.then(
|
|
2332
|
+
.then(run)
|
|
2333
|
+
.then(
|
|
2334
|
+
(result) => {
|
|
2335
|
+
client.__sliccOnSend?.({
|
|
2336
|
+
method,
|
|
2337
|
+
sessionId,
|
|
2338
|
+
ms: Date.now() - started,
|
|
2339
|
+
status: 'ok',
|
|
2340
|
+
});
|
|
2341
|
+
return result;
|
|
2342
|
+
},
|
|
2343
|
+
(error) => {
|
|
2344
|
+
client.__sliccOnSend?.({
|
|
2345
|
+
method,
|
|
2346
|
+
sessionId,
|
|
2347
|
+
ms: Date.now() - started,
|
|
2348
|
+
status: String(error?.message ?? error).slice(0, 200),
|
|
2349
|
+
});
|
|
2350
|
+
throw error;
|
|
2351
|
+
}
|
|
2352
|
+
)
|
|
1874
2353
|
.finally(() => calls.delete(id));
|
|
1875
2354
|
};
|
|
1876
2355
|
return new Proxy(client, {
|
|
@@ -2018,12 +2497,27 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2018
2497
|
}
|
|
2019
2498
|
if (legs.control.unsupported === 'trustedEmulation')
|
|
2020
2499
|
return skipRow(base, 'not-supported:trustedEmulation');
|
|
2500
|
+
const row = finishRow(vector, base, legs, opts);
|
|
2501
|
+
opts.log?.(row);
|
|
2502
|
+
return row;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function finishRow(vector, base, legs, opts) {
|
|
2506
|
+
const caps = Array.isArray(opts.capabilities)
|
|
2507
|
+
? opts.capabilities
|
|
2508
|
+
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true);
|
|
2509
|
+
const calls = {
|
|
2510
|
+
...(legs.control.calls && Object.keys(legs.control.calls).length
|
|
2511
|
+
? { control: legs.control.calls }
|
|
2512
|
+
: {}),
|
|
2513
|
+
...(legs.guarded.calls && Object.keys(legs.guarded.calls).length
|
|
2514
|
+
? { guarded: legs.guarded.calls }
|
|
2515
|
+
: {}),
|
|
2516
|
+
};
|
|
2021
2517
|
const row = {
|
|
2022
2518
|
...result(vector, {
|
|
2023
2519
|
...base,
|
|
2024
|
-
capabilities:
|
|
2025
|
-
? opts.capabilities
|
|
2026
|
-
: Object.keys(opts.capabilities).filter((k) => opts.capabilities[k] === true),
|
|
2520
|
+
capabilities: caps,
|
|
2027
2521
|
control: legs.control.observed,
|
|
2028
2522
|
guarded: legs.guarded.observed,
|
|
2029
2523
|
live: legs.control.live && legs.guarded.live,
|
|
@@ -2033,6 +2527,7 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2033
2527
|
observations: { control: legs.control.observed, guarded: legs.guarded.observed },
|
|
2034
2528
|
errors: [...legs.control.errors, ...legs.guarded.errors],
|
|
2035
2529
|
designDependent: vector.designDependent ?? [],
|
|
2530
|
+
...(Object.keys(calls).length ? { calls } : {}),
|
|
2036
2531
|
...probeEvidence(legs),
|
|
2037
2532
|
};
|
|
2038
2533
|
const notes = [
|
|
@@ -2045,7 +2540,6 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
2045
2540
|
row.control = 'fail';
|
|
2046
2541
|
row.guarded = 'fail';
|
|
2047
2542
|
}
|
|
2048
|
-
opts.log?.(row);
|
|
2049
2543
|
return row;
|
|
2050
2544
|
}
|
|
2051
2545
|
|