effect-browser 0.2.0-beta.6 → 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 +507 -130
- package/dist/Browser-n-bKgBNN.d.mts +372 -0
- package/dist/Browser.d.mts +3 -189
- package/dist/Browser.mjs +9 -1
- package/dist/Browser.mjs.map +1 -1
- package/dist/{BrowserData-DPh3Oaof.d.mts → BrowserData-CPIIIHol.d.mts} +84 -10
- package/dist/BrowserData.d.mts +2 -2
- package/dist/BrowserData.mjs +106 -7
- package/dist/BrowserData.mjs.map +1 -1
- package/dist/{BrowserRuntime-BBAjpVFa.d.mts → BrowserRuntime-CFe9uUTv.d.mts} +16 -11
- package/dist/{BrowserRuntime-DImn_Oe_.mjs → BrowserRuntime-HJ1yYlFS.mjs} +4977 -4134
- 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 +69 -17
- package/dist/Errors.mjs +57 -6
- 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 +675 -123
- 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-BsSfBYVx.mjs +0 -138
- package/dist/Association-BsSfBYVx.mjs.map +0 -1
- package/dist/BrowserRuntime-DImn_Oe_.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",
|
|
@@ -170,9 +187,9 @@ const ScriptableOperation = Schema.Literals([
|
|
|
170
187
|
"press",
|
|
171
188
|
"type",
|
|
172
189
|
"list-pages",
|
|
190
|
+
"describe-page",
|
|
173
191
|
"list-frames",
|
|
174
192
|
"select-page",
|
|
175
|
-
"select-frame",
|
|
176
193
|
"new-page",
|
|
177
194
|
"close-page",
|
|
178
195
|
"resize",
|
|
@@ -300,6 +317,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
300
317
|
url: document.url,
|
|
301
318
|
title: document.title ?? "",
|
|
302
319
|
text: document.text,
|
|
320
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
303
321
|
controls: (document.controls ?? []).map(controlState)
|
|
304
322
|
});
|
|
305
323
|
const documentFor = (url) => {
|
|
@@ -320,6 +338,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
320
338
|
registeredEpoch: registered ? 0 : -1,
|
|
321
339
|
values: /* @__PURE__ */ new Map(),
|
|
322
340
|
files: /* @__PURE__ */ new Map(),
|
|
341
|
+
scroll: {
|
|
342
|
+
x: 0,
|
|
343
|
+
y: 0
|
|
344
|
+
},
|
|
345
|
+
mutations: 0,
|
|
323
346
|
focused: void 0,
|
|
324
347
|
closed: false,
|
|
325
348
|
held: void 0,
|
|
@@ -328,6 +351,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
328
351
|
capture: void 0
|
|
329
352
|
};
|
|
330
353
|
pages.set(page.pageId, page);
|
|
354
|
+
for (const connection of live) connection.opened(page.pageId);
|
|
331
355
|
return page;
|
|
332
356
|
};
|
|
333
357
|
const openPages = () => [...pages.values()].filter((page) => !page.closed);
|
|
@@ -338,12 +362,17 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
338
362
|
/** The page shows a new document: nodes go stale, values reset, captures learn of it. */
|
|
339
363
|
const commit = (page, document) => {
|
|
340
364
|
page.document = document;
|
|
365
|
+
page.cachedTitle = void 0;
|
|
341
366
|
page.epoch++;
|
|
342
367
|
page.values.clear();
|
|
343
368
|
page.files.clear();
|
|
344
369
|
page.focused = void 0;
|
|
370
|
+
page.scroll.x = 0;
|
|
371
|
+
page.scroll.y = 0;
|
|
372
|
+
page.mutations++;
|
|
345
373
|
page.navigation = void 0;
|
|
346
374
|
for (const connection of live) connection.retire(page.pageId);
|
|
375
|
+
for (const connection of live) connection.navigated(page.pageId, false);
|
|
347
376
|
if (page.capture !== void 0) {
|
|
348
377
|
const { start } = page.capture;
|
|
349
378
|
if (start.document === void 0) start.invalidate("target-changed");
|
|
@@ -354,6 +383,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
354
383
|
};
|
|
355
384
|
/** In-document change: identity survives by control id, so retained nodes and waits carry on. */
|
|
356
385
|
const update = (page, document) => {
|
|
386
|
+
const previousUrl = page.document.url;
|
|
387
|
+
const before = JSON.stringify(page.document);
|
|
357
388
|
const previous = new Map(page.document.controls.map((control) => [control.script.id, control]));
|
|
358
389
|
const controls = (document.controls ?? []).map((script) => {
|
|
359
390
|
const existing = previous.get(script.id);
|
|
@@ -367,9 +398,15 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
367
398
|
url: document.url,
|
|
368
399
|
title: document.title ?? "",
|
|
369
400
|
text: document.text,
|
|
401
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
370
402
|
controls
|
|
371
403
|
};
|
|
404
|
+
if (JSON.stringify(page.document) !== before) page.mutations++;
|
|
372
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
|
+
}
|
|
373
410
|
notify(page);
|
|
374
411
|
};
|
|
375
412
|
const take = (operation) => {
|
|
@@ -392,11 +429,13 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
392
429
|
throw new Error("The scripted browser refused this connection");
|
|
393
430
|
}
|
|
394
431
|
const bindings = options.bindings ?? [];
|
|
395
|
-
|
|
432
|
+
const viewport = { ...options.viewport };
|
|
396
433
|
const positions = /* @__PURE__ */ new Map();
|
|
397
434
|
let pointer = null;
|
|
398
435
|
let selectedId;
|
|
399
|
-
|
|
436
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
437
|
+
const privateSnapshots = /* @__PURE__ */ new Set();
|
|
438
|
+
const resolvedElements = /* @__PURE__ */ new WeakMap();
|
|
400
439
|
let disconnected = false;
|
|
401
440
|
let index = -1;
|
|
402
441
|
const place = (pageId, point) => {
|
|
@@ -410,8 +449,16 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
410
449
|
const dispatchObservers = /* @__PURE__ */ new WeakMap();
|
|
411
450
|
const receipt = (pageId) => ({ position: positions.get(pageId) ?? null });
|
|
412
451
|
const select = (pageId) => {
|
|
452
|
+
const previous = selectedId;
|
|
413
453
|
selectedId = pageId;
|
|
414
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
|
+
}
|
|
415
462
|
};
|
|
416
463
|
const selectedPage = (operation = "target") => {
|
|
417
464
|
const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
|
|
@@ -419,7 +466,6 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
419
466
|
return page;
|
|
420
467
|
};
|
|
421
468
|
const current = (target, operation) => {
|
|
422
|
-
if (target === void 0) return selectedPage(operation);
|
|
423
469
|
if (disconnected) throw fail(operation, Reasons.Closed.make({}));
|
|
424
470
|
const page = pages.get(target.pageId);
|
|
425
471
|
if (page === void 0 || page.closed || page.frameId !== target.frameId) throw fail(operation, Reasons.Stale.make({}));
|
|
@@ -431,26 +477,91 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
431
477
|
if (page.closed || page.targetId !== info.targetId) throw fail(operation, Reasons.Stale.make({}));
|
|
432
478
|
return page;
|
|
433
479
|
};
|
|
434
|
-
const
|
|
480
|
+
const cachedPage = (page) => Object.freeze({
|
|
435
481
|
pageId: page.pageId,
|
|
482
|
+
frameId: page.frameId,
|
|
436
483
|
targetId: page.targetId,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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"
|
|
440
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
|
+
};
|
|
441
529
|
const frameInfo = (page) => FrameInfo.make({
|
|
442
530
|
frameId: page.frameId,
|
|
443
531
|
parentFrameId: null,
|
|
444
532
|
url: page.document.url,
|
|
445
533
|
name: ""
|
|
446
534
|
});
|
|
447
|
-
const invalidateSnapshot = (scope = "all") => {
|
|
448
|
-
|
|
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
|
+
}
|
|
540
|
+
};
|
|
541
|
+
/** The page closes as a browser closes it: every connection learns, and nothing on it lasts. */
|
|
542
|
+
const close = (target) => {
|
|
543
|
+
target.closed = true;
|
|
544
|
+
target.navigation?.drop();
|
|
545
|
+
target.capture?.start.invalidate("target-changed");
|
|
546
|
+
target.capture = void 0;
|
|
547
|
+
notify(target);
|
|
548
|
+
if (selectedId === target.pageId) select(void 0);
|
|
549
|
+
snapshots.delete(target.frameId);
|
|
550
|
+
events.pageClosed?.(target.pageId, cachedPage(target));
|
|
551
|
+
events.invalidate("target-changed", { pageId: target.pageId });
|
|
552
|
+
for (const other of live) {
|
|
553
|
+
if (other === connection) continue;
|
|
554
|
+
other.closed(target.pageId);
|
|
555
|
+
other.announce(target.pageId);
|
|
556
|
+
}
|
|
449
557
|
};
|
|
450
558
|
/** This connection ends: what it held on the browser's pages ends with it. */
|
|
451
559
|
const dropConnection = (announce, ended = "dropped") => {
|
|
452
560
|
if (disconnected) return;
|
|
453
561
|
disconnected = true;
|
|
562
|
+
invalidateSnapshot();
|
|
563
|
+
snapshots.clear();
|
|
564
|
+
privateSnapshots.clear();
|
|
454
565
|
connections[index] = ended;
|
|
455
566
|
live.delete(connection);
|
|
456
567
|
for (const page of pages.values()) {
|
|
@@ -460,6 +571,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
460
571
|
notify(page);
|
|
461
572
|
}
|
|
462
573
|
if (announce) events.disconnected();
|
|
574
|
+
events.retired?.();
|
|
463
575
|
};
|
|
464
576
|
const dispatch = (ticket, record) => {
|
|
465
577
|
if (record.dispatched) return;
|
|
@@ -532,13 +644,14 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
532
644
|
if (ticket !== void 0) await applyArmed(operation, ticket, record);
|
|
533
645
|
const value = await body(record);
|
|
534
646
|
record.settled = "completed";
|
|
647
|
+
if (ticket !== void 0 && isTicket(ticket)) ticket.acknowledge?.();
|
|
535
648
|
return value;
|
|
536
649
|
} catch (error) {
|
|
537
650
|
record.settled = "failed";
|
|
538
651
|
throw error;
|
|
539
652
|
}
|
|
540
653
|
};
|
|
541
|
-
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 };
|
|
542
655
|
const facts = (control) => {
|
|
543
656
|
const { script: value } = control;
|
|
544
657
|
const extra = value.facts ?? {};
|
|
@@ -553,7 +666,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
553
666
|
editable: extra.editable ?? isEditable(value),
|
|
554
667
|
...value.inputType === void 0 ? {} : { inputType: value.inputType },
|
|
555
668
|
...extra.autocomplete === void 0 ? {} : { autocomplete: extra.autocomplete },
|
|
556
|
-
...value.destination === void 0 ? {} : { destination: value.destination },
|
|
669
|
+
...value.destination === void 0 || value.destination.length > 2048 ? {} : { destination: value.destination },
|
|
557
670
|
...extra.formMethod === void 0 ? {} : { formMethod: extra.formMethod },
|
|
558
671
|
box: ViewportRect.make(extra.box ?? {
|
|
559
672
|
x: 0,
|
|
@@ -588,33 +701,62 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
588
701
|
return control;
|
|
589
702
|
};
|
|
590
703
|
/** A retained node is only as current as the observation that produced it. */
|
|
591
|
-
const retained = (target, ticket, operation, allowSuspended = false, attached = true) => {
|
|
704
|
+
const retained = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => {
|
|
592
705
|
ticket.check();
|
|
593
706
|
const stale = () => fail(operation, Reasons.Stale.make({}));
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
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);
|
|
597
711
|
if (node === void 0) throw stale();
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
if (
|
|
601
|
-
if (selectedId !== current.pageId) throw stale();
|
|
602
|
-
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();
|
|
603
715
|
if (attached && !page.document.controls.includes(node)) throw stale();
|
|
604
716
|
return {
|
|
605
717
|
node,
|
|
606
718
|
page,
|
|
607
|
-
snapshot
|
|
719
|
+
snapshot
|
|
608
720
|
};
|
|
609
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
|
|
730
|
+
};
|
|
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
|
+
});
|
|
610
746
|
const resolve = (target, ticket, operation, policy, browserTarget, allowSuspended = false, enablement = false) => {
|
|
611
747
|
let node;
|
|
612
748
|
let page;
|
|
613
749
|
if (typeof target === "string") {
|
|
614
750
|
page = current(browserTarget, operation);
|
|
615
751
|
node = bySelector(page, target, operation);
|
|
616
|
-
} else ({node, page} =
|
|
752
|
+
} else ({node, page} = retainedElement(target, ticket, operation, allowSuspended, true, browserTarget));
|
|
617
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({}));
|
|
618
760
|
if (enablement && node.script.disabled === true) throw fail(operation, Reasons.Disabled.make({}));
|
|
619
761
|
if (policy !== void 0) {
|
|
620
762
|
let admitted = false;
|
|
@@ -626,17 +768,189 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
626
768
|
if (!admitted) throw fail(operation, Reasons.Denied.make({}));
|
|
627
769
|
}
|
|
628
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
|
+
}
|
|
629
775
|
return {
|
|
630
776
|
node,
|
|
631
777
|
page,
|
|
632
778
|
facts: fresh
|
|
633
779
|
};
|
|
634
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
|
+
};
|
|
635
949
|
const requireRunning = (page, operation) => {
|
|
636
950
|
if (page.held !== void 0) throw fail(operation, Reasons.Busy.make({}));
|
|
637
951
|
};
|
|
638
952
|
const heldObservation = (pageId) => {
|
|
639
|
-
if (snapshot
|
|
953
|
+
for (const snapshot of snapshots.values()) if (snapshot.pageId === pageId && snapshot.validity === "valid") {
|
|
640
954
|
snapshot.validity = "suspended";
|
|
641
955
|
snapshot.revalidated.clear();
|
|
642
956
|
}
|
|
@@ -661,13 +975,29 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
661
975
|
if (focused !== void 0 && isEditable(focused.script)) page.values.set(focused.script.id, (page.values.get(focused.script.id) ?? "") + text);
|
|
662
976
|
};
|
|
663
977
|
/** Issued options of one select, refused exactly where native selection refuses them. */
|
|
664
|
-
const chosen = (node, ids, operation) => {
|
|
665
|
-
const current = snapshot;
|
|
666
|
-
if (current === void 0) throw fail(operation, Reasons.Stale.make({}));
|
|
978
|
+
const chosen = (page, node, ids, ticket, operation, enablement = false) => {
|
|
667
979
|
if (!isSelectable(node.script.kind)) throw fail(operation, Reasons.Unsupported.make({}));
|
|
668
980
|
const options = ids.map((id) => {
|
|
669
|
-
|
|
670
|
-
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));
|
|
671
1001
|
return option;
|
|
672
1002
|
});
|
|
673
1003
|
if (node.script.disabled === true || options.some((option) => option.script.disabled === true)) throw fail(operation, Reasons.Disabled.make({}));
|
|
@@ -695,7 +1025,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
695
1025
|
let act;
|
|
696
1026
|
let input;
|
|
697
1027
|
if (field.options !== void 0) {
|
|
698
|
-
const options = chosen(node, field.options, "fill-form");
|
|
1028
|
+
const options = chosen(page, node, field.options, ticket, "fill-form", true);
|
|
699
1029
|
act = () => choose(page, node, options);
|
|
700
1030
|
} else if (field.checked !== void 0) {
|
|
701
1031
|
if (node.checked === void 0) throw fail("fill-form", Reasons.Unsupported.make({}));
|
|
@@ -801,6 +1131,98 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
801
1131
|
page.watchers.add(check);
|
|
802
1132
|
check();
|
|
803
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
|
+
});
|
|
804
1226
|
const invokeBinding = async (name, input, origin) => {
|
|
805
1227
|
const binding = bindings.find((candidate) => candidate.name === name);
|
|
806
1228
|
if (binding === void 0) return { ok: false };
|
|
@@ -820,6 +1242,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
820
1242
|
};
|
|
821
1243
|
try {
|
|
822
1244
|
const reply = await binding.invoke({
|
|
1245
|
+
origin: documentOrigin,
|
|
823
1246
|
read: async (signal) => {
|
|
824
1247
|
await check(signal);
|
|
825
1248
|
return text;
|
|
@@ -857,6 +1280,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
857
1280
|
select(previous !== void 0 && !previous.closed ? previous.pageId : openPages()[0]?.pageId);
|
|
858
1281
|
}
|
|
859
1282
|
const driver = {
|
|
1283
|
+
cachedPages: () => Object.freeze(openPages().map(cachedPage)),
|
|
860
1284
|
...options.pageControl === true ? { pageControl: {
|
|
861
1285
|
state: async (page, ticket) => {
|
|
862
1286
|
ticket.check();
|
|
@@ -877,6 +1301,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
877
1301
|
by: connection
|
|
878
1302
|
};
|
|
879
1303
|
heldObservation(target.pageId);
|
|
1304
|
+
ticket.acknowledge?.();
|
|
1305
|
+
events.pageLifecycle?.({
|
|
1306
|
+
_tag: "Display",
|
|
1307
|
+
page: cachedPage(target)
|
|
1308
|
+
});
|
|
880
1309
|
return PageSuspension.make({
|
|
881
1310
|
pageId: target.pageId,
|
|
882
1311
|
targetId: target.targetId,
|
|
@@ -889,6 +1318,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
889
1318
|
dispatch(ticket, record);
|
|
890
1319
|
target.held = void 0;
|
|
891
1320
|
heldObservation(target.pageId);
|
|
1321
|
+
ticket.acknowledge?.();
|
|
1322
|
+
events.pageLifecycle?.({
|
|
1323
|
+
_tag: "Display",
|
|
1324
|
+
page: cachedPage(target)
|
|
1325
|
+
});
|
|
892
1326
|
}),
|
|
893
1327
|
checkTarget: async (target, ticket) => {
|
|
894
1328
|
ticket.check();
|
|
@@ -904,6 +1338,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
904
1338
|
},
|
|
905
1339
|
selectedTargetId: async () => selectedPage().targetId,
|
|
906
1340
|
listPages: (ticket) => attempt("list-pages", ticket, {}, async () => openPages().map(info)),
|
|
1341
|
+
describePage: (page, ticket) => attempt("describe-page", ticket, { pageId: page.pageId }, async () => info(pageOf(page, "describe-page"))),
|
|
907
1342
|
resolvePage: async (page, ticket) => {
|
|
908
1343
|
ticket.check();
|
|
909
1344
|
const target = pageOf(page, "target");
|
|
@@ -929,24 +1364,20 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
929
1364
|
text: ""
|
|
930
1365
|
}), false));
|
|
931
1366
|
}),
|
|
932
|
-
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) => {
|
|
933
1368
|
const target = pageOf(page, "close-page");
|
|
934
1369
|
dispatch(ticket, record);
|
|
935
|
-
target
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
target
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if (other === connection) continue;
|
|
945
|
-
other.retire(target.pageId);
|
|
946
|
-
other.announce(target.pageId);
|
|
947
|
-
}
|
|
1370
|
+
close(target);
|
|
1371
|
+
}, onDispatch),
|
|
1372
|
+
containPage: (pageId) => attempt("close-page", void 0, { pageId }, async (record) => {
|
|
1373
|
+
const target = pages.get(pageId);
|
|
1374
|
+
if (target === void 0 || target.closed) return;
|
|
1375
|
+
const outcome = take("close-page");
|
|
1376
|
+
record.dispatched = true;
|
|
1377
|
+
if (outcome?._tag === "Fail") throw fail("close-page", outcome.reason, outcome.outcome);
|
|
1378
|
+
close(target);
|
|
948
1379
|
}),
|
|
949
|
-
listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(
|
|
1380
|
+
listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(pageOf(page, "list-frames"))]),
|
|
950
1381
|
resolveFrame: async (page, frame, ticket) => {
|
|
951
1382
|
ticket.check();
|
|
952
1383
|
const target = pageOf(page, "target");
|
|
@@ -956,11 +1387,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
956
1387
|
frameId: target.frameId
|
|
957
1388
|
};
|
|
958
1389
|
},
|
|
959
|
-
|
|
960
|
-
if (id !== selectedPage("select-frame").frameId) throw fail("select-frame", Reasons.NotFound.make({}));
|
|
961
|
-
events.invalidate("target-changed", "none");
|
|
962
|
-
}),
|
|
963
|
-
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) => {
|
|
964
1391
|
ticket.check();
|
|
965
1392
|
const page = current(target, "navigate");
|
|
966
1393
|
requireRunning(page, "navigate");
|
|
@@ -1023,7 +1450,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1023
1450
|
};
|
|
1024
1451
|
}),
|
|
1025
1452
|
readText: (selector, maximumBytes, ticket, target) => attempt("read-text", ticket, {
|
|
1026
|
-
pageId: target
|
|
1453
|
+
pageId: target.pageId,
|
|
1027
1454
|
...meta(selector)
|
|
1028
1455
|
}, async () => {
|
|
1029
1456
|
const page = current(target, "read-text");
|
|
@@ -1031,11 +1458,12 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1031
1458
|
const control = bySelector(page, selector, "read-text");
|
|
1032
1459
|
return bounded(control.script.text ?? control.script.label, maximumBytes).text;
|
|
1033
1460
|
}),
|
|
1034
|
-
observe: (scope, maximumBytes, controls, ticket, match) => attempt("observe", ticket, {}, async () => {
|
|
1035
|
-
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;
|
|
1036
1464
|
const needle = match?.toLowerCase();
|
|
1037
1465
|
const matches = (value) => needle === void 0 || value.toLowerCase().includes(needle);
|
|
1038
|
-
|
|
1466
|
+
snapshots.delete(page.frameId);
|
|
1039
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));
|
|
1040
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));
|
|
1041
1469
|
const reachable = matching.filter((control) => scope === "document" || control.script.offscreen !== true);
|
|
@@ -1044,13 +1472,22 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1044
1472
|
const next = {
|
|
1045
1473
|
id: `observation-${++observationSerial}`,
|
|
1046
1474
|
pageId: page.pageId,
|
|
1475
|
+
frameId: page.frameId,
|
|
1047
1476
|
epoch: page.epoch,
|
|
1048
1477
|
generation: ticket.generation,
|
|
1478
|
+
scope,
|
|
1049
1479
|
validity: "valid",
|
|
1050
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
|
+
})),
|
|
1051
1488
|
revalidated: /* @__PURE__ */ new Set()
|
|
1052
1489
|
};
|
|
1053
|
-
|
|
1490
|
+
snapshots.set(page.frameId, next);
|
|
1054
1491
|
return {
|
|
1055
1492
|
observationId: next.id,
|
|
1056
1493
|
scope,
|
|
@@ -1061,8 +1498,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1061
1498
|
controls: kept.map(observed),
|
|
1062
1499
|
controlsTruncated: kept.length < reachable.length,
|
|
1063
1500
|
viewport: ViewportEvidence.make({
|
|
1064
|
-
width:
|
|
1065
|
-
height:
|
|
1501
|
+
width: size.width,
|
|
1502
|
+
height: size.height,
|
|
1503
|
+
...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
|
|
1066
1504
|
clippedText: 0,
|
|
1067
1505
|
coveredText: 0,
|
|
1068
1506
|
uncertainText: 0,
|
|
@@ -1071,8 +1509,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1071
1509
|
})
|
|
1072
1510
|
};
|
|
1073
1511
|
}),
|
|
1074
|
-
checkpoint: (maximumBytes, controls, pictureBytes, ticket) => attempt("checkpoint", ticket, {}, async () => {
|
|
1075
|
-
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;
|
|
1076
1515
|
const text = bounded(page.document.text, maximumBytes);
|
|
1077
1516
|
const reachable = page.document.controls.filter((control) => control.script.offscreen !== true);
|
|
1078
1517
|
return {
|
|
@@ -1082,8 +1521,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1082
1521
|
controls: reachable.slice(0, controls).map(facts),
|
|
1083
1522
|
controlsTruncated: reachable.length > controls,
|
|
1084
1523
|
viewport: ViewportEvidence.make({
|
|
1085
|
-
width:
|
|
1086
|
-
height:
|
|
1524
|
+
width: size.width,
|
|
1525
|
+
height: size.height,
|
|
1526
|
+
...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
|
|
1087
1527
|
clippedText: 0,
|
|
1088
1528
|
coveredText: 0,
|
|
1089
1529
|
uncertainText: 0,
|
|
@@ -1091,19 +1531,53 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1091
1531
|
exhausted: false
|
|
1092
1532
|
}),
|
|
1093
1533
|
documentChanged: false,
|
|
1094
|
-
...pictureBytes === void 0 ? {} : { picture:
|
|
1534
|
+
...pictureBytes === void 0 ? {} : { picture: picture(page, ticket) }
|
|
1095
1535
|
};
|
|
1096
1536
|
}),
|
|
1097
|
-
controlFacts: (target, ticket) => attempt("control-facts", ticket,
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
+
}
|
|
1100
1569
|
}),
|
|
1101
|
-
|
|
1102
|
-
|
|
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);
|
|
1103
1577
|
current.revalidated.add(target.elementId);
|
|
1104
1578
|
}),
|
|
1105
1579
|
click: (target, ticket, capture, policy, browserTarget) => attempt("click", ticket, {
|
|
1106
|
-
pageId: browserTarget
|
|
1580
|
+
pageId: browserTarget.pageId,
|
|
1107
1581
|
...meta(target)
|
|
1108
1582
|
}, async (record) => {
|
|
1109
1583
|
const { node, page } = resolve(target, ticket, "click", policy, browserTarget);
|
|
@@ -1117,9 +1591,9 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1117
1591
|
url,
|
|
1118
1592
|
input
|
|
1119
1593
|
};
|
|
1120
|
-
}, () => invalidatePointer(browserTarget
|
|
1594
|
+
}, () => invalidatePointer(browserTarget.pageId)),
|
|
1121
1595
|
fill: (target, value, ticket, policy, browserTarget) => attempt("fill", ticket, {
|
|
1122
|
-
pageId: browserTarget
|
|
1596
|
+
pageId: browserTarget.pageId,
|
|
1123
1597
|
...meta(target)
|
|
1124
1598
|
}, async (record) => {
|
|
1125
1599
|
const { node, page } = resolve(target, ticket, "fill", policy, browserTarget);
|
|
@@ -1131,25 +1605,34 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1131
1605
|
page.values.set(node.script.id, value);
|
|
1132
1606
|
return page.document.url;
|
|
1133
1607
|
}),
|
|
1134
|
-
selectOption: (target, ids, ticket, policy) => attempt("select-option", ticket,
|
|
1135
|
-
|
|
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);
|
|
1136
1613
|
requireRunning(page, "select-option");
|
|
1137
|
-
const options = chosen(node, ids, "select-option");
|
|
1614
|
+
const options = chosen(page, node, ids, ticket, "select-option");
|
|
1138
1615
|
dispatch(ticket, record);
|
|
1139
1616
|
choose(page, node, options);
|
|
1140
1617
|
return page.document.url;
|
|
1141
1618
|
}),
|
|
1142
|
-
formStep: (target, field, ticket, policy, _settleMillis, capture) => attempt("fill-form", ticket,
|
|
1143
|
-
|
|
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);
|
|
1144
1624
|
requireRunning(page, "fill-form");
|
|
1145
1625
|
return formStep(page, node, field, ticket, record, capture);
|
|
1146
|
-
}, field.checked === void 0 ? void 0 : () => invalidatePointer(
|
|
1147
|
-
formState: (targets, ticket) => attempt("fill-form", ticket, {}, async () => targets.map((target) => {
|
|
1148
|
-
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);
|
|
1149
1629
|
return page.document.controls.includes(node) ? fieldState(page, node) : void 0;
|
|
1150
1630
|
})),
|
|
1151
|
-
formSubmit: (target, ticket, capture, policy) => attempt("fill-form", ticket,
|
|
1152
|
-
|
|
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);
|
|
1153
1636
|
requireRunning(page, "fill-form");
|
|
1154
1637
|
dispatch(ticket, record);
|
|
1155
1638
|
let url = "";
|
|
@@ -1160,14 +1643,29 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1160
1643
|
url,
|
|
1161
1644
|
input
|
|
1162
1645
|
};
|
|
1163
|
-
}, () => invalidatePointer(
|
|
1164
|
-
scroll: (
|
|
1646
|
+
}, () => invalidatePointer(browserTarget.pageId)),
|
|
1647
|
+
scroll: (deltaX, deltaY, ticket, target) => attempt("scroll", ticket, { pageId: target.pageId }, async (record) => {
|
|
1165
1648
|
const page = current(target, "scroll");
|
|
1166
1649
|
requireRunning(page, "scroll");
|
|
1167
1650
|
dispatch(ticket, record);
|
|
1651
|
+
page.scroll.x += deltaX;
|
|
1652
|
+
page.scroll.y += deltaY;
|
|
1653
|
+
notify(page);
|
|
1654
|
+
return page.document.url;
|
|
1655
|
+
}),
|
|
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);
|
|
1168
1666
|
return page.document.url;
|
|
1169
1667
|
}),
|
|
1170
|
-
pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target
|
|
1668
|
+
pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target.pageId }, async (record) => {
|
|
1171
1669
|
const page = current(target, "pointer-move");
|
|
1172
1670
|
requireRunning(page, "pointer-move");
|
|
1173
1671
|
dispatch(ticket, record);
|
|
@@ -1178,7 +1676,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1178
1676
|
return receipt(page.pageId);
|
|
1179
1677
|
}),
|
|
1180
1678
|
hover: (target, ticket, policy, browserTarget) => attempt("hover", ticket, {
|
|
1181
|
-
pageId: browserTarget
|
|
1679
|
+
pageId: browserTarget.pageId,
|
|
1182
1680
|
...meta(target)
|
|
1183
1681
|
}, async (record) => {
|
|
1184
1682
|
const { node, page, facts: fresh } = resolve(target, ticket, "hover", policy, browserTarget);
|
|
@@ -1191,7 +1689,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1191
1689
|
});
|
|
1192
1690
|
return receipt(page.pageId);
|
|
1193
1691
|
}),
|
|
1194
|
-
wheel: (
|
|
1692
|
+
wheel: (deltaX, deltaY, at, ticket, target) => attempt("wheel", ticket, { pageId: target.pageId }, async (record) => {
|
|
1195
1693
|
const page = current(target, "wheel");
|
|
1196
1694
|
requireRunning(page, "wheel");
|
|
1197
1695
|
dispatch(ticket, record);
|
|
@@ -1199,10 +1697,13 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1199
1697
|
x: at.x,
|
|
1200
1698
|
y: at.y
|
|
1201
1699
|
});
|
|
1700
|
+
page.scroll.x += deltaX;
|
|
1701
|
+
page.scroll.y += deltaY;
|
|
1702
|
+
notify(page);
|
|
1202
1703
|
return receipt(page.pageId);
|
|
1203
1704
|
}),
|
|
1204
1705
|
press: (key, _modifiers, into, ticket, policy, browserTarget) => attempt("press", ticket, {
|
|
1205
|
-
pageId: browserTarget
|
|
1706
|
+
pageId: browserTarget.pageId,
|
|
1206
1707
|
...meta(into)
|
|
1207
1708
|
}, async (record) => {
|
|
1208
1709
|
const page = focusedOrRefuse(into, ticket, "press", policy, browserTarget);
|
|
@@ -1212,7 +1713,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1212
1713
|
return receipt(page.pageId);
|
|
1213
1714
|
}),
|
|
1214
1715
|
type: (text, into, ticket, policy, browserTarget) => attempt("type", ticket, {
|
|
1215
|
-
pageId: browserTarget
|
|
1716
|
+
pageId: browserTarget.pageId,
|
|
1216
1717
|
...meta(into)
|
|
1217
1718
|
}, async (record) => {
|
|
1218
1719
|
const page = focusedOrRefuse(into, ticket, "type", policy, browserTarget);
|
|
@@ -1221,19 +1722,19 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1221
1722
|
typed(page, text);
|
|
1222
1723
|
return receipt(page.pageId);
|
|
1223
1724
|
}),
|
|
1224
|
-
screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target
|
|
1225
|
-
current(target, "screenshot");
|
|
1226
|
-
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);
|
|
1227
1728
|
}),
|
|
1228
|
-
resize: (next, ticket) => attempt("resize", ticket, {}, async (record) => {
|
|
1229
|
-
const page =
|
|
1729
|
+
resize: (next, ticket, target) => attempt("resize", ticket, { pageId: target.pageId }, async (record) => {
|
|
1730
|
+
const page = current(target, "resize");
|
|
1230
1731
|
dispatch(ticket, record);
|
|
1231
|
-
viewport = {
|
|
1732
|
+
page.viewport = {
|
|
1232
1733
|
width: next.width,
|
|
1233
1734
|
height: next.height
|
|
1234
1735
|
};
|
|
1235
1736
|
page.capture?.start.invalidate("resized");
|
|
1236
|
-
events.invalidate("resized");
|
|
1737
|
+
events.invalidate("resized", { pageId: page.pageId });
|
|
1237
1738
|
}),
|
|
1238
1739
|
waitFor: (selector, state, ticket, target) => attempt("wait", ticket, {
|
|
1239
1740
|
pageId: target.pageId,
|
|
@@ -1257,12 +1758,17 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1257
1758
|
}
|
|
1258
1759
|
}, () => page.closed || page.epoch !== epoch);
|
|
1259
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()),
|
|
1260
1766
|
waitForElement: (reference, state, ticket, target) => attempt("wait", ticket, {
|
|
1261
1767
|
pageId: target.pageId,
|
|
1262
|
-
|
|
1768
|
+
...meta(reference)
|
|
1263
1769
|
}, async (record) => {
|
|
1264
1770
|
const page = current(target, "wait");
|
|
1265
|
-
const { node } =
|
|
1771
|
+
const { node } = retainedElement(reference, ticket, "wait", true, true, target);
|
|
1266
1772
|
const epoch = page.epoch;
|
|
1267
1773
|
await waitUntil(ticket, page, "wait", record, () => {
|
|
1268
1774
|
const present = page.document.controls.includes(node);
|
|
@@ -1275,8 +1781,11 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1275
1781
|
}
|
|
1276
1782
|
}, () => page.closed || page.epoch !== epoch);
|
|
1277
1783
|
}).finally(() => ticket.retire()),
|
|
1278
|
-
clickAndWait: (target, ticket, capture) => attempt("click-and-wait", ticket,
|
|
1279
|
-
|
|
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);
|
|
1280
1789
|
requireRunning(page, "click-and-wait");
|
|
1281
1790
|
dispatch(ticket, record);
|
|
1282
1791
|
let url = "";
|
|
@@ -1287,9 +1796,12 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1287
1796
|
url,
|
|
1288
1797
|
input
|
|
1289
1798
|
};
|
|
1290
|
-
}, () => invalidatePointer(
|
|
1291
|
-
clickForDownload: (target, ticket) => attempt("download-action", ticket,
|
|
1292
|
-
|
|
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);
|
|
1293
1805
|
requireRunning(page, "download-action");
|
|
1294
1806
|
if (node.script.download === void 0) throw fail("download-action", Reasons.Unsupported.make({}));
|
|
1295
1807
|
dispatch(ticket, record);
|
|
@@ -1300,8 +1812,14 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1300
1812
|
state: "completed"
|
|
1301
1813
|
};
|
|
1302
1814
|
}),
|
|
1303
|
-
selectFiles: (target, files, ticket) => attempt("select-files", ticket,
|
|
1304
|
-
|
|
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)),
|
|
1305
1823
|
documentReadiness: async (ticket, target) => {
|
|
1306
1824
|
ticket.check();
|
|
1307
1825
|
const page = current(target, "ready");
|
|
@@ -1313,7 +1831,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1313
1831
|
},
|
|
1314
1832
|
dismissDialogs: async () => {},
|
|
1315
1833
|
capture: async (target) => {
|
|
1316
|
-
const page =
|
|
1834
|
+
const page = pageOf(PageInfo.make({
|
|
1317
1835
|
pageId: target.pageId,
|
|
1318
1836
|
targetId: target.targetId,
|
|
1319
1837
|
url: "",
|
|
@@ -1341,8 +1859,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1341
1859
|
}
|
|
1342
1860
|
};
|
|
1343
1861
|
},
|
|
1344
|
-
invalidateObservation: (scope) => {
|
|
1345
|
-
invalidateSnapshot(scope);
|
|
1862
|
+
invalidateObservation: (scope, origin) => {
|
|
1863
|
+
invalidateSnapshot(scope, origin);
|
|
1346
1864
|
},
|
|
1347
1865
|
disconnect: async () => {
|
|
1348
1866
|
if (disconnected) return;
|
|
@@ -1359,8 +1877,8 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1359
1877
|
dropConnection(false, "closed");
|
|
1360
1878
|
}
|
|
1361
1879
|
};
|
|
1362
|
-
const selectFilesOn = (target, files, ticket, record, operation) => {
|
|
1363
|
-
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);
|
|
1364
1882
|
requireRunning(page, operation);
|
|
1365
1883
|
if (node.script.kind !== "input" || node.script.inputType !== "file") throw fail(operation, Reasons.Unsupported.make({}));
|
|
1366
1884
|
dispatch(ticket, record);
|
|
@@ -1368,12 +1886,43 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1368
1886
|
return page.document.url;
|
|
1369
1887
|
};
|
|
1370
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
|
+
},
|
|
1371
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
|
+
},
|
|
1372
1921
|
announce: (pageId) => events.invalidate("target-changed", { pageId }),
|
|
1373
1922
|
drop: () => dropConnection(true),
|
|
1374
1923
|
invoke: invokeBinding,
|
|
1375
1924
|
pointer: () => pointer,
|
|
1376
|
-
viewport: () => viewport
|
|
1925
|
+
viewport: (pageId) => pages.get(pageId)?.viewport ?? viewport
|
|
1377
1926
|
};
|
|
1378
1927
|
index = connections.push("open") - 1;
|
|
1379
1928
|
live.add(connection);
|
|
@@ -1384,6 +1933,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1384
1933
|
url: document.url,
|
|
1385
1934
|
title: document.title,
|
|
1386
1935
|
text: document.text,
|
|
1936
|
+
...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
|
|
1387
1937
|
controls: document.controls.map((control) => ({
|
|
1388
1938
|
...control.script,
|
|
1389
1939
|
...control.checked === void 0 ? {} : { checked: control.checked },
|
|
@@ -1422,7 +1972,7 @@ const makeScriptedBrowser = (script, timers) => {
|
|
|
1422
1972
|
const running = page.capture?.start;
|
|
1423
1973
|
if (running === void 0) return Effect.fail(fail("capture-consume", Reasons.NotFound.make({})));
|
|
1424
1974
|
const timestamp = frame.timestamp ?? lastTimestamp + 40;
|
|
1425
|
-
const viewport = page.capture?.by.viewport() ?? {
|
|
1975
|
+
const viewport = page.capture?.by.viewport(page.pageId) ?? {
|
|
1426
1976
|
width: 0,
|
|
1427
1977
|
height: 0
|
|
1428
1978
|
};
|
|
@@ -1535,23 +2085,24 @@ const makeEngine = Effect.fnUntraced(function* (script) {
|
|
|
1535
2085
|
* Open one scripted browser in the caller's Scope. The session is the real owner over a
|
|
1536
2086
|
* scripted native engine: admission, budgets, staleness, dispatch evidence, capture, page holds
|
|
1537
2087
|
* and typed callbacks are the production code paths. Only the page and its outcomes are scripted,
|
|
1538
|
-
* 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.
|
|
1539
2090
|
*/
|
|
1540
2091
|
const open = Effect.fnUntraced(function* (script, options = {}) {
|
|
1541
2092
|
const fixed = yield* checked(Script, script, "configure");
|
|
1542
2093
|
const policy = yield* checked(BrowserPolicy, options.policy ?? BrowserPolicy.unrestricted(), "configure");
|
|
1543
2094
|
const engine = yield* makeEngine(fixed);
|
|
1544
|
-
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({
|
|
1545
2101
|
implementation: "scripted",
|
|
1546
2102
|
binding: engine.binding,
|
|
1547
2103
|
...options.automation === void 0 ? {} : { automation: options.automation },
|
|
1548
2104
|
...options.viewport === void 0 ? {} : { viewport: options.viewport }
|
|
1549
|
-
}).pipe(Effect.provide(
|
|
1550
|
-
const reference = Object.freeze(ScriptedReference.make({
|
|
1551
|
-
provider: "scripted",
|
|
1552
|
-
id: `scripted-${++references}`
|
|
1553
|
-
}));
|
|
1554
|
-
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 });
|
|
1555
2106
|
const connection = yield* acquired.connect;
|
|
1556
2107
|
const control = engine.controls().at(-1);
|
|
1557
2108
|
if (control === void 0) return yield* BrowserError.make({
|
|
@@ -1570,7 +2121,8 @@ const open = Effect.fnUntraced(function* (script, options = {}) {
|
|
|
1570
2121
|
/**
|
|
1571
2122
|
* The `Crypto` scripted browsers draw their ids and handoff tokens from. Its bytes count up from
|
|
1572
2123
|
* one on each build, so every value is predictable; it is not random and computes no digest.
|
|
1573
|
-
* `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
|
|
1574
2126
|
* this one keeps such a test free of a platform package.
|
|
1575
2127
|
*/
|
|
1576
2128
|
const sequentialCrypto = Layer.sync(Crypto.Crypto, makeSequentialCrypto);
|