effect-browser 0.2.0-beta.7 → 0.2.0-beta.8
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 +486 -152
- package/dist/Browser-n-bKgBNN.d.mts +372 -0
- package/dist/Browser.d.mts +3 -200
- package/dist/Browser.mjs +9 -1
- package/dist/Browser.mjs.map +1 -1
- package/dist/{BrowserData-KHk4o__c.d.mts → BrowserData-CPIIIHol.d.mts} +81 -7
- package/dist/BrowserData.d.mts +2 -2
- package/dist/BrowserData.mjs +104 -6
- package/dist/BrowserData.mjs.map +1 -1
- package/dist/{BrowserRuntime-hxkTNlRa.d.mts → BrowserRuntime-CFe9uUTv.d.mts} +16 -11
- package/dist/{BrowserRuntime-Cr76CN4I.mjs → BrowserRuntime-HJ1yYlFS.mjs} +4990 -4309
- package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +1 -0
- package/dist/BrowserRuntime.d.mts +2 -2
- package/dist/BrowserRuntime.mjs +2 -1
- package/dist/Capture.d.mts +9 -7
- package/dist/Capture.mjs +157 -32
- package/dist/Capture.mjs.map +1 -1
- package/dist/{CaptureData-Dys0Ko9d.mjs → CaptureData-CYriZHX1.mjs} +25 -6
- package/dist/CaptureData-CYriZHX1.mjs.map +1 -0
- package/dist/CaptureData.d.mts +62 -3
- package/dist/CaptureData.mjs +2 -2
- package/dist/Chromium.d.mts +2 -2
- package/dist/Chromium.mjs +19 -10
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Errors.d.mts +66 -17
- package/dist/Errors.mjs +47 -5
- package/dist/Errors.mjs.map +1 -1
- package/dist/Images-CaaPNeed.mjs +76 -0
- package/dist/Images-CaaPNeed.mjs.map +1 -0
- package/dist/OperationOptions-DAeRS6O9.mjs +55 -0
- package/dist/OperationOptions-DAeRS6O9.mjs.map +1 -0
- package/dist/PageControl.d.mts +6 -6
- package/dist/PageControl.mjs +16 -10
- package/dist/PageControl.mjs.map +1 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +164 -0
- package/dist/PageRegistry-B3q9DS3g.mjs.map +1 -0
- package/dist/Plan-D8qQ48Ic.mjs +512 -0
- package/dist/Plan-D8qQ48Ic.mjs.map +1 -0
- package/dist/Plan.d.mts +3 -0
- package/dist/Plan.mjs +2 -0
- package/dist/PlanData-B668RxbQ.d.mts +4831 -0
- package/dist/PlanData.d.mts +3 -0
- package/dist/PlanData.mjs +268 -0
- package/dist/PlanData.mjs.map +1 -0
- package/dist/PlanInput-BJcPPncs.mjs +75 -0
- package/dist/PlanInput-BJcPPncs.mjs.map +1 -0
- package/dist/PublicSession-Bs61Eal8.mjs +5601 -0
- package/dist/PublicSession-Bs61Eal8.mjs.map +1 -0
- package/dist/Testing.d.mts +48 -6
- package/dist/Testing.mjs +651 -113
- package/dist/Testing.mjs.map +1 -1
- package/dist/Timeline-fV22Xk-S.d.mts +29 -0
- package/dist/Timeline.d.mts +4 -0
- package/dist/Timeline.mjs +20 -0
- package/dist/Timeline.mjs.map +1 -0
- package/dist/TimelineData-BsOn-YGZ.d.mts +3818 -0
- package/dist/TimelineData.d.mts +3 -0
- package/dist/TimelineData.mjs +482 -0
- package/dist/TimelineData.mjs.map +1 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +5 -1
- package/package.json +17 -1
- package/dist/Association-CH3kPHXE.mjs +0 -167
- package/dist/Association-CH3kPHXE.mjs.map +0 -1
- package/dist/BrowserRuntime-Cr76CN4I.mjs.map +0 -1
- package/dist/CaptureData-Dys0Ko9d.mjs.map +0 -1
- package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
- package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
package/dist/Testing.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
|
|
2
|
-
import { BrowserPolicy, ControlFacts, FrameInfo, Identifier, ObservedControl, PageExecutionState, PageInfo, PageSuspension, SafeFilename, TargetUrl, ViewportEvidence, ViewportRect } from "./BrowserData.mjs";
|
|
3
|
-
import { a as
|
|
2
|
+
import { BrowserPolicy, ControlFacts, DocumentScroll, FrameInfo, Identifier, ObservedControl, ObservedElement, PageExecutionState, PageInfo, PageSuspension, SafeFilename, TargetUrl, ViewportEvidence, ViewportRect } from "./BrowserData.mjs";
|
|
3
|
+
import { a as stableIdentityOf, c as issueBinding, i as identityOf, n as make, o as MaximumGroupNodes, s as fromNativeAttempt } from "./BrowserRuntime-HJ1yYlFS.mjs";
|
|
4
|
+
import { l as reported, s as cleanupStep, t as checked } from "./PublicSession-Bs61Eal8.mjs";
|
|
5
|
+
import { n as pngGeometry } from "./Images-CaaPNeed.mjs";
|
|
4
6
|
import { Crypto, Effect, Layer, Option, PlatformError, Redacted, Schema } from "effect";
|
|
5
7
|
//#region src/internal/testing/Crypto.ts
|
|
6
8
|
/**
|
|
7
9
|
* A `Crypto` whose bytes count up from one, so every id and handoff token a scripted browser draws
|
|
8
|
-
* is predictable.
|
|
10
|
+
* is predictable. A namespace occupies UUID bytes untouched by its version/variant bits.
|
|
11
|
+
* It is not random and computes no digest.
|
|
9
12
|
*/
|
|
10
|
-
const makeSequentialCrypto = () => {
|
|
13
|
+
const makeSequentialCrypto = (namespace = 0) => {
|
|
11
14
|
let draws = 0;
|
|
12
15
|
return Crypto.make({
|
|
13
16
|
randomBytes: (size) => {
|
|
@@ -17,6 +20,12 @@ const makeSequentialCrypto = () => {
|
|
|
17
20
|
bytes[index] = rest % 256;
|
|
18
21
|
rest = Math.floor(rest / 256);
|
|
19
22
|
}
|
|
23
|
+
let scope = BigInt(namespace);
|
|
24
|
+
for (let index = 0; index < size - 8 && scope > 0n; index++) {
|
|
25
|
+
if (index === 6) continue;
|
|
26
|
+
bytes[index] = Number(scope % 256n);
|
|
27
|
+
scope /= 256n;
|
|
28
|
+
}
|
|
20
29
|
return bytes;
|
|
21
30
|
},
|
|
22
31
|
digest: () => Effect.fail(PlatformError.badArgument({
|
|
@@ -97,6 +106,11 @@ const DocumentScript = Schema.Struct({
|
|
|
97
106
|
url: TargetUrl,
|
|
98
107
|
title: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(512))),
|
|
99
108
|
text: Schema.String.check(Schema.isMaxLength(131072)),
|
|
109
|
+
/**
|
|
110
|
+
* Authored root-document geometry, not inferred layout. Readings carry this exact snapshot;
|
|
111
|
+
* use document.update to supply the next state after a scripted scroll or resize.
|
|
112
|
+
*/
|
|
113
|
+
documentScroll: Schema.optionalKey(DocumentScroll),
|
|
100
114
|
controls: Schema.optionalKey(Schema.Array(ControlScript).check(Schema.isMaxLength(64), uniqueBy((control) => control.id, "unique control ids")))
|
|
101
115
|
});
|
|
102
116
|
/**
|
|
@@ -155,6 +169,9 @@ const ScriptableOperation = Schema.Literals([
|
|
|
155
169
|
"read-text",
|
|
156
170
|
"screenshot",
|
|
157
171
|
"wait",
|
|
172
|
+
"resolve",
|
|
173
|
+
"run",
|
|
174
|
+
"settled",
|
|
158
175
|
"click",
|
|
159
176
|
"fill",
|
|
160
177
|
"fill-form",
|
|
@@ -173,7 +190,6 @@ const ScriptableOperation = Schema.Literals([
|
|
|
173
190
|
"describe-page",
|
|
174
191
|
"list-frames",
|
|
175
192
|
"select-page",
|
|
176
|
-
"select-frame",
|
|
177
193
|
"new-page",
|
|
178
194
|
"close-page",
|
|
179
195
|
"resize",
|
|
@@ -301,6 +317,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
301
317
|
url: document.url,
|
|
302
318
|
title: document.title ?? "",
|
|
303
319
|
text: document.text,
|
|
320
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
304
321
|
controls: (document.controls ?? []).map(controlState)
|
|
305
322
|
});
|
|
306
323
|
const documentFor = (url) => {
|
|
@@ -321,6 +338,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
321
338
|
registeredEpoch: registered ? 0 : -1,
|
|
322
339
|
values: /* @__PURE__ */ new Map(),
|
|
323
340
|
files: /* @__PURE__ */ new Map(),
|
|
341
|
+
scroll: {
|
|
342
|
+
x: 0,
|
|
343
|
+
y: 0
|
|
344
|
+
},
|
|
345
|
+
mutations: 0,
|
|
324
346
|
focused: void 0,
|
|
325
347
|
closed: false,
|
|
326
348
|
held: void 0,
|
|
@@ -329,6 +351,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
329
351
|
capture: void 0
|
|
330
352
|
};
|
|
331
353
|
pages.set(page.pageId, page);
|
|
354
|
+
for (const connection of live) connection.opened(page.pageId);
|
|
332
355
|
return page;
|
|
333
356
|
};
|
|
334
357
|
const openPages = () => [...pages.values()].filter((page) => !page.closed);
|
|
@@ -339,12 +362,17 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
339
362
|
/** The page shows a new document: nodes go stale, values reset, captures learn of it. */
|
|
340
363
|
const commit = (page, document) => {
|
|
341
364
|
page.document = document;
|
|
365
|
+
page.cachedTitle = void 0;
|
|
342
366
|
page.epoch++;
|
|
343
367
|
page.values.clear();
|
|
344
368
|
page.files.clear();
|
|
345
369
|
page.focused = void 0;
|
|
370
|
+
page.scroll.x = 0;
|
|
371
|
+
page.scroll.y = 0;
|
|
372
|
+
page.mutations++;
|
|
346
373
|
page.navigation = void 0;
|
|
347
374
|
for (const connection of live) connection.retire(page.pageId);
|
|
375
|
+
for (const connection of live) connection.navigated(page.pageId, false);
|
|
348
376
|
if (page.capture !== void 0) {
|
|
349
377
|
const { start } = page.capture;
|
|
350
378
|
if (start.document === void 0) start.invalidate("target-changed");
|
|
@@ -355,6 +383,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
355
383
|
};
|
|
356
384
|
/** In-document change: identity survives by control id, so retained nodes and waits carry on. */
|
|
357
385
|
const update = (page, document) => {
|
|
386
|
+
const previousUrl = page.document.url;
|
|
387
|
+
const before = JSON.stringify(page.document);
|
|
358
388
|
const previous = new Map(page.document.controls.map((control) => [control.script.id, control]));
|
|
359
389
|
const controls = (document.controls ?? []).map((script) => {
|
|
360
390
|
const existing = previous.get(script.id);
|
|
@@ -368,9 +398,15 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
368
398
|
url: document.url,
|
|
369
399
|
title: document.title ?? "",
|
|
370
400
|
text: document.text,
|
|
401
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
371
402
|
controls
|
|
372
403
|
};
|
|
404
|
+
if (JSON.stringify(page.document) !== before) page.mutations++;
|
|
373
405
|
if (page.focused !== void 0 && !previous.has(page.focused)) page.focused = void 0;
|
|
406
|
+
if (previousUrl !== document.url) {
|
|
407
|
+
for (const connection of live) connection.navigated(page.pageId, true);
|
|
408
|
+
page.capture?.start.document?.(document.url, true);
|
|
409
|
+
}
|
|
374
410
|
notify(page);
|
|
375
411
|
};
|
|
376
412
|
const take = (operation) => {
|
|
@@ -393,11 +429,13 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
393
429
|
throw new Error("The scripted browser refused this connection");
|
|
394
430
|
}
|
|
395
431
|
const bindings = options.bindings ?? [];
|
|
396
|
-
|
|
432
|
+
const viewport = { ...options.viewport };
|
|
397
433
|
const positions = /* @__PURE__ */ new Map();
|
|
398
434
|
let pointer = null;
|
|
399
435
|
let selectedId;
|
|
400
|
-
|
|
436
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
437
|
+
const privateSnapshots = /* @__PURE__ */ new Set();
|
|
438
|
+
const resolvedElements = /* @__PURE__ */ new WeakMap();
|
|
401
439
|
let disconnected = false;
|
|
402
440
|
let index = -1;
|
|
403
441
|
const place = (pageId, point) => {
|
|
@@ -411,8 +449,16 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
411
449
|
const dispatchObservers = /* @__PURE__ */ new WeakMap();
|
|
412
450
|
const receipt = (pageId) => ({ position: positions.get(pageId) ?? null });
|
|
413
451
|
const select = (pageId) => {
|
|
452
|
+
const previous = selectedId;
|
|
414
453
|
selectedId = pageId;
|
|
415
454
|
lastSelected = pageId;
|
|
455
|
+
for (const id of previous === pageId ? [pageId] : [previous, pageId]) {
|
|
456
|
+
const page = id === void 0 ? void 0 : pages.get(id);
|
|
457
|
+
if (page !== void 0 && !page.closed) events.pageLifecycle?.({
|
|
458
|
+
_tag: "Display",
|
|
459
|
+
page: cachedPage(page)
|
|
460
|
+
});
|
|
461
|
+
}
|
|
416
462
|
};
|
|
417
463
|
const selectedPage = (operation = "target") => {
|
|
418
464
|
const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
|
|
@@ -420,7 +466,6 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
420
466
|
return page;
|
|
421
467
|
};
|
|
422
468
|
const current = (target, operation) => {
|
|
423
|
-
if (target === void 0) return selectedPage(operation);
|
|
424
469
|
if (disconnected) throw fail(operation, Reasons.Closed.make({}));
|
|
425
470
|
const page = pages.get(target.pageId);
|
|
426
471
|
if (page === void 0 || page.closed || page.frameId !== target.frameId) throw fail(operation, Reasons.Stale.make({}));
|
|
@@ -432,21 +477,66 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
432
477
|
if (page.closed || page.targetId !== info.targetId) throw fail(operation, Reasons.Stale.make({}));
|
|
433
478
|
return page;
|
|
434
479
|
};
|
|
435
|
-
const
|
|
480
|
+
const cachedPage = (page) => Object.freeze({
|
|
436
481
|
pageId: page.pageId,
|
|
482
|
+
frameId: page.frameId,
|
|
437
483
|
targetId: page.targetId,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
484
|
+
documentEpoch: page.epoch,
|
|
485
|
+
url: page.document.url.length <= 8192 ? page.document.url : null,
|
|
486
|
+
urlQualification: page.document.url.length <= 8192 ? "NativeCached" : "Omitted",
|
|
487
|
+
title: page.cachedTitle ?? null,
|
|
488
|
+
titleQualification: page.cachedTitle === void 0 ? "Unread" : page.cachedTitle === null ? "Omitted" : "ObservedCached",
|
|
489
|
+
selected: page.pageId === selectedId,
|
|
490
|
+
displayState: options.pageControl === true ? page.held === void 0 ? "running" : "suspended" : "unknown"
|
|
441
491
|
});
|
|
492
|
+
const info = (page) => {
|
|
493
|
+
page.cachedTitle = page.document.title.length <= 512 ? page.document.title : null;
|
|
494
|
+
const value = PageInfo.make({
|
|
495
|
+
pageId: page.pageId,
|
|
496
|
+
targetId: page.targetId,
|
|
497
|
+
url: page.document.url,
|
|
498
|
+
title: page.document.title,
|
|
499
|
+
selected: page.pageId === selectedId
|
|
500
|
+
});
|
|
501
|
+
events.pageLifecycle?.({
|
|
502
|
+
_tag: "Metadata",
|
|
503
|
+
page: cachedPage(page)
|
|
504
|
+
});
|
|
505
|
+
return value;
|
|
506
|
+
};
|
|
507
|
+
const picture = (page, ticket) => {
|
|
508
|
+
const target = {
|
|
509
|
+
pageId: page.pageId,
|
|
510
|
+
frameId: page.frameId
|
|
511
|
+
};
|
|
512
|
+
const geometry = pngGeometry(PNG);
|
|
513
|
+
ticket.picture?.({
|
|
514
|
+
phase: "Requested",
|
|
515
|
+
target,
|
|
516
|
+
documentEpoch: page.epoch,
|
|
517
|
+
geometry
|
|
518
|
+
});
|
|
519
|
+
const bytes = new Uint8Array(PNG);
|
|
520
|
+
ticket.picture?.({
|
|
521
|
+
phase: "Returned",
|
|
522
|
+
target,
|
|
523
|
+
documentEpoch: page.epoch,
|
|
524
|
+
geometry,
|
|
525
|
+
byteLength: bytes.length
|
|
526
|
+
});
|
|
527
|
+
return bytes;
|
|
528
|
+
};
|
|
442
529
|
const frameInfo = (page) => FrameInfo.make({
|
|
443
530
|
frameId: page.frameId,
|
|
444
531
|
parentFrameId: null,
|
|
445
532
|
url: page.document.url,
|
|
446
533
|
name: ""
|
|
447
534
|
});
|
|
448
|
-
const invalidateSnapshot = (scope = "all") => {
|
|
449
|
-
|
|
535
|
+
const invalidateSnapshot = (scope = "all", origin) => {
|
|
536
|
+
for (const snapshot of [...snapshots.values(), ...privateSnapshots]) if (scope !== "none" && (scope === "all" || scope.pageId === snapshot.pageId && (scope.frameId === void 0 || scope.frameId === snapshot.frameId))) {
|
|
537
|
+
if (privateSnapshots.has(snapshot) && origin !== void 0 && snapshot.origin === origin) continue;
|
|
538
|
+
snapshot.validity = "invalid";
|
|
539
|
+
}
|
|
450
540
|
};
|
|
451
541
|
/** The page closes as a browser closes it: every connection learns, and nothing on it lasts. */
|
|
452
542
|
const close = (target) => {
|
|
@@ -456,11 +546,12 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
456
546
|
target.capture = void 0;
|
|
457
547
|
notify(target);
|
|
458
548
|
if (selectedId === target.pageId) select(void 0);
|
|
459
|
-
|
|
549
|
+
snapshots.delete(target.frameId);
|
|
550
|
+
events.pageClosed?.(target.pageId, cachedPage(target));
|
|
460
551
|
events.invalidate("target-changed", { pageId: target.pageId });
|
|
461
552
|
for (const other of live) {
|
|
462
553
|
if (other === connection) continue;
|
|
463
|
-
other.
|
|
554
|
+
other.closed(target.pageId);
|
|
464
555
|
other.announce(target.pageId);
|
|
465
556
|
}
|
|
466
557
|
};
|
|
@@ -468,6 +559,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
468
559
|
const dropConnection = (announce, ended = "dropped") => {
|
|
469
560
|
if (disconnected) return;
|
|
470
561
|
disconnected = true;
|
|
562
|
+
invalidateSnapshot();
|
|
563
|
+
snapshots.clear();
|
|
564
|
+
privateSnapshots.clear();
|
|
471
565
|
connections[index] = ended;
|
|
472
566
|
live.delete(connection);
|
|
473
567
|
for (const page of pages.values()) {
|
|
@@ -477,6 +571,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
477
571
|
notify(page);
|
|
478
572
|
}
|
|
479
573
|
if (announce) events.disconnected();
|
|
574
|
+
events.retired?.();
|
|
480
575
|
};
|
|
481
576
|
const dispatch = (ticket, record) => {
|
|
482
577
|
if (record.dispatched) return;
|
|
@@ -549,13 +644,14 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
549
644
|
if (ticket !== void 0) await applyArmed(operation, ticket, record);
|
|
550
645
|
const value = await body(record);
|
|
551
646
|
record.settled = "completed";
|
|
647
|
+
if (ticket !== void 0 && isTicket(ticket)) ticket.acknowledge?.();
|
|
552
648
|
return value;
|
|
553
649
|
} catch (error) {
|
|
554
650
|
record.settled = "failed";
|
|
555
651
|
throw error;
|
|
556
652
|
}
|
|
557
653
|
};
|
|
558
|
-
const meta = (target) => target === void 0 ? {} : typeof target === "string" ? { selector: target } : { elementId: target.elementId };
|
|
654
|
+
const meta = (target) => target === void 0 ? {} : typeof target === "string" ? { selector: target } : { elementId: "_tag" in target ? resolvedElements.get(target)?.node.script.id : target.elementId };
|
|
559
655
|
const facts = (control) => {
|
|
560
656
|
const { script: value } = control;
|
|
561
657
|
const extra = value.facts ?? {};
|
|
@@ -570,7 +666,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
570
666
|
editable: extra.editable ?? isEditable(value),
|
|
571
667
|
...value.inputType === void 0 ? {} : { inputType: value.inputType },
|
|
572
668
|
...extra.autocomplete === void 0 ? {} : { autocomplete: extra.autocomplete },
|
|
573
|
-
...value.destination === void 0 ? {} : { destination: value.destination },
|
|
669
|
+
...value.destination === void 0 || value.destination.length > 2048 ? {} : { destination: value.destination },
|
|
574
670
|
...extra.formMethod === void 0 ? {} : { formMethod: extra.formMethod },
|
|
575
671
|
box: ViewportRect.make(extra.box ?? {
|
|
576
672
|
x: 0,
|
|
@@ -605,33 +701,62 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
605
701
|
return control;
|
|
606
702
|
};
|
|
607
703
|
/** A retained node is only as current as the observation that produced it. */
|
|
608
|
-
const retained = (target, ticket, operation, allowSuspended = false, attached = true) => {
|
|
704
|
+
const retained = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => {
|
|
609
705
|
ticket.check();
|
|
610
706
|
const stale = () => fail(operation, Reasons.Stale.make({}));
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
707
|
+
const page = current(browserTarget, operation);
|
|
708
|
+
const snapshot = snapshots.get(page.frameId);
|
|
709
|
+
if (snapshot === void 0 || snapshot.pageId !== page.pageId || snapshot.id !== target.observationId) throw stale();
|
|
710
|
+
const node = snapshot.nodes.get(target.elementId);
|
|
614
711
|
if (node === void 0) throw stale();
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
if (selectedId !== current.pageId) throw stale();
|
|
619
|
-
if (current.validity === "suspended" && !allowSuspended && !current.revalidated.has(target.elementId)) throw stale();
|
|
712
|
+
if (snapshot.validity === "invalid" || snapshot.generation !== ticket.generation) throw stale();
|
|
713
|
+
if (page.epoch !== snapshot.epoch) throw stale();
|
|
714
|
+
if (snapshot.validity === "suspended" && !allowSuspended && !snapshot.revalidated.has(target.elementId)) throw stale();
|
|
620
715
|
if (attached && !page.document.controls.includes(node)) throw stale();
|
|
621
716
|
return {
|
|
622
717
|
node,
|
|
623
718
|
page,
|
|
624
|
-
snapshot
|
|
719
|
+
snapshot
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
const privateRetained = (target, ticket, operation, browserTarget, attached = true) => {
|
|
723
|
+
ticket.check();
|
|
724
|
+
const record = resolvedElements.get(target);
|
|
725
|
+
const page = current(browserTarget, operation);
|
|
726
|
+
if (record === void 0 || record.snapshot.validity !== "valid" || !privateSnapshots.has(record.snapshot) || record.snapshot.generation !== ticket.generation || record.snapshot.pageId !== page.pageId || record.snapshot.frameId !== page.frameId || record.snapshot.epoch !== page.epoch || attached && !page.document.controls.includes(record.node) || record.parent !== void 0 && (!page.document.controls.includes(record.parent) || record.node.script.selectElementId !== record.parent.script.id)) throw fail(operation, Reasons.Stale.make({}));
|
|
727
|
+
return {
|
|
728
|
+
...record,
|
|
729
|
+
page
|
|
625
730
|
};
|
|
626
731
|
};
|
|
732
|
+
const retainedElement = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => "_tag" in target ? privateRetained(target, ticket, operation, browserTarget, attached) : retained(target, ticket, operation, allowSuspended, attached, browserTarget);
|
|
733
|
+
const sample = (node, fresh, scope, descriptor) => ({
|
|
734
|
+
facts: fresh,
|
|
735
|
+
scope,
|
|
736
|
+
frameComplete: true,
|
|
737
|
+
completeness: {
|
|
738
|
+
label: "complete",
|
|
739
|
+
inputType: node.script.inputType === void 0 ? "absent" : "complete",
|
|
740
|
+
autocomplete: node.script.facts?.autocomplete === void 0 ? "absent" : "complete",
|
|
741
|
+
destination: node.script.destination === void 0 ? "absent" : node.script.destination.length > 2048 ? "omitted" : "complete",
|
|
742
|
+
formMethod: node.script.facts?.formMethod === void 0 ? "absent" : "complete"
|
|
743
|
+
},
|
|
744
|
+
...descriptor?.ordinal === void 0 ? {} : { ordinal: descriptor.ordinal }
|
|
745
|
+
});
|
|
627
746
|
const resolve = (target, ticket, operation, policy, browserTarget, allowSuspended = false, enablement = false) => {
|
|
628
747
|
let node;
|
|
629
748
|
let page;
|
|
630
749
|
if (typeof target === "string") {
|
|
631
750
|
page = current(browserTarget, operation);
|
|
632
751
|
node = bySelector(page, target, operation);
|
|
633
|
-
} else ({node, page} =
|
|
752
|
+
} else ({node, page} = retainedElement(target, ticket, operation, allowSuspended, true, browserTarget));
|
|
634
753
|
const fresh = facts(node);
|
|
754
|
+
const captured = ControlFacts.make({
|
|
755
|
+
...fresh,
|
|
756
|
+
box: ViewportRect.make({ ...fresh.box })
|
|
757
|
+
});
|
|
758
|
+
const privateTarget = typeof target !== "string" && "_tag" in target ? resolvedElements.get(target) : void 0;
|
|
759
|
+
if (privateTarget !== void 0 && (enablement ? stableIdentityOf(fresh) !== privateTarget.stable : identityOf(fresh) !== privateTarget.identity)) throw fail(operation, Reasons.Stale.make({}));
|
|
635
760
|
if (enablement && node.script.disabled === true) throw fail(operation, Reasons.Disabled.make({}));
|
|
636
761
|
if (policy !== void 0) {
|
|
637
762
|
let admitted = false;
|
|
@@ -643,17 +768,189 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
643
768
|
if (!admitted) throw fail(operation, Reasons.Denied.make({}));
|
|
644
769
|
}
|
|
645
770
|
ticket.check();
|
|
771
|
+
if (isTicket(ticket)) {
|
|
772
|
+
const scope = privateTarget?.scope ?? (typeof target === "string" ? "document" : snapshots.get(page.frameId)?.scope ?? "document");
|
|
773
|
+
ticket.captureTarget?.(target, sample(node, captured, scope, privateTarget?.descriptor));
|
|
774
|
+
}
|
|
646
775
|
return {
|
|
647
776
|
node,
|
|
648
777
|
page,
|
|
649
778
|
facts: fresh
|
|
650
779
|
};
|
|
651
780
|
};
|
|
781
|
+
/** The complete scripted document, never a previously clipped observation, proves cardinality. */
|
|
782
|
+
const descriptorNode = (descriptor, page, operation, contextual, parent) => {
|
|
783
|
+
if (descriptor.frame !== void 0) throw fail(operation, Reasons.Unsupported.make({}));
|
|
784
|
+
const matches = page.document.controls.filter((node) => {
|
|
785
|
+
const fresh = facts(node);
|
|
786
|
+
const identity = descriptor.identity;
|
|
787
|
+
return fresh.kind === descriptor.kind && fresh.label === descriptor.label && (parent === void 0 || parent.script.kind === "select" && node.script.selectElementId === parent.script.id) && (descriptor.destination === void 0 || fresh.destination === descriptor.destination) && (identity?.inputType === void 0 || fresh.inputType === identity.inputType) && (identity?.autocomplete === void 0 || fresh.autocomplete === identity.autocomplete) && (identity?.formMethod === void 0 || fresh.formMethod === identity.formMethod);
|
|
788
|
+
});
|
|
789
|
+
const viewportMatches = matches.filter((node) => facts(node).hitTest === "self");
|
|
790
|
+
if (descriptor.matchScope === "viewport") {
|
|
791
|
+
if (matches.some((node) => {
|
|
792
|
+
const fresh = facts(node);
|
|
793
|
+
return fresh.hitTest === "uncertain" && fresh.placement !== "outside";
|
|
794
|
+
})) throw fail(operation, Reasons.Incomplete.make({}));
|
|
795
|
+
if (!contextual && matches.length > 1) throw fail(operation, Reasons.Ambiguous.make({ count: matches.length }));
|
|
796
|
+
}
|
|
797
|
+
const candidates = descriptor.matchScope === "viewport" ? viewportMatches : matches;
|
|
798
|
+
if (candidates.length === 0) throw fail(operation, Reasons.Missing.make({}));
|
|
799
|
+
if (descriptor.ordinal !== void 0 && descriptor.ordinal.of !== candidates.length) throw fail(operation, Reasons.Incomplete.make({}));
|
|
800
|
+
if (descriptor.ordinal === void 0 && candidates.length !== 1) throw fail(operation, Reasons.Ambiguous.make({ count: candidates.length }));
|
|
801
|
+
const picked = candidates[descriptor.ordinal?.index ?? 0];
|
|
802
|
+
if (picked === void 0) throw fail(operation, Reasons.Incomplete.make({}));
|
|
803
|
+
return picked;
|
|
804
|
+
};
|
|
805
|
+
const expectations = (conditions, ticket, browserTarget) => {
|
|
806
|
+
ticket.check();
|
|
807
|
+
const page = current(browserTarget, "run");
|
|
808
|
+
const size = page.viewport ?? viewport;
|
|
809
|
+
if (conditions.length > 16) throw fail("run", Reasons.Limit.make({
|
|
810
|
+
dimension: "controls",
|
|
811
|
+
maximum: 16,
|
|
812
|
+
observed: conditions.length
|
|
813
|
+
}));
|
|
814
|
+
for (const condition of conditions) {
|
|
815
|
+
ticket.check();
|
|
816
|
+
let matches;
|
|
817
|
+
switch (condition._tag) {
|
|
818
|
+
case "Origin":
|
|
819
|
+
matches = originOf(page.document.url) === condition.value;
|
|
820
|
+
break;
|
|
821
|
+
case "Path":
|
|
822
|
+
matches = new URL(page.document.url).pathname === condition.value;
|
|
823
|
+
break;
|
|
824
|
+
case "Viewport":
|
|
825
|
+
matches = size.width === condition.width && size.height === condition.height;
|
|
826
|
+
break;
|
|
827
|
+
case "Scroll":
|
|
828
|
+
matches = page.scroll.x === condition.x && page.scroll.y === condition.y;
|
|
829
|
+
break;
|
|
830
|
+
case "Text":
|
|
831
|
+
if (condition.scope === "viewport") throw fail("run", Reasons.Unsupported.make({}));
|
|
832
|
+
matches = condition.match === "equals" ? page.document.text === condition.value : page.document.text.includes(condition.value);
|
|
833
|
+
break;
|
|
834
|
+
case "Geometry": {
|
|
835
|
+
const node = descriptorNode(condition.target, page, "run", false);
|
|
836
|
+
const box = facts(node).box;
|
|
837
|
+
matches = box.x === condition.box.x && box.y === condition.box.y && box.width === condition.box.width && box.height === condition.box.height;
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (!matches) throw fail("run", Reasons.Denied.make({}));
|
|
842
|
+
}
|
|
843
|
+
ticket.check();
|
|
844
|
+
};
|
|
845
|
+
const resolveGroup = (requests, ticket, browserTarget, guard) => {
|
|
846
|
+
ticket.check();
|
|
847
|
+
const page = current(browserTarget, "resolve");
|
|
848
|
+
if (requests.length === 0 || requests.length > 2081) throw fail("resolve", Reasons.Limit.make({
|
|
849
|
+
dimension: "controls",
|
|
850
|
+
maximum: MaximumGroupNodes,
|
|
851
|
+
observed: requests.length
|
|
852
|
+
}));
|
|
853
|
+
if (guard._tag === "ViewportContext") expectations(guard.before, ticket, browserTarget);
|
|
854
|
+
const retainedSnapshots = [...snapshots.values(), ...privateSnapshots];
|
|
855
|
+
const perPage = retainedSnapshots.filter((snapshot) => snapshot.pageId === page.pageId).length;
|
|
856
|
+
const pageMaximum = options.observationLimits?.maxSnapshotsPerPage ?? 16;
|
|
857
|
+
const sessionMaximum = options.observationLimits?.maxSnapshotsPerSession ?? 64;
|
|
858
|
+
if (perPage >= pageMaximum || retainedSnapshots.length >= sessionMaximum) throw fail("resolve", Reasons.Limit.make({
|
|
859
|
+
dimension: "observation-snapshots",
|
|
860
|
+
maximum: perPage >= pageMaximum ? pageMaximum : sessionMaximum,
|
|
861
|
+
observed: perPage >= pageMaximum ? perPage + 1 : retainedSnapshots.length + 1
|
|
862
|
+
}));
|
|
863
|
+
const snapshot = {
|
|
864
|
+
id: `observation-${++observationSerial}`,
|
|
865
|
+
pageId: page.pageId,
|
|
866
|
+
frameId: page.frameId,
|
|
867
|
+
epoch: page.epoch,
|
|
868
|
+
generation: ticket.generation,
|
|
869
|
+
scope: "document",
|
|
870
|
+
validity: "valid",
|
|
871
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
872
|
+
identities: /* @__PURE__ */ new Map(),
|
|
873
|
+
revalidated: /* @__PURE__ */ new Set()
|
|
874
|
+
};
|
|
875
|
+
const elements = [];
|
|
876
|
+
const records = [];
|
|
877
|
+
const samples = [];
|
|
878
|
+
privateSnapshots.add(snapshot);
|
|
879
|
+
const release = async () => {
|
|
880
|
+
snapshot.validity = "invalid";
|
|
881
|
+
privateSnapshots.delete(snapshot);
|
|
882
|
+
};
|
|
883
|
+
try {
|
|
884
|
+
for (const [index, request] of requests.entries()) {
|
|
885
|
+
ticket.check();
|
|
886
|
+
if (request.parent !== void 0 && (!Number.isInteger(request.parent) || request.parent < 0 || request.parent >= index)) throw fail("resolve", Reasons.Malformed.make({}));
|
|
887
|
+
const parent = request.parent === void 0 ? void 0 : records[request.parent]?.node;
|
|
888
|
+
let node;
|
|
889
|
+
let scope;
|
|
890
|
+
let identity;
|
|
891
|
+
let stable;
|
|
892
|
+
if (request.target._tag === "Ref") {
|
|
893
|
+
const source = retained(request.target.reference, ticket, "resolve", false, true, browserTarget);
|
|
894
|
+
const captured = source.snapshot.identities.get(request.target.reference.elementId);
|
|
895
|
+
if (captured === void 0) throw fail("resolve", Reasons.Incomplete.make({}));
|
|
896
|
+
node = source.node;
|
|
897
|
+
scope = source.snapshot.scope;
|
|
898
|
+
({identity, stable} = captured);
|
|
899
|
+
} else {
|
|
900
|
+
node = descriptorNode(request.target.descriptor, page, "resolve", guard._tag === "ViewportContext", parent);
|
|
901
|
+
scope = request.target.descriptor.matchScope;
|
|
902
|
+
const fresh = facts(node);
|
|
903
|
+
identity = identityOf(fresh);
|
|
904
|
+
stable = stableIdentityOf(fresh);
|
|
905
|
+
}
|
|
906
|
+
if (parent !== void 0 && (parent.script.kind !== "select" || node.script.selectElementId !== parent.script.id)) throw fail("resolve", Reasons.Stale.make({}));
|
|
907
|
+
const element = Object.freeze({
|
|
908
|
+
_tag: "ResolvedElement",
|
|
909
|
+
target: Object.freeze({
|
|
910
|
+
pageId: page.pageId,
|
|
911
|
+
frameId: page.frameId
|
|
912
|
+
})
|
|
913
|
+
});
|
|
914
|
+
const record = {
|
|
915
|
+
node,
|
|
916
|
+
snapshot,
|
|
917
|
+
scope,
|
|
918
|
+
identity,
|
|
919
|
+
stable,
|
|
920
|
+
...parent === void 0 ? {} : { parent },
|
|
921
|
+
...request.target._tag === "Descriptor" ? { descriptor: request.target.descriptor } : {}
|
|
922
|
+
};
|
|
923
|
+
snapshot.nodes.set(node.script.id, node);
|
|
924
|
+
snapshot.identities.set(node.script.id, {
|
|
925
|
+
identity,
|
|
926
|
+
stable
|
|
927
|
+
});
|
|
928
|
+
resolvedElements.set(element, record);
|
|
929
|
+
elements.push(element);
|
|
930
|
+
records.push(record);
|
|
931
|
+
samples.push(request.target._tag === "Descriptor" || request.captureInitial === true && ticket.captureTarget !== void 0 ? sample(node, facts(node), scope, record.descriptor) : void 0);
|
|
932
|
+
}
|
|
933
|
+
ticket.check();
|
|
934
|
+
return {
|
|
935
|
+
elements: Object.freeze(elements),
|
|
936
|
+
samples: Object.freeze(samples),
|
|
937
|
+
activate: (next) => {
|
|
938
|
+
for (const element of elements) privateRetained(element, next, "run", browserTarget);
|
|
939
|
+
snapshot.origin = next.signal;
|
|
940
|
+
},
|
|
941
|
+
release
|
|
942
|
+
};
|
|
943
|
+
} catch (error) {
|
|
944
|
+
snapshot.validity = "invalid";
|
|
945
|
+
privateSnapshots.delete(snapshot);
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
652
949
|
const requireRunning = (page, operation) => {
|
|
653
950
|
if (page.held !== void 0) throw fail(operation, Reasons.Busy.make({}));
|
|
654
951
|
};
|
|
655
952
|
const heldObservation = (pageId) => {
|
|
656
|
-
if (snapshot
|
|
953
|
+
for (const snapshot of snapshots.values()) if (snapshot.pageId === pageId && snapshot.validity === "valid") {
|
|
657
954
|
snapshot.validity = "suspended";
|
|
658
955
|
snapshot.revalidated.clear();
|
|
659
956
|
}
|
|
@@ -678,13 +975,29 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
678
975
|
if (focused !== void 0 && isEditable(focused.script)) page.values.set(focused.script.id, (page.values.get(focused.script.id) ?? "") + text);
|
|
679
976
|
};
|
|
680
977
|
/** Issued options of one select, refused exactly where native selection refuses them. */
|
|
681
|
-
const chosen = (node, ids, operation) => {
|
|
682
|
-
const current = snapshot;
|
|
683
|
-
if (current === void 0) throw fail(operation, Reasons.Stale.make({}));
|
|
978
|
+
const chosen = (page, node, ids, ticket, operation, enablement = false) => {
|
|
684
979
|
if (!isSelectable(node.script.kind)) throw fail(operation, Reasons.Unsupported.make({}));
|
|
685
980
|
const options = ids.map((id) => {
|
|
686
|
-
|
|
687
|
-
if (
|
|
981
|
+
let option;
|
|
982
|
+
if (typeof id === "string") {
|
|
983
|
+
const snapshot = snapshots.get(page.frameId);
|
|
984
|
+
const kept = snapshot?.nodes.get(id);
|
|
985
|
+
if (snapshot === void 0 || kept === void 0 || snapshot.nodes.get(node.script.id) !== node) throw fail(operation, Reasons.Stale.make({}));
|
|
986
|
+
option = kept;
|
|
987
|
+
} else {
|
|
988
|
+
const kept = privateRetained(id, ticket, operation, {
|
|
989
|
+
pageId: page.pageId,
|
|
990
|
+
frameId: page.frameId
|
|
991
|
+
});
|
|
992
|
+
const fresh = facts(kept.node);
|
|
993
|
+
if (kept.parent !== node || (enablement ? stableIdentityOf(fresh) !== kept.stable : identityOf(fresh) !== kept.identity)) throw fail(operation, Reasons.Stale.make({}));
|
|
994
|
+
option = kept.node;
|
|
995
|
+
}
|
|
996
|
+
if (option.script.selectElementId !== node.script.id || !page.document.controls.includes(option)) throw fail(operation, Reasons.Stale.make({}));
|
|
997
|
+
ticket.captureTarget?.(typeof id === "string" ? ObservedElement.make({
|
|
998
|
+
observationId: snapshots.get(page.frameId)?.id ?? "",
|
|
999
|
+
elementId: id
|
|
1000
|
+
}) : id, sample(option, facts(option), typeof id === "string" ? snapshots.get(page.frameId)?.scope ?? "document" : resolvedElements.get(id)?.scope ?? "document", typeof id === "string" ? void 0 : resolvedElements.get(id)?.descriptor));
|
|
688
1001
|
return option;
|
|
689
1002
|
});
|
|
690
1003
|
if (node.script.disabled === true || options.some((option) => option.script.disabled === true)) throw fail(operation, Reasons.Disabled.make({}));
|
|
@@ -712,7 +1025,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
712
1025
|
let act;
|
|
713
1026
|
let input;
|
|
714
1027
|
if (field.options !== void 0) {
|
|
715
|
-
const options = chosen(node, field.options, "fill-form");
|
|
1028
|
+
const options = chosen(page, node, field.options, ticket, "fill-form", true);
|
|
716
1029
|
act = () => choose(page, node, options);
|
|
717
1030
|
} else if (field.checked !== void 0) {
|
|
718
1031
|
if (node.checked === void 0) throw fail("fill-form", Reasons.Unsupported.make({}));
|
|
@@ -818,6 +1131,98 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
818
1131
|
page.watchers.add(check);
|
|
819
1132
|
check();
|
|
820
1133
|
});
|
|
1134
|
+
/** Quiet in the finite scripted source; the actual Chromium observer owns native root metrics. */
|
|
1135
|
+
const waitSettled = (options, ticket, page, record) => new Promise((resolve, reject) => {
|
|
1136
|
+
const epoch = page.epoch;
|
|
1137
|
+
const started = ticket.deadline - ticket.remainingMillis();
|
|
1138
|
+
const elapsed = () => Math.max(0, ticket.deadline - ticket.remainingMillis() - started);
|
|
1139
|
+
const geometry = () => JSON.stringify(page.document.controls.map((node) => facts(node).box));
|
|
1140
|
+
const size = () => page.viewport ?? viewport;
|
|
1141
|
+
let previousGeometry = geometry();
|
|
1142
|
+
let previousViewport = JSON.stringify(size());
|
|
1143
|
+
let previousScroll = JSON.stringify(page.scroll);
|
|
1144
|
+
let previousMutations = page.mutations;
|
|
1145
|
+
let lastChanged = 0;
|
|
1146
|
+
let samples = 0;
|
|
1147
|
+
let mutations = 0;
|
|
1148
|
+
let scrollChanges = 0;
|
|
1149
|
+
let geometryChanges = 0;
|
|
1150
|
+
let viewportChanges = 0;
|
|
1151
|
+
let finished = false;
|
|
1152
|
+
let timer;
|
|
1153
|
+
const cleanup = () => {
|
|
1154
|
+
timer?.cancel();
|
|
1155
|
+
timer = void 0;
|
|
1156
|
+
page.watchers.delete(check);
|
|
1157
|
+
ticket.signal.removeEventListener("abort", onAbort);
|
|
1158
|
+
};
|
|
1159
|
+
const failWait = (error) => {
|
|
1160
|
+
if (finished) return;
|
|
1161
|
+
finished = true;
|
|
1162
|
+
cleanup();
|
|
1163
|
+
record.settled = "failed";
|
|
1164
|
+
reject(error);
|
|
1165
|
+
};
|
|
1166
|
+
const onAbort = () => failWait(aborted(ticket, "settled"));
|
|
1167
|
+
const check = () => {
|
|
1168
|
+
if (finished) return;
|
|
1169
|
+
try {
|
|
1170
|
+
ticket.check();
|
|
1171
|
+
if (disconnected) throw fail("settled", Reasons.Disconnected.make({}));
|
|
1172
|
+
if (page.closed || page.epoch !== epoch) throw fail("settled", Reasons.Stale.make({}));
|
|
1173
|
+
const now = elapsed();
|
|
1174
|
+
const nextGeometry = geometry();
|
|
1175
|
+
const nextViewport = JSON.stringify(size());
|
|
1176
|
+
const nextScroll = JSON.stringify(page.scroll);
|
|
1177
|
+
const changed = page.mutations !== previousMutations || nextGeometry !== previousGeometry || nextViewport !== previousViewport || nextScroll !== previousScroll;
|
|
1178
|
+
samples = Math.min(Number.MAX_SAFE_INTEGER, samples + 1);
|
|
1179
|
+
mutations = Math.min(Number.MAX_SAFE_INTEGER, mutations + Math.max(0, page.mutations - previousMutations));
|
|
1180
|
+
if (nextGeometry !== previousGeometry) geometryChanges++;
|
|
1181
|
+
if (nextViewport !== previousViewport) viewportChanges++;
|
|
1182
|
+
if (nextScroll !== previousScroll) scrollChanges++;
|
|
1183
|
+
previousMutations = page.mutations;
|
|
1184
|
+
previousGeometry = nextGeometry;
|
|
1185
|
+
previousViewport = nextViewport;
|
|
1186
|
+
previousScroll = nextScroll;
|
|
1187
|
+
if (changed) lastChanged = now;
|
|
1188
|
+
if (now >= options.withinMillis) throw fail("settled", Reasons.Timeout.make({}));
|
|
1189
|
+
if (now - lastChanged >= options.quietMillis) {
|
|
1190
|
+
finished = true;
|
|
1191
|
+
cleanup();
|
|
1192
|
+
resolve({
|
|
1193
|
+
quietMillis: options.quietMillis,
|
|
1194
|
+
withinMillis: options.withinMillis,
|
|
1195
|
+
observedMillis: now,
|
|
1196
|
+
signals: [
|
|
1197
|
+
"dom-mutation",
|
|
1198
|
+
"scroll",
|
|
1199
|
+
"root-geometry",
|
|
1200
|
+
"viewport"
|
|
1201
|
+
],
|
|
1202
|
+
samples,
|
|
1203
|
+
mutations,
|
|
1204
|
+
scrollChanges,
|
|
1205
|
+
geometryChanges,
|
|
1206
|
+
viewportChanges,
|
|
1207
|
+
visibility: "visible"
|
|
1208
|
+
});
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
timer?.cancel();
|
|
1212
|
+
timer = timers.sleep(Math.min(25, options.quietMillis - (now - lastChanged), options.withinMillis - now, ticket.remainingMillis()));
|
|
1213
|
+
timer.done.then(check, () => {});
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
failWait(error);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
if (ticket.signal.aborted) {
|
|
1219
|
+
onAbort();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
ticket.signal.addEventListener("abort", onAbort, { once: true });
|
|
1223
|
+
page.watchers.add(check);
|
|
1224
|
+
check();
|
|
1225
|
+
});
|
|
821
1226
|
const invokeBinding = async (name, input, origin) => {
|
|
822
1227
|
const binding = bindings.find((candidate) => candidate.name === name);
|
|
823
1228
|
if (binding === void 0) return { ok: false };
|
|
@@ -837,6 +1242,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
837
1242
|
};
|
|
838
1243
|
try {
|
|
839
1244
|
const reply = await binding.invoke({
|
|
1245
|
+
origin: documentOrigin,
|
|
840
1246
|
read: async (signal) => {
|
|
841
1247
|
await check(signal);
|
|
842
1248
|
return text;
|
|
@@ -874,6 +1280,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
874
1280
|
select(previous !== void 0 && !previous.closed ? previous.pageId : openPages()[0]?.pageId);
|
|
875
1281
|
}
|
|
876
1282
|
const driver = {
|
|
1283
|
+
cachedPages: () => Object.freeze(openPages().map(cachedPage)),
|
|
877
1284
|
...options.pageControl === true ? { pageControl: {
|
|
878
1285
|
state: async (page, ticket) => {
|
|
879
1286
|
ticket.check();
|
|
@@ -894,6 +1301,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
894
1301
|
by: connection
|
|
895
1302
|
};
|
|
896
1303
|
heldObservation(target.pageId);
|
|
1304
|
+
ticket.acknowledge?.();
|
|
1305
|
+
events.pageLifecycle?.({
|
|
1306
|
+
_tag: "Display",
|
|
1307
|
+
page: cachedPage(target)
|
|
1308
|
+
});
|
|
897
1309
|
return PageSuspension.make({
|
|
898
1310
|
pageId: target.pageId,
|
|
899
1311
|
targetId: target.targetId,
|
|
@@ -906,6 +1318,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
906
1318
|
dispatch(ticket, record);
|
|
907
1319
|
target.held = void 0;
|
|
908
1320
|
heldObservation(target.pageId);
|
|
1321
|
+
ticket.acknowledge?.();
|
|
1322
|
+
events.pageLifecycle?.({
|
|
1323
|
+
_tag: "Display",
|
|
1324
|
+
page: cachedPage(target)
|
|
1325
|
+
});
|
|
909
1326
|
}),
|
|
910
1327
|
checkTarget: async (target, ticket) => {
|
|
911
1328
|
ticket.check();
|
|
@@ -947,11 +1364,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
947
1364
|
text: ""
|
|
948
1365
|
}), false));
|
|
949
1366
|
}),
|
|
950
|
-
closePage: (page, ticket) => attempt("close-page", ticket, { pageId: page.pageId }, async (record) => {
|
|
1367
|
+
closePage: (page, ticket, onDispatch) => attempt("close-page", ticket, { pageId: page.pageId }, async (record) => {
|
|
951
1368
|
const target = pageOf(page, "close-page");
|
|
952
1369
|
dispatch(ticket, record);
|
|
953
1370
|
close(target);
|
|
954
|
-
}),
|
|
1371
|
+
}, onDispatch),
|
|
955
1372
|
containPage: (pageId) => attempt("close-page", void 0, { pageId }, async (record) => {
|
|
956
1373
|
const target = pages.get(pageId);
|
|
957
1374
|
if (target === void 0 || target.closed) return;
|
|
@@ -960,7 +1377,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
960
1377
|
if (outcome?._tag === "Fail") throw fail("close-page", outcome.reason, outcome.outcome);
|
|
961
1378
|
close(target);
|
|
962
1379
|
}),
|
|
963
|
-
listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(
|
|
1380
|
+
listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(pageOf(page, "list-frames"))]),
|
|
964
1381
|
resolveFrame: async (page, frame, ticket) => {
|
|
965
1382
|
ticket.check();
|
|
966
1383
|
const target = pageOf(page, "target");
|
|
@@ -970,11 +1387,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
970
1387
|
frameId: target.frameId
|
|
971
1388
|
};
|
|
972
1389
|
},
|
|
973
|
-
|
|
974
|
-
if (id !== selectedPage("select-frame").frameId) throw fail("select-frame", Reasons.NotFound.make({}));
|
|
975
|
-
events.invalidate("target-changed", "none");
|
|
976
|
-
}),
|
|
977
|
-
beginNavigation: (url, timeoutMillis, ticket, target) => attempt("navigate", void 0, { pageId: target?.pageId }, async (record) => {
|
|
1390
|
+
beginNavigation: (url, timeoutMillis, ticket, target) => attempt("navigate", void 0, { pageId: target.pageId }, async (record) => {
|
|
978
1391
|
ticket.check();
|
|
979
1392
|
const page = current(target, "navigate");
|
|
980
1393
|
requireRunning(page, "navigate");
|
|
@@ -1037,7 +1450,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1037
1450
|
};
|
|
1038
1451
|
}),
|
|
1039
1452
|
readText: (selector, maximumBytes, ticket, target) => attempt("read-text", ticket, {
|
|
1040
|
-
pageId: target
|
|
1453
|
+
pageId: target.pageId,
|
|
1041
1454
|
...meta(selector)
|
|
1042
1455
|
}, async () => {
|
|
1043
1456
|
const page = current(target, "read-text");
|
|
@@ -1045,11 +1458,12 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1045
1458
|
const control = bySelector(page, selector, "read-text");
|
|
1046
1459
|
return bounded(control.script.text ?? control.script.label, maximumBytes).text;
|
|
1047
1460
|
}),
|
|
1048
|
-
observe: (scope, maximumBytes, controls, ticket, match) => attempt("observe", ticket, {}, async () => {
|
|
1049
|
-
const page =
|
|
1461
|
+
observe: (scope, maximumBytes, controls, ticket, match, target) => attempt("observe", ticket, { pageId: target.pageId }, async () => {
|
|
1462
|
+
const page = current(target, "observe");
|
|
1463
|
+
const size = page.viewport ?? viewport;
|
|
1050
1464
|
const needle = match?.toLowerCase();
|
|
1051
1465
|
const matches = (value) => needle === void 0 || value.toLowerCase().includes(needle);
|
|
1052
|
-
|
|
1466
|
+
snapshots.delete(page.frameId);
|
|
1053
1467
|
const selects = new Set(page.document.controls.filter((control) => isSelectable(control.script.kind) && (matches(control.script.label) || page.document.controls.some((option) => option.script.selectElementId === control.script.id && matches(option.script.label)))).map((control) => control.script.id));
|
|
1054
1468
|
const matching = page.document.controls.filter((control) => control.script.selectElementId !== void 0 ? selects.has(control.script.selectElementId) : isSelectable(control.script.kind) ? selects.has(control.script.id) : matches(control.script.label));
|
|
1055
1469
|
const reachable = matching.filter((control) => scope === "document" || control.script.offscreen !== true);
|
|
@@ -1058,13 +1472,22 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1058
1472
|
const next = {
|
|
1059
1473
|
id: `observation-${++observationSerial}`,
|
|
1060
1474
|
pageId: page.pageId,
|
|
1475
|
+
frameId: page.frameId,
|
|
1061
1476
|
epoch: page.epoch,
|
|
1062
1477
|
generation: ticket.generation,
|
|
1478
|
+
scope,
|
|
1063
1479
|
validity: "valid",
|
|
1064
1480
|
nodes: new Map(kept.map((control) => [control.script.id, control])),
|
|
1481
|
+
identities: new Map(kept.map((control) => {
|
|
1482
|
+
const fresh = facts(control);
|
|
1483
|
+
return [control.script.id, {
|
|
1484
|
+
identity: identityOf(fresh),
|
|
1485
|
+
stable: stableIdentityOf(fresh)
|
|
1486
|
+
}];
|
|
1487
|
+
})),
|
|
1065
1488
|
revalidated: /* @__PURE__ */ new Set()
|
|
1066
1489
|
};
|
|
1067
|
-
|
|
1490
|
+
snapshots.set(page.frameId, next);
|
|
1068
1491
|
return {
|
|
1069
1492
|
observationId: next.id,
|
|
1070
1493
|
scope,
|
|
@@ -1075,8 +1498,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1075
1498
|
controls: kept.map(observed),
|
|
1076
1499
|
controlsTruncated: kept.length < reachable.length,
|
|
1077
1500
|
viewport: ViewportEvidence.make({
|
|
1078
|
-
width:
|
|
1079
|
-
height:
|
|
1501
|
+
width: size.width,
|
|
1502
|
+
height: size.height,
|
|
1503
|
+
...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
|
|
1080
1504
|
clippedText: 0,
|
|
1081
1505
|
coveredText: 0,
|
|
1082
1506
|
uncertainText: 0,
|
|
@@ -1085,8 +1509,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1085
1509
|
})
|
|
1086
1510
|
};
|
|
1087
1511
|
}),
|
|
1088
|
-
checkpoint: (maximumBytes, controls, pictureBytes, ticket) => attempt("checkpoint", ticket, {}, async () => {
|
|
1089
|
-
const page =
|
|
1512
|
+
checkpoint: (maximumBytes, controls, pictureBytes, ticket, target) => attempt("checkpoint", ticket, { pageId: target.pageId }, async () => {
|
|
1513
|
+
const page = current(target, "checkpoint");
|
|
1514
|
+
const size = page.viewport ?? viewport;
|
|
1090
1515
|
const text = bounded(page.document.text, maximumBytes);
|
|
1091
1516
|
const reachable = page.document.controls.filter((control) => control.script.offscreen !== true);
|
|
1092
1517
|
return {
|
|
@@ -1096,8 +1521,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1096
1521
|
controls: reachable.slice(0, controls).map(facts),
|
|
1097
1522
|
controlsTruncated: reachable.length > controls,
|
|
1098
1523
|
viewport: ViewportEvidence.make({
|
|
1099
|
-
width:
|
|
1100
|
-
height:
|
|
1524
|
+
width: size.width,
|
|
1525
|
+
height: size.height,
|
|
1526
|
+
...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
|
|
1101
1527
|
clippedText: 0,
|
|
1102
1528
|
coveredText: 0,
|
|
1103
1529
|
uncertainText: 0,
|
|
@@ -1105,19 +1531,53 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1105
1531
|
exhausted: false
|
|
1106
1532
|
}),
|
|
1107
1533
|
documentChanged: false,
|
|
1108
|
-
...pictureBytes === void 0 ? {} : { picture:
|
|
1534
|
+
...pictureBytes === void 0 ? {} : { picture: picture(page, ticket) }
|
|
1109
1535
|
};
|
|
1110
1536
|
}),
|
|
1111
|
-
controlFacts: (target, ticket) => attempt("control-facts", ticket,
|
|
1112
|
-
|
|
1113
|
-
|
|
1537
|
+
controlFacts: (target, ticket, browserTarget) => attempt("control-facts", ticket, {
|
|
1538
|
+
pageId: browserTarget.pageId,
|
|
1539
|
+
...meta(target)
|
|
1540
|
+
}, async () => resolve(target, ticket, "control-facts", void 0, browserTarget).facts),
|
|
1541
|
+
resolveDescriptor: (descriptor, ticket, target, guard = { _tag: "Strict" }) => attempt("resolve", ticket, { pageId: target.pageId }, async () => {
|
|
1542
|
+
const group = resolveGroup([{ target: {
|
|
1543
|
+
_tag: "Descriptor",
|
|
1544
|
+
descriptor
|
|
1545
|
+
} }], ticket, target, guard);
|
|
1546
|
+
const element = group.elements[0];
|
|
1547
|
+
const resolved = element === void 0 ? void 0 : resolvedElements.get(element);
|
|
1548
|
+
if (resolved === void 0) {
|
|
1549
|
+
await group.release();
|
|
1550
|
+
throw fail("resolve", Reasons.Malformed.make({}));
|
|
1551
|
+
}
|
|
1552
|
+
try {
|
|
1553
|
+
ticket.check();
|
|
1554
|
+
const previous = snapshots.get(resolved.snapshot.frameId);
|
|
1555
|
+
if (previous !== void 0) previous.validity = "invalid";
|
|
1556
|
+
privateSnapshots.delete(resolved.snapshot);
|
|
1557
|
+
snapshots.set(resolved.snapshot.frameId, {
|
|
1558
|
+
...resolved.snapshot,
|
|
1559
|
+
scope: resolved.scope
|
|
1560
|
+
});
|
|
1561
|
+
return ObservedElement.make({
|
|
1562
|
+
observationId: resolved.snapshot.id,
|
|
1563
|
+
elementId: resolved.node.script.id
|
|
1564
|
+
});
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
await group.release();
|
|
1567
|
+
throw error;
|
|
1568
|
+
}
|
|
1114
1569
|
}),
|
|
1115
|
-
|
|
1116
|
-
|
|
1570
|
+
resolveGroup: (requests, ticket, target, guard) => attempt("resolve", ticket, { pageId: target.pageId }, async () => resolveGroup(requests, ticket, target, guard)),
|
|
1571
|
+
expectations: (conditions, ticket, target) => attempt("run", ticket, { pageId: target.pageId }, async () => expectations(conditions, ticket, target)),
|
|
1572
|
+
revalidate: (target, ticket, browserTarget) => attempt("revalidate", ticket, {
|
|
1573
|
+
pageId: browserTarget.pageId,
|
|
1574
|
+
...meta(target)
|
|
1575
|
+
}, async () => {
|
|
1576
|
+
const { snapshot: current } = retained(target, ticket, "revalidate", true, true, browserTarget);
|
|
1117
1577
|
current.revalidated.add(target.elementId);
|
|
1118
1578
|
}),
|
|
1119
1579
|
click: (target, ticket, capture, policy, browserTarget) => attempt("click", ticket, {
|
|
1120
|
-
pageId: browserTarget
|
|
1580
|
+
pageId: browserTarget.pageId,
|
|
1121
1581
|
...meta(target)
|
|
1122
1582
|
}, async (record) => {
|
|
1123
1583
|
const { node, page } = resolve(target, ticket, "click", policy, browserTarget);
|
|
@@ -1131,9 +1591,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1131
1591
|
url,
|
|
1132
1592
|
input
|
|
1133
1593
|
};
|
|
1134
|
-
}, () => invalidatePointer(browserTarget
|
|
1594
|
+
}, () => invalidatePointer(browserTarget.pageId)),
|
|
1135
1595
|
fill: (target, value, ticket, policy, browserTarget) => attempt("fill", ticket, {
|
|
1136
|
-
pageId: browserTarget
|
|
1596
|
+
pageId: browserTarget.pageId,
|
|
1137
1597
|
...meta(target)
|
|
1138
1598
|
}, async (record) => {
|
|
1139
1599
|
const { node, page } = resolve(target, ticket, "fill", policy, browserTarget);
|
|
@@ -1145,25 +1605,34 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1145
1605
|
page.values.set(node.script.id, value);
|
|
1146
1606
|
return page.document.url;
|
|
1147
1607
|
}),
|
|
1148
|
-
selectOption: (target, ids, ticket, policy) => attempt("select-option", ticket,
|
|
1149
|
-
|
|
1608
|
+
selectOption: (target, ids, ticket, policy, browserTarget) => attempt("select-option", ticket, {
|
|
1609
|
+
pageId: browserTarget.pageId,
|
|
1610
|
+
...meta(target)
|
|
1611
|
+
}, async (record) => {
|
|
1612
|
+
const { node, page } = resolve(target, ticket, "select-option", policy, browserTarget);
|
|
1150
1613
|
requireRunning(page, "select-option");
|
|
1151
|
-
const options = chosen(node, ids, "select-option");
|
|
1614
|
+
const options = chosen(page, node, ids, ticket, "select-option");
|
|
1152
1615
|
dispatch(ticket, record);
|
|
1153
1616
|
choose(page, node, options);
|
|
1154
1617
|
return page.document.url;
|
|
1155
1618
|
}),
|
|
1156
|
-
formStep: (target, field, ticket, policy, _settleMillis, capture) => attempt("fill-form", ticket,
|
|
1157
|
-
|
|
1619
|
+
formStep: (target, field, ticket, policy, _settleMillis, capture, browserTarget) => attempt("fill-form", ticket, {
|
|
1620
|
+
pageId: browserTarget.pageId,
|
|
1621
|
+
...meta(target)
|
|
1622
|
+
}, async (record) => {
|
|
1623
|
+
const { node, page } = resolve(target, ticket, "fill-form", policy, browserTarget, false, true);
|
|
1158
1624
|
requireRunning(page, "fill-form");
|
|
1159
1625
|
return formStep(page, node, field, ticket, record, capture);
|
|
1160
|
-
}, field.checked === void 0 ? void 0 : () => invalidatePointer(
|
|
1161
|
-
formState: (targets, ticket) => attempt("fill-form", ticket, {}, async () => targets.map((target) => {
|
|
1162
|
-
const { node, page } =
|
|
1626
|
+
}, field.checked === void 0 ? void 0 : () => invalidatePointer(browserTarget.pageId)),
|
|
1627
|
+
formState: (targets, ticket, browserTarget) => attempt("fill-form", ticket, { pageId: browserTarget.pageId }, async () => targets.map((target) => {
|
|
1628
|
+
const { node, page } = retainedElement(target, ticket, "fill-form", false, false, browserTarget);
|
|
1163
1629
|
return page.document.controls.includes(node) ? fieldState(page, node) : void 0;
|
|
1164
1630
|
})),
|
|
1165
|
-
formSubmit: (target, ticket, capture, policy) => attempt("fill-form", ticket,
|
|
1166
|
-
|
|
1631
|
+
formSubmit: (target, ticket, capture, policy, browserTarget) => attempt("fill-form", ticket, {
|
|
1632
|
+
pageId: browserTarget.pageId,
|
|
1633
|
+
...meta(target)
|
|
1634
|
+
}, async (record) => {
|
|
1635
|
+
const { node, page } = resolve(target, ticket, "fill-form", policy, browserTarget, false, true);
|
|
1167
1636
|
requireRunning(page, "fill-form");
|
|
1168
1637
|
dispatch(ticket, record);
|
|
1169
1638
|
let url = "";
|
|
@@ -1174,14 +1643,29 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1174
1643
|
url,
|
|
1175
1644
|
input
|
|
1176
1645
|
};
|
|
1177
|
-
}, () => invalidatePointer(
|
|
1178
|
-
scroll: (
|
|
1646
|
+
}, () => invalidatePointer(browserTarget.pageId)),
|
|
1647
|
+
scroll: (deltaX, deltaY, ticket, target) => attempt("scroll", ticket, { pageId: target.pageId }, async (record) => {
|
|
1179
1648
|
const page = current(target, "scroll");
|
|
1180
1649
|
requireRunning(page, "scroll");
|
|
1181
1650
|
dispatch(ticket, record);
|
|
1651
|
+
page.scroll.x += deltaX;
|
|
1652
|
+
page.scroll.y += deltaY;
|
|
1653
|
+
notify(page);
|
|
1182
1654
|
return page.document.url;
|
|
1183
1655
|
}),
|
|
1184
|
-
|
|
1656
|
+
scrollTo: (target, ticket, browserTarget) => attempt("scroll", ticket, {
|
|
1657
|
+
pageId: browserTarget.pageId,
|
|
1658
|
+
...meta(target)
|
|
1659
|
+
}, async (record) => {
|
|
1660
|
+
const { page, facts: fresh } = resolve(target, ticket, "scroll", void 0, browserTarget);
|
|
1661
|
+
requireRunning(page, "scroll");
|
|
1662
|
+
dispatch(ticket, record);
|
|
1663
|
+
page.scroll.x += fresh.box.x;
|
|
1664
|
+
page.scroll.y += fresh.box.y;
|
|
1665
|
+
notify(page);
|
|
1666
|
+
return page.document.url;
|
|
1667
|
+
}),
|
|
1668
|
+
pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target.pageId }, async (record) => {
|
|
1185
1669
|
const page = current(target, "pointer-move");
|
|
1186
1670
|
requireRunning(page, "pointer-move");
|
|
1187
1671
|
dispatch(ticket, record);
|
|
@@ -1192,7 +1676,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1192
1676
|
return receipt(page.pageId);
|
|
1193
1677
|
}),
|
|
1194
1678
|
hover: (target, ticket, policy, browserTarget) => attempt("hover", ticket, {
|
|
1195
|
-
pageId: browserTarget
|
|
1679
|
+
pageId: browserTarget.pageId,
|
|
1196
1680
|
...meta(target)
|
|
1197
1681
|
}, async (record) => {
|
|
1198
1682
|
const { node, page, facts: fresh } = resolve(target, ticket, "hover", policy, browserTarget);
|
|
@@ -1205,7 +1689,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1205
1689
|
});
|
|
1206
1690
|
return receipt(page.pageId);
|
|
1207
1691
|
}),
|
|
1208
|
-
wheel: (
|
|
1692
|
+
wheel: (deltaX, deltaY, at, ticket, target) => attempt("wheel", ticket, { pageId: target.pageId }, async (record) => {
|
|
1209
1693
|
const page = current(target, "wheel");
|
|
1210
1694
|
requireRunning(page, "wheel");
|
|
1211
1695
|
dispatch(ticket, record);
|
|
@@ -1213,10 +1697,13 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1213
1697
|
x: at.x,
|
|
1214
1698
|
y: at.y
|
|
1215
1699
|
});
|
|
1700
|
+
page.scroll.x += deltaX;
|
|
1701
|
+
page.scroll.y += deltaY;
|
|
1702
|
+
notify(page);
|
|
1216
1703
|
return receipt(page.pageId);
|
|
1217
1704
|
}),
|
|
1218
1705
|
press: (key, _modifiers, into, ticket, policy, browserTarget) => attempt("press", ticket, {
|
|
1219
|
-
pageId: browserTarget
|
|
1706
|
+
pageId: browserTarget.pageId,
|
|
1220
1707
|
...meta(into)
|
|
1221
1708
|
}, async (record) => {
|
|
1222
1709
|
const page = focusedOrRefuse(into, ticket, "press", policy, browserTarget);
|
|
@@ -1226,7 +1713,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1226
1713
|
return receipt(page.pageId);
|
|
1227
1714
|
}),
|
|
1228
1715
|
type: (text, into, ticket, policy, browserTarget) => attempt("type", ticket, {
|
|
1229
|
-
pageId: browserTarget
|
|
1716
|
+
pageId: browserTarget.pageId,
|
|
1230
1717
|
...meta(into)
|
|
1231
1718
|
}, async (record) => {
|
|
1232
1719
|
const page = focusedOrRefuse(into, ticket, "type", policy, browserTarget);
|
|
@@ -1235,19 +1722,19 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1235
1722
|
typed(page, text);
|
|
1236
1723
|
return receipt(page.pageId);
|
|
1237
1724
|
}),
|
|
1238
|
-
screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target
|
|
1239
|
-
current(target, "screenshot");
|
|
1240
|
-
return
|
|
1725
|
+
screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target.pageId }, async () => {
|
|
1726
|
+
const page = current(target, "screenshot");
|
|
1727
|
+
return picture(page, ticket);
|
|
1241
1728
|
}),
|
|
1242
|
-
resize: (next, ticket) => attempt("resize", ticket, {}, async (record) => {
|
|
1243
|
-
const page =
|
|
1729
|
+
resize: (next, ticket, target) => attempt("resize", ticket, { pageId: target.pageId }, async (record) => {
|
|
1730
|
+
const page = current(target, "resize");
|
|
1244
1731
|
dispatch(ticket, record);
|
|
1245
|
-
viewport = {
|
|
1732
|
+
page.viewport = {
|
|
1246
1733
|
width: next.width,
|
|
1247
1734
|
height: next.height
|
|
1248
1735
|
};
|
|
1249
1736
|
page.capture?.start.invalidate("resized");
|
|
1250
|
-
events.invalidate("resized");
|
|
1737
|
+
events.invalidate("resized", { pageId: page.pageId });
|
|
1251
1738
|
}),
|
|
1252
1739
|
waitFor: (selector, state, ticket, target) => attempt("wait", ticket, {
|
|
1253
1740
|
pageId: target.pageId,
|
|
@@ -1271,12 +1758,17 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1271
1758
|
}
|
|
1272
1759
|
}, () => page.closed || page.epoch !== epoch);
|
|
1273
1760
|
}).finally(() => ticket.retire()),
|
|
1761
|
+
settled: (options, ticket, target) => attempt("settled", ticket, { pageId: target.pageId }, async (record) => {
|
|
1762
|
+
const page = current(target, "settled");
|
|
1763
|
+
requireRunning(page, "settled");
|
|
1764
|
+
return waitSettled(options, ticket, page, record);
|
|
1765
|
+
}).finally(() => ticket.retire()),
|
|
1274
1766
|
waitForElement: (reference, state, ticket, target) => attempt("wait", ticket, {
|
|
1275
1767
|
pageId: target.pageId,
|
|
1276
|
-
|
|
1768
|
+
...meta(reference)
|
|
1277
1769
|
}, async (record) => {
|
|
1278
1770
|
const page = current(target, "wait");
|
|
1279
|
-
const { node } =
|
|
1771
|
+
const { node } = retainedElement(reference, ticket, "wait", true, true, target);
|
|
1280
1772
|
const epoch = page.epoch;
|
|
1281
1773
|
await waitUntil(ticket, page, "wait", record, () => {
|
|
1282
1774
|
const present = page.document.controls.includes(node);
|
|
@@ -1289,8 +1781,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1289
1781
|
}
|
|
1290
1782
|
}, () => page.closed || page.epoch !== epoch);
|
|
1291
1783
|
}).finally(() => ticket.retire()),
|
|
1292
|
-
clickAndWait: (target, ticket, capture) => attempt("click-and-wait", ticket,
|
|
1293
|
-
|
|
1784
|
+
clickAndWait: (target, ticket, capture, browserTarget) => attempt("click-and-wait", ticket, {
|
|
1785
|
+
pageId: browserTarget.pageId,
|
|
1786
|
+
...meta(target)
|
|
1787
|
+
}, async (record) => {
|
|
1788
|
+
const { node, page } = resolve(target, ticket, "click-and-wait", void 0, browserTarget);
|
|
1294
1789
|
requireRunning(page, "click-and-wait");
|
|
1295
1790
|
dispatch(ticket, record);
|
|
1296
1791
|
let url = "";
|
|
@@ -1301,9 +1796,12 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1301
1796
|
url,
|
|
1302
1797
|
input
|
|
1303
1798
|
};
|
|
1304
|
-
}, () => invalidatePointer(
|
|
1305
|
-
clickForDownload: (target, ticket) => attempt("download-action", ticket,
|
|
1306
|
-
|
|
1799
|
+
}, () => invalidatePointer(browserTarget.pageId)),
|
|
1800
|
+
clickForDownload: (target, ticket, browserTarget) => attempt("download-action", ticket, {
|
|
1801
|
+
pageId: browserTarget.pageId,
|
|
1802
|
+
...meta(target)
|
|
1803
|
+
}, async (record) => {
|
|
1804
|
+
const { node, page } = resolve(target, ticket, "download-action", void 0, browserTarget);
|
|
1307
1805
|
requireRunning(page, "download-action");
|
|
1308
1806
|
if (node.script.download === void 0) throw fail("download-action", Reasons.Unsupported.make({}));
|
|
1309
1807
|
dispatch(ticket, record);
|
|
@@ -1314,8 +1812,14 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1314
1812
|
state: "completed"
|
|
1315
1813
|
};
|
|
1316
1814
|
}),
|
|
1317
|
-
selectFiles: (target, files, ticket) => attempt("select-files", ticket,
|
|
1318
|
-
|
|
1815
|
+
selectFiles: (target, files, ticket, browserTarget) => attempt("select-files", ticket, {
|
|
1816
|
+
pageId: browserTarget.pageId,
|
|
1817
|
+
...meta(target)
|
|
1818
|
+
}, async (record) => selectFilesOn(target, files, ticket, record, "select-files", browserTarget)),
|
|
1819
|
+
clickForFileSelection: (target, files, ticket, browserTarget) => attempt("file-chooser", ticket, {
|
|
1820
|
+
pageId: browserTarget.pageId,
|
|
1821
|
+
...meta(target)
|
|
1822
|
+
}, async (record) => selectFilesOn(target, files, ticket, record, "file-chooser", browserTarget)),
|
|
1319
1823
|
documentReadiness: async (ticket, target) => {
|
|
1320
1824
|
ticket.check();
|
|
1321
1825
|
const page = current(target, "ready");
|
|
@@ -1327,7 +1831,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1327
1831
|
},
|
|
1328
1832
|
dismissDialogs: async () => {},
|
|
1329
1833
|
capture: async (target) => {
|
|
1330
|
-
const page =
|
|
1834
|
+
const page = pageOf(PageInfo.make({
|
|
1331
1835
|
pageId: target.pageId,
|
|
1332
1836
|
targetId: target.targetId,
|
|
1333
1837
|
url: "",
|
|
@@ -1355,8 +1859,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1355
1859
|
}
|
|
1356
1860
|
};
|
|
1357
1861
|
},
|
|
1358
|
-
invalidateObservation: (scope) => {
|
|
1359
|
-
invalidateSnapshot(scope);
|
|
1862
|
+
invalidateObservation: (scope, origin) => {
|
|
1863
|
+
invalidateSnapshot(scope, origin);
|
|
1360
1864
|
},
|
|
1361
1865
|
disconnect: async () => {
|
|
1362
1866
|
if (disconnected) return;
|
|
@@ -1373,8 +1877,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1373
1877
|
dropConnection(false, "closed");
|
|
1374
1878
|
}
|
|
1375
1879
|
};
|
|
1376
|
-
const selectFilesOn = (target, files, ticket, record, operation) => {
|
|
1377
|
-
const { node, page } = resolve(target, ticket, operation);
|
|
1880
|
+
const selectFilesOn = (target, files, ticket, record, operation, browserTarget) => {
|
|
1881
|
+
const { node, page } = resolve(target, ticket, operation, void 0, browserTarget);
|
|
1378
1882
|
requireRunning(page, operation);
|
|
1379
1883
|
if (node.script.kind !== "input" || node.script.inputType !== "file") throw fail(operation, Reasons.Unsupported.make({}));
|
|
1380
1884
|
dispatch(ticket, record);
|
|
@@ -1382,12 +1886,43 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1382
1886
|
return page.document.url;
|
|
1383
1887
|
};
|
|
1384
1888
|
const connection = {
|
|
1889
|
+
opened: (pageId) => {
|
|
1890
|
+
const page = pages.get(pageId);
|
|
1891
|
+
if (page !== void 0) events.pageLifecycle?.({
|
|
1892
|
+
_tag: "Opened",
|
|
1893
|
+
page: cachedPage(page)
|
|
1894
|
+
});
|
|
1895
|
+
},
|
|
1896
|
+
navigated: (pageId, sameDocument) => {
|
|
1897
|
+
const page = pages.get(pageId);
|
|
1898
|
+
if (page !== void 0) events.pageLifecycle?.({
|
|
1899
|
+
_tag: "Navigated",
|
|
1900
|
+
page: cachedPage(page),
|
|
1901
|
+
frameId: page.frameId,
|
|
1902
|
+
documentEpoch: page.epoch,
|
|
1903
|
+
sameDocument,
|
|
1904
|
+
url: page.document.url.length <= 8192 ? page.document.url : null,
|
|
1905
|
+
urlQualification: page.document.url.length <= 8192 ? "NativeCached" : "Omitted"
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
metadata: (pageId) => {
|
|
1909
|
+
const page = pages.get(pageId);
|
|
1910
|
+
if (page !== void 0) events.pageLifecycle?.({
|
|
1911
|
+
_tag: "Metadata",
|
|
1912
|
+
page: cachedPage(page)
|
|
1913
|
+
});
|
|
1914
|
+
},
|
|
1385
1915
|
retire: (pageId) => invalidateSnapshot({ pageId }),
|
|
1916
|
+
closed: (pageId) => {
|
|
1917
|
+
const page = pages.get(pageId);
|
|
1918
|
+
if (page !== void 0) snapshots.delete(page.frameId);
|
|
1919
|
+
events.pageClosed?.(pageId, page === void 0 ? void 0 : cachedPage(page));
|
|
1920
|
+
},
|
|
1386
1921
|
announce: (pageId) => events.invalidate("target-changed", { pageId }),
|
|
1387
1922
|
drop: () => dropConnection(true),
|
|
1388
1923
|
invoke: invokeBinding,
|
|
1389
1924
|
pointer: () => pointer,
|
|
1390
|
-
viewport: () => viewport
|
|
1925
|
+
viewport: (pageId) => pages.get(pageId)?.viewport ?? viewport
|
|
1391
1926
|
};
|
|
1392
1927
|
index = connections.push("open") - 1;
|
|
1393
1928
|
live.add(connection);
|
|
@@ -1398,6 +1933,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1398
1933
|
url: document.url,
|
|
1399
1934
|
title: document.title,
|
|
1400
1935
|
text: document.text,
|
|
1936
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
1401
1937
|
controls: document.controls.map((control) => ({
|
|
1402
1938
|
...control.script,
|
|
1403
1939
|
...control.checked === void 0 ? {} : { checked: control.checked },
|
|
@@ -1436,7 +1972,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1436
1972
|
const running = page.capture?.start;
|
|
1437
1973
|
if (running === void 0) return Effect.fail(fail("capture-consume", Reasons.NotFound.make({})));
|
|
1438
1974
|
const timestamp = frame.timestamp ?? lastTimestamp + 40;
|
|
1439
|
-
const viewport = page.capture?.by.viewport() ?? {
|
|
1975
|
+
const viewport = page.capture?.by.viewport(page.pageId) ?? {
|
|
1440
1976
|
width: 0,
|
|
1441
1977
|
height: 0
|
|
1442
1978
|
};
|
|
@@ -1549,23 +2085,24 @@ const makeEngine = Effect.fnUntraced(function* (script) {
|
|
|
1549
2085
|
* Open one scripted browser in the caller's Scope. The session is the real owner over a
|
|
1550
2086
|
* scripted native engine: admission, budgets, staleness, dispatch evidence, capture, page holds
|
|
1551
2087
|
* and typed callbacks are the production code paths. Only the page and its outcomes are scripted,
|
|
1552
|
-
* and the owner's ids and handoff tokens
|
|
2088
|
+
* and the owner's ids and handoff tokens use deterministic Crypto namespaced by this
|
|
2089
|
+
* scripted reference, so independently opened sessions have distinct evidence domains.
|
|
1553
2090
|
*/
|
|
1554
2091
|
const open = Effect.fnUntraced(function* (script, options = {}) {
|
|
1555
2092
|
const fixed = yield* checked(Script, script, "configure");
|
|
1556
2093
|
const policy = yield* checked(BrowserPolicy, options.policy ?? BrowserPolicy.unrestricted(), "configure");
|
|
1557
2094
|
const engine = yield* makeEngine(fixed);
|
|
1558
|
-
const
|
|
2095
|
+
const namespace = ++references;
|
|
2096
|
+
const reference = Object.freeze(ScriptedReference.make({
|
|
2097
|
+
provider: "scripted",
|
|
2098
|
+
id: `scripted-${namespace}`
|
|
2099
|
+
}));
|
|
2100
|
+
const acquired = yield* (yield* make({
|
|
1559
2101
|
implementation: "scripted",
|
|
1560
2102
|
binding: engine.binding,
|
|
1561
2103
|
...options.automation === void 0 ? {} : { automation: options.automation },
|
|
1562
2104
|
...options.viewport === void 0 ? {} : { viewport: options.viewport }
|
|
1563
|
-
}).pipe(Effect.provide(
|
|
1564
|
-
const reference = Object.freeze(ScriptedReference.make({
|
|
1565
|
-
provider: "scripted",
|
|
1566
|
-
id: `scripted-${++references}`
|
|
1567
|
-
}));
|
|
1568
|
-
const acquired = yield* runtime.acquire(policy, scriptedSource(reference, options.onCleanup), options.bootstrap === void 0 ? {} : { bootstrap: options.bootstrap });
|
|
2105
|
+
}).pipe(Effect.provide(Layer.sync(Crypto.Crypto, () => makeSequentialCrypto(namespace))))).acquire(policy, scriptedSource(reference, options.onCleanup), options.bootstrap === void 0 ? {} : { bootstrap: options.bootstrap });
|
|
1569
2106
|
const connection = yield* acquired.connect;
|
|
1570
2107
|
const control = engine.controls().at(-1);
|
|
1571
2108
|
if (control === void 0) return yield* BrowserError.make({
|
|
@@ -1584,7 +2121,8 @@ const open = Effect.fnUntraced(function* (script, options = {}) {
|
|
|
1584
2121
|
/**
|
|
1585
2122
|
* The `Crypto` scripted browsers draw their ids and handoff tokens from. Its bytes count up from
|
|
1586
2123
|
* one on each build, so every value is predictable; it is not random and computes no digest.
|
|
1587
|
-
* `open` uses
|
|
2124
|
+
* `open` uses the same counter with its original scripted-reference namespace. A provider
|
|
2125
|
+
* Layer built over `binding` still requires a `Crypto`, and
|
|
1588
2126
|
* this one keeps such a test free of a platform package.
|
|
1589
2127
|
*/
|
|
1590
2128
|
const sequentialCrypto = Layer.sync(Crypto.Crypto, makeSequentialCrypto);
|