effect-browser 0.2.0-beta.9 → 0.3.0-beta.10

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.
Files changed (204) hide show
  1. package/README.md +849 -1161
  2. package/dist/Browser-S4kvpmqa.mjs +605 -0
  3. package/dist/Browser-S4kvpmqa.mjs.map +1 -0
  4. package/dist/Browser.d.mts +127 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +211 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +297 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-iMlcuzVD.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-BERmf_gM.d.mts +117 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +123 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-B5cjaUp5.mjs +618 -0
  39. package/dist/Page-B5cjaUp5.mjs.map +1 -0
  40. package/dist/Page-BlPjfibv.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-BI8XGvDu.d.mts +209 -0
  44. package/dist/Plan-D1wrJVs8.mjs +508 -0
  45. package/dist/Plan-D1wrJVs8.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +64 -0
  59. package/dist/Stage.mjs +237 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
  62. package/dist/Supervisor-TNKNOVzu.mjs +599 -0
  63. package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-B6xr8nVi.mjs +261 -0
  67. package/dist/context-B6xr8nVi.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-B8tUQELg.mjs +5073 -0
  73. package/dist/page-B8tUQELg.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-Cd04Okid.mjs +264 -0
  76. package/dist/url-Cd04Okid.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +666 -0
  79. package/src/BrowserError.ts +234 -0
  80. package/src/BrowserEvent.ts +296 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +130 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +522 -0
  89. package/src/Plan.ts +663 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +467 -0
  94. package/src/Supervisor.ts +599 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +610 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/gate.ts +224 -0
  101. package/src/internal/input/guard.inpage.ts +605 -0
  102. package/src/internal/input/guard.ts +184 -0
  103. package/src/internal/input/keyboard.ts +112 -0
  104. package/src/internal/input/keys.ts +127 -0
  105. package/src/internal/input/perform.ts +376 -0
  106. package/src/internal/input/pointer.ts +153 -0
  107. package/src/internal/input/replies.ts +195 -0
  108. package/src/internal/input/settle.ts +50 -0
  109. package/src/internal/input/style.ts +186 -0
  110. package/src/internal/input/targets.inpage.ts +293 -0
  111. package/src/internal/input/targets.ts +148 -0
  112. package/src/internal/page/bridge.ts +386 -0
  113. package/src/internal/page/context.ts +312 -0
  114. package/src/internal/page/lane.ts +311 -0
  115. package/src/internal/page/navigation.ts +183 -0
  116. package/src/internal/page/openings.ts +87 -0
  117. package/src/internal/page/page.ts +165 -0
  118. package/src/internal/page/url.ts +100 -0
  119. package/src/internal/page/viewport.ts +43 -0
  120. package/src/internal/pictures/capture.ts +776 -0
  121. package/src/internal/pictures/clock.ts +141 -0
  122. package/src/internal/pictures/counts.ts +92 -0
  123. package/src/internal/pictures/pictures.ts +520 -0
  124. package/src/internal/pictures/transport.ts +242 -0
  125. package/src/internal/reading/choose.ts +100 -0
  126. package/src/internal/reading/context.inpage.ts +201 -0
  127. package/src/internal/reading/controls.inpage.ts +158 -0
  128. package/src/internal/reading/expect.inpage.ts +70 -0
  129. package/src/internal/reading/match.inpage.ts +92 -0
  130. package/src/internal/reading/names.inpage.ts +306 -0
  131. package/src/internal/reading/outline.inpage.ts +260 -0
  132. package/src/internal/reading/reading.ts +223 -0
  133. package/src/internal/reading/ready.inpage.ts +164 -0
  134. package/src/internal/reading/ready.ts +111 -0
  135. package/src/internal/reading/subjects.inpage.ts +188 -0
  136. package/src/internal/reading/text.inpage.ts +135 -0
  137. package/src/internal/reading/walk.inpage.ts +220 -0
  138. package/src/internal/supervisor/lifecycle.ts +416 -0
  139. package/src/internal/timeline/changes.inpage.ts +216 -0
  140. package/src/internal/timeline/changes.ts +100 -0
  141. package/src/internal/timeline/events.ts +94 -0
  142. package/src/internal/timeline/fold.inpage.ts +123 -0
  143. package/src/internal/timeline/history.inpage.ts +229 -0
  144. package/src/internal/timeline/marks.inpage.ts +207 -0
  145. package/src/internal/timeline/record.inpage.ts +133 -0
  146. package/src/internal/timeline/sight.inpage.ts +122 -0
  147. package/src/internal/timeline/window.ts +96 -0
  148. package/src/internal/timeline/words.ts +65 -0
  149. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  150. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  151. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  152. package/dist/Bootstrap.d.mts +0 -3
  153. package/dist/Bootstrap.mjs +0 -2
  154. package/dist/Browser-BF9_sORP.d.mts +0 -388
  155. package/dist/Browser.mjs.map +0 -1
  156. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  157. package/dist/BrowserData.d.mts +0 -3
  158. package/dist/BrowserData.mjs +0 -695
  159. package/dist/BrowserData.mjs.map +0 -1
  160. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  161. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  163. package/dist/BrowserRuntime.d.mts +0 -3
  164. package/dist/BrowserRuntime.mjs +0 -3
  165. package/dist/Capture.d.mts +0 -56
  166. package/dist/Capture.mjs +0 -678
  167. package/dist/Capture.mjs.map +0 -1
  168. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  169. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  170. package/dist/CaptureData.d.mts +0 -219
  171. package/dist/CaptureData.mjs +0 -2
  172. package/dist/Errors.d.mts +0 -158
  173. package/dist/Errors.mjs +0 -207
  174. package/dist/Errors.mjs.map +0 -1
  175. package/dist/Images-yCG9BmRU.mjs +0 -76
  176. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  177. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  178. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  179. package/dist/PageControl.d.mts +0 -15
  180. package/dist/PageControl.mjs +0 -34
  181. package/dist/PageControl.mjs.map +0 -1
  182. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  183. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  184. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  185. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  186. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  187. package/dist/PlanData.d.mts +0 -3
  188. package/dist/PlanData.mjs +0 -268
  189. package/dist/PlanData.mjs.map +0 -1
  190. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  191. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  192. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  193. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  194. package/dist/Testing.d.mts +0 -479
  195. package/dist/Testing.mjs +0 -2147
  196. package/dist/Testing.mjs.map +0 -1
  197. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  198. package/dist/Timeline.d.mts +0 -4
  199. package/dist/Timeline.mjs +0 -20
  200. package/dist/Timeline.mjs.map +0 -1
  201. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  202. package/dist/TimelineData.d.mts +0 -3
  203. package/dist/TimelineData.mjs +0 -482
  204. package/dist/TimelineData.mjs.map +0 -1
package/dist/Testing.mjs DELETED
@@ -1,2147 +0,0 @@
1
- import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
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-C1r2aSRK.mjs";
4
- import { l as reported, s as cleanupStep, t as checked } from "./PublicSession-DCSMMqOj.mjs";
5
- import { n as pngGeometry } from "./Images-yCG9BmRU.mjs";
6
- import { Crypto, Effect, Layer, Option, PlatformError, Redacted, Schema } from "effect";
7
- //#region src/internal/testing/Crypto.ts
8
- /**
9
- * A `Crypto` whose bytes count up from one, so every id and handoff token a scripted browser draws
10
- * is predictable. A namespace occupies UUID bytes untouched by its version/variant bits.
11
- * It is not random and computes no digest.
12
- */
13
- const makeSequentialCrypto = (namespace = 0) => {
14
- let draws = 0;
15
- return Crypto.make({
16
- randomBytes: (size) => {
17
- const bytes = new Uint8Array(size);
18
- let rest = ++draws;
19
- for (let index = size - 1; index >= 0 && rest > 0; index--) {
20
- bytes[index] = rest % 256;
21
- rest = Math.floor(rest / 256);
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
- }
29
- return bytes;
30
- },
31
- digest: () => Effect.fail(PlatformError.badArgument({
32
- module: "Crypto",
33
- method: "digest",
34
- description: "A scripted browser's Crypto computes no digest"
35
- }))
36
- });
37
- };
38
- //#endregion
39
- //#region src/internal/testing/Frame.ts
40
- /** A valid 64×48 baseline JPEG, the bytes a scripted capture frame carries by default. */
41
- const jpegFrame = () => Uint8Array.from(atob("/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wAARCAAwAEADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDyVLf2qdLf2q6lv7VOlv7UoyMadcpJb+1Tpb+1XUt/ap0t/atoyPSp1yklv7VYS39qupb+1Tpb+1bxkejTrlJLf2qdLf2q6lv7VOlv7VtGR6NOuYKW/tU6W/tV1Lf2qwlv7V4kZH5fTrlFLf2qwlv7VdS39qnS39q2jI9GnXKSW/tU6W/tV1Lf2qdLf2raMj0adcpJb+1Tpb+1Xkt/ap0t/at4yPRp1zBS39qnS39qupb+1Tpb+1eHGR+X065SS39qnS39qupb+1Tpb+1bxkejTrlJLf2qwlv7VdS39qnS39q2jI9GnXKSW/tU6W/tV1Lf2qdLf2raMj0adc//2Q=="), (character) => character.charCodeAt(0));
42
- //#endregion
43
- //#region src/internal/testing/Script.ts
44
- const uniqueBy = (key, title) => Schema.makeFilter((items) => new Set(items.map(key)).size === items.length, { title });
45
- /** Host-only facts a scripted control reports beyond what its observed fields imply. */
46
- const ControlFactsScript = Schema.Struct({
47
- editable: Schema.optionalKey(Schema.Boolean),
48
- formMethod: Schema.optionalKey(Schema.Literals([
49
- "get",
50
- "post",
51
- "dialog"
52
- ])),
53
- hitTest: Schema.optionalKey(Schema.Literals([
54
- "self",
55
- "covered",
56
- "uncertain",
57
- "unsampled"
58
- ])),
59
- placement: Schema.optionalKey(Schema.Literals([
60
- "inside",
61
- "partial",
62
- "outside"
63
- ])),
64
- autocomplete: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
65
- box: Schema.optionalKey(Schema.Struct({
66
- x: Schema.Finite,
67
- y: Schema.Finite,
68
- width: Schema.Finite,
69
- height: Schema.Finite
70
- }))
71
- });
72
- /**
73
- * One control a scripted document offers. `id` becomes the issued `elementId`, so a scripted
74
- * model turn can name it statically. Nothing here is a live capability.
75
- */
76
- const ControlScript = Schema.Struct({
77
- id: Identifier,
78
- kind: ObservedControl.fields.kind,
79
- label: ObservedControl.fields.label,
80
- disabled: Schema.optionalKey(Schema.Boolean),
81
- /** A checkbox or radio `input` without it starts unchecked, as a real one does. */
82
- checked: Schema.optionalKey(Schema.Boolean),
83
- selected: Schema.optionalKey(Schema.Boolean),
84
- inputType: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(32))),
85
- required: Schema.optionalKey(Schema.Boolean),
86
- multiple: Schema.optionalKey(Schema.Boolean),
87
- /** The scripted select this option belongs to; it must be in the same document. */
88
- selectElementId: Schema.optionalKey(Identifier),
89
- /**
90
- * The link target, or the form action a submit control sends to: reported as the control's
91
- * destination fact, and where activating it leads. Only a `link`, or a `button`/`input` whose
92
- * `inputType` is `submit` or `image`, has one, because that is all a real document reports.
93
- */
94
- destination: Schema.optionalKey(TargetUrl),
95
- /** Where activation leads with no destination fact: what an `onclick` handler does. */
96
- activates: Schema.optionalKey(TargetUrl),
97
- /** Outside the viewport: absent from viewport readings and refused by hover. */
98
- offscreen: Schema.optionalKey(Schema.Boolean),
99
- /** What `readText({ selector: "#id" })` returns; defaults to the label. */
100
- text: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(65536))),
101
- /** Clicking for a download completes one with this name. */
102
- download: Schema.optionalKey(SafeFilename),
103
- facts: Schema.optionalKey(ControlFactsScript)
104
- }).check(Schema.makeFilter((control) => control.destination === void 0 || control.kind === "link" || (control.kind === "button" || control.kind === "input") && (control.inputType === "submit" || control.inputType === "image"), { title: "a destination only on a link or a submit control" }));
105
- const DocumentScript = Schema.Struct({
106
- url: TargetUrl,
107
- title: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(512))),
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),
114
- controls: Schema.optionalKey(Schema.Array(ControlScript).check(Schema.isMaxLength(64), uniqueBy((control) => control.id, "unique control ids")))
115
- });
116
- /**
117
- * The documents a scripted browser can show. A navigation to a listed address reaches that
118
- * document; an unlisted address reaches an empty document with that address.
119
- */
120
- const Script = Schema.Struct({
121
- documents: Schema.Array(DocumentScript).check(Schema.isMinLength(1), Schema.isMaxLength(64), uniqueBy((document) => document.url, "unique document addresses")),
122
- /** The first page's document at connect; defaults to the first listed document. */
123
- start: Schema.optionalKey(TargetUrl),
124
- /**
125
- * What successive connection attempts to one browser do: `refuse` fails the attempt as an
126
- * unreachable endpoint would, and `accept` connects. Attempts past the list connect.
127
- */
128
- connections: Schema.optionalKey(Schema.Array(Schema.Literals(["accept", "refuse"])).check(Schema.isMaxLength(16)))
129
- });
130
- var ScriptedReference = class extends Schema.Class("ScriptedBrowserReference")({
131
- provider: Schema.Literal("scripted"),
132
- id: Identifier
133
- }) {};
134
- var ScriptedCleanupIssue = class extends Schema.Class("ScriptedCleanupIssue")({
135
- step: Schema.Literals([
136
- "fence",
137
- "capture",
138
- "initialization",
139
- "disconnect"
140
- ]),
141
- reason: Schema.Literals([
142
- "timeout",
143
- "failed",
144
- "interrupted"
145
- ])
146
- }) {};
147
- /** Connection teardown facts for a scripted browser; there is no process or provider status. */
148
- var ScriptedCleanupResult = class extends Schema.Class("ScriptedCleanupResult")({
149
- reference: ScriptedReference,
150
- connection: Schema.Literals([
151
- "closed",
152
- "failed",
153
- "not-connected",
154
- "pending"
155
- ]),
156
- issues: Schema.Array(ScriptedCleanupIssue).check(Schema.isMaxLength(16))
157
- }) {};
158
- /**
159
- * The operations whose next call a test may script. `disconnect` is the owner's native teardown
160
- * of a connection: a `Fail` makes it fail and a `Hold` parks it, however long cleanup waits.
161
- */
162
- const ScriptableOperation = Schema.Literals([
163
- "navigate",
164
- "navigate-stop",
165
- "observe",
166
- "checkpoint",
167
- "control-facts",
168
- "revalidate",
169
- "read-text",
170
- "screenshot",
171
- "wait",
172
- "resolve",
173
- "run",
174
- "settled",
175
- "click",
176
- "fill",
177
- "fill-form",
178
- "select-option",
179
- "scroll",
180
- "click-and-wait",
181
- "download-action",
182
- "select-files",
183
- "file-chooser",
184
- "pointer-move",
185
- "hover",
186
- "wheel",
187
- "press",
188
- "type",
189
- "list-pages",
190
- "describe-page",
191
- "list-frames",
192
- "select-page",
193
- "new-page",
194
- "close-page",
195
- "resize",
196
- "page-suspend",
197
- "page-resume",
198
- "capture-stop",
199
- "disconnect"
200
- ]);
201
- //#endregion
202
- //#region src/internal/testing/Engine.ts
203
- const gates = /* @__PURE__ */ new WeakMap();
204
- /** A gate that holds a scripted call until the test opens it. */
205
- const makeGate = Effect.sync(() => {
206
- let reach = () => {};
207
- let open = () => {};
208
- const reached = new Promise((resolve) => {
209
- reach = resolve;
210
- });
211
- const opened = new Promise((resolve) => {
212
- open = resolve;
213
- });
214
- const gate = {
215
- reached: Effect.promise(() => reached),
216
- open: Effect.sync(() => {
217
- open();
218
- })
219
- };
220
- gates.set(gate, {
221
- reach,
222
- opened
223
- });
224
- return gate;
225
- });
226
- /** A 1×1 PNG: what a scripted screenshot or checkpoint picture contains. */
227
- const PNG = Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="), (character) => character.charCodeAt(0));
228
- const fail = (operation, reason, outcome = "undispatched") => BrowserError.make({
229
- operation,
230
- reason,
231
- outcome
232
- });
233
- const encoder = new TextEncoder();
234
- /** Cut at a character boundary so the bounded text is still valid UTF-16. */
235
- const bounded = (text, maximumBytes) => {
236
- if (encoder.encode(text).length <= maximumBytes) return {
237
- text,
238
- truncated: false
239
- };
240
- let kept = "";
241
- let bytes = 0;
242
- for (const character of text) {
243
- const size = encoder.encode(character).length;
244
- if (bytes + size > maximumBytes) break;
245
- kept += character;
246
- bytes += size;
247
- }
248
- return {
249
- text: kept,
250
- truncated: true
251
- };
252
- };
253
- const originOf = (url) => {
254
- try {
255
- return new URL(url).origin;
256
- } catch {
257
- return "null";
258
- }
259
- };
260
- const isSelectable = (kind) => kind === "select";
261
- const isEditable = (control) => (control.kind === "input" || control.kind === "textarea") && control.disabled !== true;
262
- const isToggle = (control) => control.kind === "input" && (control.inputType === "checkbox" || control.inputType === "radio");
263
- /** Input types native fill types or assigns text into; every other input takes none. */
264
- const textInputs = /* @__PURE__ */ new Set([
265
- "",
266
- "email",
267
- "number",
268
- "password",
269
- "search",
270
- "tel",
271
- "text",
272
- "url",
273
- "color",
274
- "date",
275
- "time",
276
- "datetime-local",
277
- "month",
278
- "range",
279
- "week"
280
- ]);
281
- /**
282
- * What the real driver refuses before dispatching text into a control: a disabled one, one that
283
- * takes no text, and a number field given something that is not a number.
284
- */
285
- const textRefusal = (control, text) => {
286
- const editable = control.facts?.editable;
287
- if (control.disabled === true) return Reasons.Disabled.make({});
288
- if (editable === false) return Reasons.Unsupported.make({});
289
- if (control.kind === "textarea") return void 0;
290
- if (control.kind !== "input") return editable === true ? void 0 : Reasons.Unsupported.make({});
291
- const type = (control.inputType ?? "").toLowerCase();
292
- if (!textInputs.has(type)) return Reasons.Unsupported.make({});
293
- return type === "number" && Number.isNaN(Number(text.trim())) ? Reasons.Unsupported.make({}) : void 0;
294
- };
295
- const controlState = (script) => ({
296
- script,
297
- checked: script.checked ?? (isToggle(script) ? false : void 0),
298
- selected: script.selected
299
- });
300
- const makeScriptedBrowser = (script, timers) => {
301
- const pages = /* @__PURE__ */ new Map();
302
- const calls = [];
303
- const armed = /* @__PURE__ */ new Map();
304
- const connections = [];
305
- const live = /* @__PURE__ */ new Set();
306
- let latest;
307
- /** The page last selected by any connection: where the control's document operations act. */
308
- let lastSelected;
309
- let attempts = 0;
310
- let sequence = 0;
311
- let pageSerial = 0;
312
- let observationSerial = 0;
313
- let suspensionSerial = 0;
314
- let downloadSerial = 0;
315
- let lastTimestamp = 17e11;
316
- const state = (document) => ({
317
- url: document.url,
318
- title: document.title ?? "",
319
- text: document.text,
320
- ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
321
- controls: (document.controls ?? []).map(controlState)
322
- });
323
- const documentFor = (url) => {
324
- const listed = script.documents.find((document) => document.url === url);
325
- return state(listed ?? {
326
- url,
327
- text: ""
328
- });
329
- };
330
- const createPage = (document, registered) => {
331
- const serial = ++pageSerial;
332
- const page = {
333
- pageId: `page-${serial}`,
334
- targetId: `target-${serial}`,
335
- frameId: `frame-${serial}`,
336
- document,
337
- epoch: 0,
338
- registeredEpoch: registered ? 0 : -1,
339
- values: /* @__PURE__ */ new Map(),
340
- files: /* @__PURE__ */ new Map(),
341
- scroll: {
342
- x: 0,
343
- y: 0
344
- },
345
- mutations: 0,
346
- focused: void 0,
347
- closed: false,
348
- held: void 0,
349
- navigation: void 0,
350
- watchers: /* @__PURE__ */ new Set(),
351
- capture: void 0
352
- };
353
- pages.set(page.pageId, page);
354
- for (const connection of live) connection.opened(page.pageId);
355
- return page;
356
- };
357
- const openPages = () => [...pages.values()].filter((page) => !page.closed);
358
- const notify = (page) => {
359
- const watchers = Array.from(page.watchers);
360
- for (const watcher of watchers) watcher();
361
- };
362
- /** The page shows a new document: nodes go stale, values reset, captures learn of it. */
363
- const commit = (page, document) => {
364
- page.document = document;
365
- page.cachedTitle = void 0;
366
- page.epoch++;
367
- page.values.clear();
368
- page.files.clear();
369
- page.focused = void 0;
370
- page.scroll.x = 0;
371
- page.scroll.y = 0;
372
- page.mutations++;
373
- page.navigation = void 0;
374
- for (const connection of live) connection.retire(page.pageId);
375
- for (const connection of live) connection.navigated(page.pageId, false);
376
- if (page.capture !== void 0) {
377
- const { start } = page.capture;
378
- if (start.document === void 0) start.invalidate("target-changed");
379
- else start.document(document.url, false);
380
- }
381
- notify(page);
382
- for (const connection of live) connection.announce(page.pageId);
383
- };
384
- /** In-document change: identity survives by control id, so retained nodes and waits carry on. */
385
- const update = (page, document) => {
386
- const previousUrl = page.document.url;
387
- const before = JSON.stringify(page.document);
388
- const previous = new Map(page.document.controls.map((control) => [control.script.id, control]));
389
- const controls = (document.controls ?? []).map((script) => {
390
- const existing = previous.get(script.id);
391
- if (existing === void 0) return controlState(script);
392
- existing.script = script;
393
- if (script.checked !== void 0) existing.checked = script.checked;
394
- if (script.selected !== void 0) existing.selected = script.selected;
395
- return existing;
396
- });
397
- page.document = {
398
- url: document.url,
399
- title: document.title ?? "",
400
- text: document.text,
401
- ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
402
- controls
403
- };
404
- if (JSON.stringify(page.document) !== before) page.mutations++;
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
- }
410
- notify(page);
411
- };
412
- const take = (operation) => {
413
- const queue = armed.get(operation);
414
- const next = queue?.shift();
415
- if (queue !== void 0 && queue.length === 0) armed.delete(operation);
416
- return next;
417
- };
418
- const internalGate = (gate) => {
419
- const internal = gates.get(gate);
420
- if (internal === void 0) throw new Error("A scripted gate must come from the browser it holds");
421
- return internal;
422
- };
423
- /** Only a mutation's Ticket carries dispatch evidence; a read's ticket has none to give. */
424
- const isTicket = (ticket) => "dispatch" in ticket;
425
- const connect = (options, events) => {
426
- const ordinal = attempts++;
427
- if (script.connections?.[ordinal] === "refuse") {
428
- connections.push("refused");
429
- throw new Error("The scripted browser refused this connection");
430
- }
431
- const bindings = options.bindings ?? [];
432
- const viewport = { ...options.viewport };
433
- const positions = /* @__PURE__ */ new Map();
434
- let pointer = null;
435
- let selectedId;
436
- const snapshots = /* @__PURE__ */ new Map();
437
- const privateSnapshots = /* @__PURE__ */ new Set();
438
- const resolvedElements = /* @__PURE__ */ new WeakMap();
439
- let disconnected = false;
440
- let index = -1;
441
- const place = (pageId, point) => {
442
- positions.set(pageId, point);
443
- pointer = point;
444
- };
445
- const invalidatePointer = (pageId) => {
446
- positions.delete(pageId);
447
- if (selectedId === pageId) pointer = null;
448
- };
449
- const dispatchObservers = /* @__PURE__ */ new WeakMap();
450
- const receipt = (pageId) => ({ position: positions.get(pageId) ?? null });
451
- const select = (pageId) => {
452
- const previous = selectedId;
453
- selectedId = pageId;
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
- }
462
- };
463
- const selectedPage = (operation = "target") => {
464
- const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
465
- if (disconnected || page === void 0 || page.closed) throw fail(operation, Reasons.Closed.make({}));
466
- return page;
467
- };
468
- const current = (target, operation) => {
469
- if (disconnected) throw fail(operation, Reasons.Closed.make({}));
470
- const page = pages.get(target.pageId);
471
- if (page === void 0 || page.closed || page.frameId !== target.frameId) throw fail(operation, Reasons.Stale.make({}));
472
- return page;
473
- };
474
- const pageOf = (info, operation) => {
475
- const page = pages.get(info.pageId);
476
- if (page === void 0) throw fail(operation, Reasons.NotFound.make({}));
477
- if (page.closed || page.targetId !== info.targetId) throw fail(operation, Reasons.Stale.make({}));
478
- return page;
479
- };
480
- const cachedPage = (page) => Object.freeze({
481
- pageId: page.pageId,
482
- frameId: page.frameId,
483
- targetId: page.targetId,
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"
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
- };
529
- const frameInfo = (page) => FrameInfo.make({
530
- frameId: page.frameId,
531
- parentFrameId: null,
532
- url: page.document.url,
533
- name: ""
534
- });
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
- }
557
- };
558
- /** This connection ends: what it held on the browser's pages ends with it. */
559
- const dropConnection = (announce, ended = "dropped") => {
560
- if (disconnected) return;
561
- disconnected = true;
562
- invalidateSnapshot();
563
- snapshots.clear();
564
- privateSnapshots.clear();
565
- connections[index] = ended;
566
- live.delete(connection);
567
- for (const page of pages.values()) {
568
- if (page.navigation?.by === connection) page.navigation.drop();
569
- if (page.held?.by === connection) page.held = void 0;
570
- if (page.capture?.by === connection) page.capture = void 0;
571
- notify(page);
572
- }
573
- if (announce) events.disconnected();
574
- events.retired?.();
575
- };
576
- const dispatch = (ticket, record) => {
577
- if (record.dispatched) return;
578
- if (isTicket(ticket)) ticket.dispatch();
579
- record.dispatched = true;
580
- dispatchObservers.get(record)?.();
581
- };
582
- /** Once the owner aborts the admitted call, its own check names the reason. */
583
- const aborted = (ticket, operation) => {
584
- try {
585
- ticket.check();
586
- } catch (error) {
587
- if (Schema.is(BrowserError)(error)) return error;
588
- }
589
- return fail(operation, Reasons.Interrupted.make({}), "unknown");
590
- };
591
- const hold = (gate, ticket, operation, record) => {
592
- const internal = internalGate(gate);
593
- internal.reach();
594
- return new Promise((resolve, reject) => {
595
- const onAbort = () => {
596
- cleanup();
597
- record.settled = "failed";
598
- reject(aborted(ticket, operation));
599
- };
600
- const cleanup = () => ticket.signal.removeEventListener("abort", onAbort);
601
- if (ticket.signal.aborted) {
602
- onAbort();
603
- return;
604
- }
605
- ticket.signal.addEventListener("abort", onAbort, { once: true });
606
- internal.opened.then(() => {
607
- cleanup();
608
- resolve();
609
- });
610
- });
611
- };
612
- const applyArmed = async (operation, ticket, record) => {
613
- const outcome = take(operation);
614
- if (outcome === void 0) return;
615
- switch (outcome._tag) {
616
- case "Fail":
617
- if (outcome.outcome === "unknown") dispatch(ticket, record);
618
- throw fail(operation, outcome.reason, outcome.outcome);
619
- case "Hold":
620
- if (outcome.dispatched) dispatch(ticket, record);
621
- await hold(outcome.gate, ticket, operation, record);
622
- ticket.check();
623
- return;
624
- case "Disconnect":
625
- if (outcome.dispatched === true) dispatch(ticket, record);
626
- dropConnection(true);
627
- throw fail(operation, Reasons.Disconnected.make({}), record.dispatched ? "unknown" : "undispatched");
628
- }
629
- };
630
- const attempt = async (operation, ticket, meta, body, onDispatch) => {
631
- const record = {
632
- sequence: ++sequence,
633
- operation,
634
- pageId: meta.pageId ?? selectedId ?? "",
635
- ...meta.elementId === void 0 ? {} : { elementId: meta.elementId },
636
- ...meta.selector === void 0 ? {} : { selector: meta.selector },
637
- dispatched: false,
638
- settled: "pending"
639
- };
640
- calls.push(record);
641
- if (onDispatch !== void 0) dispatchObservers.set(record, onDispatch);
642
- try {
643
- ticket?.check();
644
- if (ticket !== void 0) await applyArmed(operation, ticket, record);
645
- const value = await body(record);
646
- record.settled = "completed";
647
- if (ticket !== void 0 && isTicket(ticket)) ticket.acknowledge?.();
648
- return value;
649
- } catch (error) {
650
- record.settled = "failed";
651
- throw error;
652
- }
653
- };
654
- const meta = (target) => target === void 0 ? {} : typeof target === "string" ? { selector: target } : { elementId: "_tag" in target ? resolvedElements.get(target)?.node.script.id : target.elementId };
655
- const facts = (control) => {
656
- const { script: value } = control;
657
- const extra = value.facts ?? {};
658
- return ControlFacts.make({
659
- kind: value.kind,
660
- label: value.label,
661
- disabled: value.disabled ?? false,
662
- ...control.checked === void 0 ? {} : { checked: control.checked },
663
- ...control.selected === void 0 ? {} : { selected: control.selected },
664
- ...value.required === void 0 ? {} : { required: value.required },
665
- ...value.multiple === void 0 ? {} : { multiple: value.multiple },
666
- editable: extra.editable ?? isEditable(value),
667
- ...value.inputType === void 0 ? {} : { inputType: value.inputType },
668
- ...extra.autocomplete === void 0 ? {} : { autocomplete: extra.autocomplete },
669
- ...value.destination === void 0 || value.destination.length > 2048 ? {} : { destination: value.destination },
670
- ...extra.formMethod === void 0 ? {} : { formMethod: extra.formMethod },
671
- box: ViewportRect.make(extra.box ?? {
672
- x: 0,
673
- y: 0,
674
- width: 120,
675
- height: 24
676
- }),
677
- placement: extra.placement ?? (value.offscreen === true ? "outside" : "inside"),
678
- hitTest: extra.hitTest ?? (value.offscreen === true ? "unsampled" : "self"),
679
- mainFrame: true
680
- });
681
- };
682
- const observed = (control) => {
683
- const { script: value } = control;
684
- return ObservedControl.make({
685
- elementId: value.id,
686
- kind: value.kind,
687
- label: value.label,
688
- disabled: value.disabled ?? false,
689
- ...control.checked === void 0 ? {} : { checked: control.checked },
690
- ...control.selected === void 0 ? {} : { selected: control.selected },
691
- ...value.inputType === void 0 ? {} : { inputType: value.inputType },
692
- ...value.required === void 0 ? {} : { required: value.required },
693
- ...value.multiple === void 0 ? {} : { multiple: value.multiple },
694
- ...value.selectElementId === void 0 ? {} : { selectElementId: value.selectElementId }
695
- });
696
- };
697
- const bySelector = (page, selector, operation) => {
698
- const id = selector.startsWith("#") ? selector.slice(1) : void 0;
699
- const control = id === void 0 ? void 0 : page.document.controls.find((c) => c.script.id === id);
700
- if (control === void 0) throw fail(operation, Reasons.NotFound.make({}));
701
- return control;
702
- };
703
- /** A retained node is only as current as the observation that produced it. */
704
- const retained = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => {
705
- ticket.check();
706
- const stale = () => fail(operation, Reasons.Stale.make({}));
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);
711
- if (node === void 0) 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();
715
- if (attached && !page.document.controls.includes(node)) throw stale();
716
- return {
717
- node,
718
- page,
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
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
- });
746
- const resolve = (target, ticket, operation, policy, browserTarget, allowSuspended = false, enablement = false) => {
747
- let node;
748
- let page;
749
- if (typeof target === "string") {
750
- page = current(browserTarget, operation);
751
- node = bySelector(page, target, operation);
752
- } else ({node, page} = retainedElement(target, ticket, operation, allowSuspended, true, browserTarget));
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({}));
760
- if (enablement && node.script.disabled === true) throw fail(operation, Reasons.Disabled.make({}));
761
- if (policy !== void 0) {
762
- let admitted = false;
763
- try {
764
- admitted = policy(fresh) === true;
765
- } catch {
766
- admitted = false;
767
- }
768
- if (!admitted) throw fail(operation, Reasons.Denied.make({}));
769
- }
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
- }
775
- return {
776
- node,
777
- page,
778
- facts: fresh
779
- };
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
- };
949
- const requireRunning = (page, operation) => {
950
- if (page.held !== void 0) throw fail(operation, Reasons.Busy.make({}));
951
- };
952
- const heldObservation = (pageId) => {
953
- for (const snapshot of snapshots.values()) if (snapshot.pageId === pageId && snapshot.validity === "valid") {
954
- snapshot.validity = "suspended";
955
- snapshot.revalidated.clear();
956
- }
957
- };
958
- const activate = (page, node) => {
959
- const { script: value } = node;
960
- page.focused = value.id;
961
- if (value.kind === "input" && value.inputType === "checkbox") node.checked = !node.checked;
962
- if (value.kind === "input" && value.inputType === "radio") node.checked = true;
963
- const target = value.destination ?? value.activates;
964
- if (target !== void 0) commit(page, documentFor(target));
965
- return page.document.url;
966
- };
967
- const focusedOrRefuse = (into, ticket, operation, policy, browserTarget) => {
968
- if (into === void 0) return current(browserTarget, operation);
969
- const { node, page } = resolve(into, ticket, operation, policy, browserTarget);
970
- if (page.focused !== node.script.id) throw fail(operation, Reasons.NotFocused.make({}));
971
- return page;
972
- };
973
- const typed = (page, text) => {
974
- const focused = page.focused === void 0 ? void 0 : page.document.controls.find((control) => control.script.id === page.focused);
975
- if (focused !== void 0 && isEditable(focused.script)) page.values.set(focused.script.id, (page.values.get(focused.script.id) ?? "") + text);
976
- };
977
- /** Issued options of one select, refused exactly where native selection refuses them. */
978
- const chosen = (page, node, ids, ticket, operation, enablement = false) => {
979
- if (!isSelectable(node.script.kind)) throw fail(operation, Reasons.Unsupported.make({}));
980
- const options = ids.map((id) => {
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));
1001
- return option;
1002
- });
1003
- if (node.script.disabled === true || options.some((option) => option.script.disabled === true)) throw fail(operation, Reasons.Disabled.make({}));
1004
- if (node.script.multiple !== true && options.length > 1) throw fail(operation, Reasons.Unsupported.make({}));
1005
- return options;
1006
- };
1007
- const choose = (page, node, options) => {
1008
- if (node.script.multiple !== true) {
1009
- for (const candidate of page.document.controls) if (candidate.script.selectElementId === node.script.id) candidate.selected = false;
1010
- }
1011
- for (const option of options) option.selected = true;
1012
- page.focused = node.script.id;
1013
- };
1014
- /** A control's private state after a form step. It is compared here and never returned. */
1015
- const fieldState = (page, node) => {
1016
- const { script: value } = node;
1017
- if (node.checked !== void 0) return JSON.stringify(["checked", node.checked]);
1018
- if (value.kind === "select") return JSON.stringify(["options", page.document.controls.filter((option) => option.script.selectElementId === value.id && option.selected).map((option) => option.script.id)]);
1019
- if (value.kind === "input" || value.kind === "textarea" || value.facts?.editable === true) return JSON.stringify(["value", page.values.get(value.id) ?? ""]);
1020
- return JSON.stringify(["other"]);
1021
- };
1022
- /** One form step, as the real driver takes it; see `Driver["formStep"]`. */
1023
- const formStep = async (page, node, field, ticket, record, capture) => {
1024
- const epoch = page.epoch;
1025
- let act;
1026
- let input;
1027
- if (field.options !== void 0) {
1028
- const options = chosen(page, node, field.options, ticket, "fill-form", true);
1029
- act = () => choose(page, node, options);
1030
- } else if (field.checked !== void 0) {
1031
- if (node.checked === void 0) throw fail("fill-form", Reasons.Unsupported.make({}));
1032
- if (node.checked !== field.checked) {
1033
- if (!field.checked && node.script.inputType === "radio") throw fail("fill-form", Reasons.Unsupported.make({}));
1034
- act = async () => {
1035
- invalidatePointer(page.pageId);
1036
- input = await capture(async () => {
1037
- activate(page, node);
1038
- }, { position: null });
1039
- };
1040
- }
1041
- } else {
1042
- const text = field.value ?? "";
1043
- const refusal = textRefusal(node.script, text);
1044
- if (refusal !== void 0) throw fail("fill-form", refusal);
1045
- act = () => {
1046
- page.values.set(node.script.id, text);
1047
- page.focused = void 0;
1048
- };
1049
- }
1050
- if (act !== void 0) {
1051
- dispatch(ticket, record);
1052
- await act();
1053
- }
1054
- const state = page.epoch === epoch && !page.closed && page.document.controls.includes(node) ? fieldState(page, node) : void 0;
1055
- return {
1056
- status: act === void 0 ? "unchanged" : "set",
1057
- reached: field.checked === void 0 || state === void 0 || state === JSON.stringify(["checked", field.checked]),
1058
- state,
1059
- url: page.document.url,
1060
- ...input === void 0 ? {} : { input }
1061
- };
1062
- };
1063
- const inFlight = (page, url, outcome, timeoutMillis, record) => new Promise((resolve, reject) => {
1064
- let done = false;
1065
- const timer = timers.sleep(timeoutMillis);
1066
- const finish = (settle) => {
1067
- if (done) return;
1068
- done = true;
1069
- timer.cancel();
1070
- if (page.navigation === flight) page.navigation = void 0;
1071
- settle();
1072
- };
1073
- const failed = (reason) => finish(() => {
1074
- record.settled = "failed";
1075
- reject(fail("navigate", reason, "unknown"));
1076
- });
1077
- const flight = {
1078
- stop: () => failed(Reasons.Interrupted.make({})),
1079
- drop: () => failed(Reasons.Disconnected.make({})),
1080
- by: connection
1081
- };
1082
- page.navigation = flight;
1083
- timer.done.then(() => failed(Reasons.Timeout.make({})), () => {});
1084
- switch (outcome._tag) {
1085
- case "Fail":
1086
- failed(outcome.reason);
1087
- return;
1088
- case "Disconnect":
1089
- dropConnection(true);
1090
- return;
1091
- case "Hold": {
1092
- const gate = internalGate(outcome.gate);
1093
- gate.reach();
1094
- gate.opened.then(() => finish(() => {
1095
- commit(page, documentFor(url));
1096
- resolve(url);
1097
- }));
1098
- }
1099
- }
1100
- });
1101
- const waitUntil = (ticket, page, operation, record, satisfied, detached) => new Promise((resolve, reject) => {
1102
- const cleanup = () => {
1103
- page.watchers.delete(check);
1104
- ticket.signal.removeEventListener("abort", onAbort);
1105
- ticket.retire();
1106
- };
1107
- const onAbort = () => {
1108
- cleanup();
1109
- record.settled = "failed";
1110
- reject(aborted(ticket, operation));
1111
- };
1112
- const check = () => {
1113
- try {
1114
- ticket.check();
1115
- if (disconnected) throw fail(operation, Reasons.Disconnected.make({}));
1116
- if (detached()) throw fail(operation, Reasons.Stale.make({}));
1117
- if (!satisfied()) return;
1118
- cleanup();
1119
- resolve();
1120
- } catch (error) {
1121
- cleanup();
1122
- record.settled = "failed";
1123
- reject(error);
1124
- }
1125
- };
1126
- if (ticket.signal.aborted) {
1127
- onAbort();
1128
- return;
1129
- }
1130
- ticket.signal.addEventListener("abort", onAbort, { once: true });
1131
- page.watchers.add(check);
1132
- check();
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
- });
1226
- const invokeBinding = async (name, input, origin) => {
1227
- const binding = bindings.find((candidate) => candidate.name === name);
1228
- if (binding === void 0) return { ok: false };
1229
- const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
1230
- const epoch = page?.epoch;
1231
- const documentOrigin = origin ?? (page === void 0 ? "null" : originOf(page.document.url));
1232
- const text = JSON.stringify(input) ?? "null";
1233
- const error = (reason) => InitializationError.make({
1234
- operation: "callback",
1235
- step: name,
1236
- reason
1237
- });
1238
- const check = async (signal) => {
1239
- if (disconnected || signal.aborted) throw error("closed");
1240
- if (page === void 0 || page.closed || page.epoch !== epoch) throw error("stale");
1241
- if (!binding.origins.includes(documentOrigin)) throw error("origin");
1242
- };
1243
- try {
1244
- const reply = await binding.invoke({
1245
- origin: documentOrigin,
1246
- read: async (signal) => {
1247
- await check(signal);
1248
- return text;
1249
- },
1250
- check,
1251
- dispose: async () => {}
1252
- });
1253
- return {
1254
- ok: true,
1255
- output: JSON.parse(reply)
1256
- };
1257
- } catch {
1258
- return { ok: false };
1259
- }
1260
- };
1261
- if (pages.size === 0) {
1262
- const startUrl = script.start ?? script.documents[0]?.url ?? "https://scripted.invalid/";
1263
- const first = createPage(documentFor(startUrl), true);
1264
- select(first.pageId);
1265
- if (options.initialTargetId !== void 0 && options.initialTargetId !== first.targetId) throw fail("connect", Reasons.NotFound.make({}));
1266
- if (options.newPage === true) select(createPage(state({
1267
- url: startUrl,
1268
- text: ""
1269
- }), false).pageId);
1270
- } else if (options.initialTargetId !== void 0) {
1271
- const target = openPages().find((page) => page.targetId === options.initialTargetId);
1272
- if (target === void 0) throw fail("connect", Reasons.NotFound.make({}));
1273
- select(target.pageId);
1274
- } else if (options.newPage === true) select(createPage(state({
1275
- url: "https://scripted.invalid/blank",
1276
- text: ""
1277
- }), false).pageId);
1278
- else {
1279
- const previous = lastSelected === void 0 ? void 0 : pages.get(lastSelected);
1280
- select(previous !== void 0 && !previous.closed ? previous.pageId : openPages()[0]?.pageId);
1281
- }
1282
- const driver = {
1283
- cachedPages: () => Object.freeze(openPages().map(cachedPage)),
1284
- ...options.pageControl === true ? { pageControl: {
1285
- state: async (page, ticket) => {
1286
- ticket.check();
1287
- const target = pageOf(page, "page-control");
1288
- return PageExecutionState.make({
1289
- pageId: target.pageId,
1290
- targetId: target.targetId,
1291
- state: target.held === void 0 ? "running" : "suspended",
1292
- ...target.held === void 0 ? {} : { suspensionId: target.held.suspensionId }
1293
- });
1294
- },
1295
- suspend: (page, ticket) => attempt("page-suspend", ticket, { pageId: page.pageId }, async (record) => {
1296
- const target = pageOf(page, "page-suspend");
1297
- requireRunning(target, "page-suspend");
1298
- dispatch(ticket, record);
1299
- target.held = {
1300
- suspensionId: `suspension-${++suspensionSerial}`,
1301
- by: connection
1302
- };
1303
- heldObservation(target.pageId);
1304
- ticket.acknowledge?.();
1305
- events.pageLifecycle?.({
1306
- _tag: "Display",
1307
- page: cachedPage(target)
1308
- });
1309
- return PageSuspension.make({
1310
- pageId: target.pageId,
1311
- targetId: target.targetId,
1312
- suspensionId: target.held.suspensionId
1313
- });
1314
- }),
1315
- resume: (receipt, ticket) => attempt("page-resume", ticket, { pageId: receipt.pageId }, async (record) => {
1316
- const target = pages.get(receipt.pageId);
1317
- if (target === void 0 || target.closed || target.held === void 0 || target.targetId !== receipt.targetId || target.held.suspensionId !== receipt.suspensionId) throw fail("page-resume", Reasons.Stale.make({}));
1318
- dispatch(ticket, record);
1319
- target.held = void 0;
1320
- heldObservation(target.pageId);
1321
- ticket.acknowledge?.();
1322
- events.pageLifecycle?.({
1323
- _tag: "Display",
1324
- page: cachedPage(target)
1325
- });
1326
- }),
1327
- checkTarget: async (target, ticket) => {
1328
- ticket.check();
1329
- requireRunning(current(target, "page-control"), "page-control");
1330
- }
1331
- } } : {},
1332
- selected: () => {
1333
- const page = selectedPage();
1334
- return {
1335
- pageId: page.pageId,
1336
- frameId: page.frameId
1337
- };
1338
- },
1339
- selectedTargetId: async () => selectedPage().targetId,
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"))),
1342
- resolvePage: async (page, ticket) => {
1343
- ticket.check();
1344
- const target = pageOf(page, "target");
1345
- return {
1346
- pageId: target.pageId,
1347
- frameId: target.frameId
1348
- };
1349
- },
1350
- selectPage: (page, ticket) => attempt("select-page", ticket, { pageId: page.pageId }, async () => {
1351
- const target = pageOf(page, "select-page");
1352
- select(target.pageId);
1353
- events.invalidate("target-changed", "none");
1354
- }),
1355
- newPage: (ticket) => attempt("new-page", ticket, {}, async (record) => {
1356
- if (openPages().length >= options.maxPages) throw fail("new-page", Reasons.Limit.make({
1357
- dimension: "pages",
1358
- maximum: options.maxPages,
1359
- observed: openPages().length
1360
- }));
1361
- dispatch(ticket, record);
1362
- return info(createPage(state({
1363
- url: "https://scripted.invalid/blank",
1364
- text: ""
1365
- }), false));
1366
- }),
1367
- closePage: (page, ticket, onDispatch) => attempt("close-page", ticket, { pageId: page.pageId }, async (record) => {
1368
- const target = pageOf(page, "close-page");
1369
- dispatch(ticket, record);
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);
1379
- }),
1380
- listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(pageOf(page, "list-frames"))]),
1381
- resolveFrame: async (page, frame, ticket) => {
1382
- ticket.check();
1383
- const target = pageOf(page, "target");
1384
- if (frame.frameId !== target.frameId) throw fail("target", Reasons.NotFound.make({}));
1385
- return {
1386
- pageId: target.pageId,
1387
- frameId: target.frameId
1388
- };
1389
- },
1390
- beginNavigation: (url, timeoutMillis, ticket, target) => attempt("navigate", void 0, { pageId: target.pageId }, async (record) => {
1391
- ticket.check();
1392
- const page = current(target, "navigate");
1393
- requireRunning(page, "navigate");
1394
- const outcome = take("navigate");
1395
- if (outcome?._tag === "Fail" && outcome.outcome !== "unknown") throw fail("navigate", outcome.reason, outcome.outcome);
1396
- if (outcome?._tag === "Hold" && !outcome.dispatched) {
1397
- await hold(outcome.gate, ticket, "navigate", record);
1398
- ticket.check();
1399
- }
1400
- if (outcome?._tag === "Disconnect" && outcome.dispatched !== true) {
1401
- dropConnection(true);
1402
- throw fail("navigate", Reasons.Disconnected.make({}));
1403
- }
1404
- dispatch(ticket, record);
1405
- page.navigation?.stop();
1406
- const inFlightOutcome = outcome === void 0 || outcome._tag === "Hold" && !outcome.dispatched ? void 0 : outcome;
1407
- let settled;
1408
- if (inFlightOutcome === void 0) {
1409
- commit(page, documentFor(url));
1410
- settled = Promise.resolve(url);
1411
- } else {
1412
- settled = inFlight(page, url, inFlightOutcome, timeoutMillis, record);
1413
- settled.catch(() => {});
1414
- }
1415
- return {
1416
- pageId: page.pageId,
1417
- mainFrame: true,
1418
- settled,
1419
- stop: (stopTicket, pending, onDispatch) => attempt("navigate-stop", void 0, { pageId: page.pageId }, async (stopRecord) => {
1420
- stopTicket.check();
1421
- const armedStop = take("navigate-stop");
1422
- if (armedStop?._tag === "Hold" && !armedStop.dispatched) {
1423
- await hold(armedStop.gate, stopTicket, "navigate-stop", stopRecord);
1424
- stopTicket.check();
1425
- }
1426
- if (!pending()) return "settled";
1427
- if (armedStop?._tag === "Fail" && armedStop.outcome !== "unknown") throw fail("navigate-stop", armedStop.reason, armedStop.outcome);
1428
- if (armedStop?._tag === "Disconnect" && armedStop.dispatched !== true) {
1429
- dropConnection(true);
1430
- throw fail("navigate-stop", Reasons.Disconnected.make({}));
1431
- }
1432
- dispatch(stopTicket, stopRecord);
1433
- onDispatch();
1434
- switch (armedStop?._tag) {
1435
- case "Fail": throw fail("navigate-stop", armedStop.reason, "unknown");
1436
- case "Disconnect":
1437
- dropConnection(true);
1438
- throw fail("navigate-stop", Reasons.Disconnected.make({}), "unknown");
1439
- case "Hold":
1440
- if (armedStop.dispatched) {
1441
- await hold(armedStop.gate, stopTicket, "navigate-stop", stopRecord);
1442
- stopTicket.check();
1443
- }
1444
- break;
1445
- case void 0:
1446
- }
1447
- page.navigation?.stop();
1448
- return "dispatched";
1449
- })
1450
- };
1451
- }),
1452
- readText: (selector, maximumBytes, ticket, target) => attempt("read-text", ticket, {
1453
- pageId: target.pageId,
1454
- ...meta(selector)
1455
- }, async () => {
1456
- const page = current(target, "read-text");
1457
- if (selector === void 0) return bounded(page.document.text, maximumBytes).text;
1458
- const control = bySelector(page, selector, "read-text");
1459
- return bounded(control.script.text ?? control.script.label, maximumBytes).text;
1460
- }),
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;
1464
- const needle = match?.toLowerCase();
1465
- const matches = (value) => needle === void 0 || value.toLowerCase().includes(needle);
1466
- snapshots.delete(page.frameId);
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));
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));
1469
- const reachable = matching.filter((control) => scope === "document" || control.script.offscreen !== true);
1470
- const kept = reachable.slice(0, controls);
1471
- const text = bounded(needle === void 0 ? page.document.text : page.document.text.split("\n").map((line) => line.trim()).filter((line) => line !== "" && matches(line)).join("\n"), maximumBytes);
1472
- const next = {
1473
- id: `observation-${++observationSerial}`,
1474
- pageId: page.pageId,
1475
- frameId: page.frameId,
1476
- epoch: page.epoch,
1477
- generation: ticket.generation,
1478
- scope,
1479
- validity: "valid",
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
- })),
1488
- revalidated: /* @__PURE__ */ new Set()
1489
- };
1490
- snapshots.set(page.frameId, next);
1491
- return {
1492
- observationId: next.id,
1493
- scope,
1494
- ...match === void 0 ? {} : { match },
1495
- url: page.document.url,
1496
- text: text.text,
1497
- textTruncated: text.truncated,
1498
- controls: kept.map(observed),
1499
- controlsTruncated: kept.length < reachable.length,
1500
- viewport: ViewportEvidence.make({
1501
- width: size.width,
1502
- height: size.height,
1503
- ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1504
- clippedText: 0,
1505
- coveredText: 0,
1506
- uncertainText: 0,
1507
- unreachableControls: matching.length - reachable.length,
1508
- exhausted: false
1509
- })
1510
- };
1511
- }),
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;
1515
- const text = bounded(page.document.text, maximumBytes);
1516
- const reachable = page.document.controls.filter((control) => control.script.offscreen !== true);
1517
- return {
1518
- url: page.document.url,
1519
- text: text.text,
1520
- textTruncated: text.truncated,
1521
- controls: reachable.slice(0, controls).map(facts),
1522
- controlsTruncated: reachable.length > controls,
1523
- viewport: ViewportEvidence.make({
1524
- width: size.width,
1525
- height: size.height,
1526
- ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1527
- clippedText: 0,
1528
- coveredText: 0,
1529
- uncertainText: 0,
1530
- unreachableControls: page.document.controls.length - reachable.length,
1531
- exhausted: false
1532
- }),
1533
- documentChanged: false,
1534
- ...pictureBytes === void 0 ? {} : { picture: picture(page, ticket) }
1535
- };
1536
- }),
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
- }
1569
- }),
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);
1577
- current.revalidated.add(target.elementId);
1578
- }),
1579
- click: (target, ticket, capture, policy, browserTarget) => attempt("click", ticket, {
1580
- pageId: browserTarget.pageId,
1581
- ...meta(target)
1582
- }, async (record) => {
1583
- const { node, page } = resolve(target, ticket, "click", policy, browserTarget);
1584
- requireRunning(page, "click");
1585
- dispatch(ticket, record);
1586
- let url = "";
1587
- const input = await capture(async () => {
1588
- url = activate(page, node);
1589
- }, { position: null });
1590
- return {
1591
- url,
1592
- input
1593
- };
1594
- }, () => invalidatePointer(browserTarget.pageId)),
1595
- fill: (target, value, ticket, policy, browserTarget) => attempt("fill", ticket, {
1596
- pageId: browserTarget.pageId,
1597
- ...meta(target)
1598
- }, async (record) => {
1599
- const { node, page } = resolve(target, ticket, "fill", policy, browserTarget);
1600
- requireRunning(page, "fill");
1601
- const refusal = textRefusal(node.script, value);
1602
- if (refusal !== void 0) throw fail("fill", refusal);
1603
- dispatch(ticket, record);
1604
- page.focused = node.script.id;
1605
- page.values.set(node.script.id, value);
1606
- return page.document.url;
1607
- }),
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);
1613
- requireRunning(page, "select-option");
1614
- const options = chosen(page, node, ids, ticket, "select-option");
1615
- dispatch(ticket, record);
1616
- choose(page, node, options);
1617
- return page.document.url;
1618
- }),
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);
1624
- requireRunning(page, "fill-form");
1625
- return formStep(page, node, field, ticket, record, capture);
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);
1629
- return page.document.controls.includes(node) ? fieldState(page, node) : void 0;
1630
- })),
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);
1636
- requireRunning(page, "fill-form");
1637
- dispatch(ticket, record);
1638
- let url = "";
1639
- const input = await capture(async () => {
1640
- url = activate(page, node);
1641
- }, { position: null });
1642
- return {
1643
- url,
1644
- input
1645
- };
1646
- }, () => invalidatePointer(browserTarget.pageId)),
1647
- scroll: (deltaX, deltaY, ticket, target) => attempt("scroll", ticket, { pageId: target.pageId }, async (record) => {
1648
- const page = current(target, "scroll");
1649
- requireRunning(page, "scroll");
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);
1666
- return page.document.url;
1667
- }),
1668
- pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target.pageId }, async (record) => {
1669
- const page = current(target, "pointer-move");
1670
- requireRunning(page, "pointer-move");
1671
- dispatch(ticket, record);
1672
- place(page.pageId, {
1673
- x: to.x,
1674
- y: to.y
1675
- });
1676
- return receipt(page.pageId);
1677
- }),
1678
- hover: (target, ticket, policy, browserTarget) => attempt("hover", ticket, {
1679
- pageId: browserTarget.pageId,
1680
- ...meta(target)
1681
- }, async (record) => {
1682
- const { node, page, facts: fresh } = resolve(target, ticket, "hover", policy, browserTarget);
1683
- requireRunning(page, "hover");
1684
- if (node.script.offscreen === true) throw fail("hover", Reasons.NotVisible.make({}));
1685
- dispatch(ticket, record);
1686
- place(page.pageId, {
1687
- x: fresh.box.x + fresh.box.width / 2,
1688
- y: fresh.box.y + fresh.box.height / 2
1689
- });
1690
- return receipt(page.pageId);
1691
- }),
1692
- wheel: (deltaX, deltaY, at, ticket, target) => attempt("wheel", ticket, { pageId: target.pageId }, async (record) => {
1693
- const page = current(target, "wheel");
1694
- requireRunning(page, "wheel");
1695
- dispatch(ticket, record);
1696
- if (at !== void 0) place(page.pageId, {
1697
- x: at.x,
1698
- y: at.y
1699
- });
1700
- page.scroll.x += deltaX;
1701
- page.scroll.y += deltaY;
1702
- notify(page);
1703
- return receipt(page.pageId);
1704
- }),
1705
- press: (key, _modifiers, into, ticket, policy, browserTarget) => attempt("press", ticket, {
1706
- pageId: browserTarget.pageId,
1707
- ...meta(into)
1708
- }, async (record) => {
1709
- const page = focusedOrRefuse(into, ticket, "press", policy, browserTarget);
1710
- requireRunning(page, "press");
1711
- dispatch(ticket, record);
1712
- if (key.length === 1) typed(page, key);
1713
- return receipt(page.pageId);
1714
- }),
1715
- type: (text, into, ticket, policy, browserTarget) => attempt("type", ticket, {
1716
- pageId: browserTarget.pageId,
1717
- ...meta(into)
1718
- }, async (record) => {
1719
- const page = focusedOrRefuse(into, ticket, "type", policy, browserTarget);
1720
- requireRunning(page, "type");
1721
- dispatch(ticket, record);
1722
- typed(page, text);
1723
- return receipt(page.pageId);
1724
- }),
1725
- screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target.pageId }, async () => {
1726
- const page = current(target, "screenshot");
1727
- return picture(page, ticket);
1728
- }),
1729
- resize: (next, ticket, target) => attempt("resize", ticket, { pageId: target.pageId }, async (record) => {
1730
- const page = current(target, "resize");
1731
- dispatch(ticket, record);
1732
- page.viewport = {
1733
- width: next.width,
1734
- height: next.height
1735
- };
1736
- page.capture?.start.invalidate("resized");
1737
- events.invalidate("resized", { pageId: page.pageId });
1738
- }),
1739
- waitFor: (selector, state, ticket, target) => attempt("wait", ticket, {
1740
- pageId: target.pageId,
1741
- selector
1742
- }, async (record) => {
1743
- const page = current(target, "wait");
1744
- const epoch = page.epoch;
1745
- const find = () => {
1746
- const id = selector.startsWith("#") ? selector.slice(1) : void 0;
1747
- return id === void 0 ? void 0 : page.document.controls.find((control) => control.script.id === id);
1748
- };
1749
- await waitUntil(ticket, page, "wait", record, () => {
1750
- const control = find();
1751
- const present = control !== void 0;
1752
- const visible = present && control.script.offscreen !== true;
1753
- switch (state) {
1754
- case "visible": return visible;
1755
- case "hidden": return !visible;
1756
- case "attached": return present;
1757
- case "detached": return !present;
1758
- }
1759
- }, () => page.closed || page.epoch !== epoch);
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()),
1766
- waitForElement: (reference, state, ticket, target) => attempt("wait", ticket, {
1767
- pageId: target.pageId,
1768
- ...meta(reference)
1769
- }, async (record) => {
1770
- const page = current(target, "wait");
1771
- const { node } = retainedElement(reference, ticket, "wait", true, true, target);
1772
- const epoch = page.epoch;
1773
- await waitUntil(ticket, page, "wait", record, () => {
1774
- const present = page.document.controls.includes(node);
1775
- const visible = present && node.script.offscreen !== true;
1776
- switch (state) {
1777
- case "visible": return visible;
1778
- case "hidden": return !visible;
1779
- case "enabled": return present && node.script.disabled !== true;
1780
- case "disabled": return present && node.script.disabled === true;
1781
- }
1782
- }, () => page.closed || page.epoch !== epoch);
1783
- }).finally(() => ticket.retire()),
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);
1789
- requireRunning(page, "click-and-wait");
1790
- dispatch(ticket, record);
1791
- let url = "";
1792
- const input = await capture(async () => {
1793
- url = activate(page, node);
1794
- }, { position: null });
1795
- return {
1796
- url,
1797
- input
1798
- };
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);
1805
- requireRunning(page, "download-action");
1806
- if (node.script.download === void 0) throw fail("download-action", Reasons.Unsupported.make({}));
1807
- dispatch(ticket, record);
1808
- page.focused = node.script.id;
1809
- return {
1810
- downloadId: `download-${++downloadSerial}`,
1811
- filename: node.script.download,
1812
- state: "completed"
1813
- };
1814
- }),
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)),
1823
- documentReadiness: async (ticket, target) => {
1824
- ticket.check();
1825
- const page = current(target, "ready");
1826
- const bootstrap = options.bootstrap;
1827
- if (bootstrap === void 0 || bootstrap.readiness.length === 0) return { _tag: "Ready" };
1828
- if (page.epoch <= page.registeredEpoch && bootstrap.existingDocuments === "RequireFreshNavigation") return { _tag: "RequiresNavigation" };
1829
- const origin = originOf(page.document.url);
1830
- return bootstrap.readiness.some((requirement) => requirement.origins === void 0 || requirement.origins.includes(origin)) ? { _tag: "Ready" } : { _tag: "NotApplicable" };
1831
- },
1832
- dismissDialogs: async () => {},
1833
- capture: async (target) => {
1834
- const page = pageOf(PageInfo.make({
1835
- pageId: target.pageId,
1836
- targetId: target.targetId,
1837
- url: "",
1838
- title: "",
1839
- selected: false
1840
- }), "capture");
1841
- return {
1842
- pageId: page.pageId,
1843
- targetId: page.targetId,
1844
- frameId: page.frameId,
1845
- source: {
1846
- start: async (start) => {
1847
- if (disconnected || page.closed) throw fail("capture-start", Reasons.Closed.make({}));
1848
- page.capture = {
1849
- start,
1850
- by: connection
1851
- };
1852
- start.opened?.(page.document.url);
1853
- },
1854
- stop: async () => {
1855
- page.capture = void 0;
1856
- const outcome = take("capture-stop");
1857
- if (outcome?._tag === "Fail") throw fail("capture-stop", outcome.reason, outcome.outcome);
1858
- }
1859
- }
1860
- };
1861
- },
1862
- invalidateObservation: (scope, origin) => {
1863
- invalidateSnapshot(scope, origin);
1864
- },
1865
- disconnect: async () => {
1866
- if (disconnected) return;
1867
- const outcome = take("disconnect");
1868
- if (outcome?._tag === "Hold") {
1869
- const gate = internalGate(outcome.gate);
1870
- gate.reach();
1871
- await gate.opened;
1872
- }
1873
- if (outcome?._tag === "Fail") {
1874
- dropConnection(false, "close-failed");
1875
- throw fail("disconnect", outcome.reason, outcome.outcome);
1876
- }
1877
- dropConnection(false, "closed");
1878
- }
1879
- };
1880
- const selectFilesOn = (target, files, ticket, record, operation, browserTarget) => {
1881
- const { node, page } = resolve(target, ticket, operation, void 0, browserTarget);
1882
- requireRunning(page, operation);
1883
- if (node.script.kind !== "input" || node.script.inputType !== "file") throw fail(operation, Reasons.Unsupported.make({}));
1884
- dispatch(ticket, record);
1885
- page.files.set(node.script.id, files.map((file) => file._tag === "Inline" ? file.name : file.path));
1886
- return page.document.url;
1887
- };
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
- },
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
- },
1921
- announce: (pageId) => events.invalidate("target-changed", { pageId }),
1922
- drop: () => dropConnection(true),
1923
- invoke: invokeBinding,
1924
- pointer: () => pointer,
1925
- viewport: (pageId) => pages.get(pageId)?.viewport ?? viewport
1926
- };
1927
- index = connections.push("open") - 1;
1928
- live.add(connection);
1929
- latest = connection;
1930
- return driver;
1931
- };
1932
- const toScript = (document) => ({
1933
- url: document.url,
1934
- title: document.title,
1935
- text: document.text,
1936
- ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
1937
- controls: document.controls.map((control) => ({
1938
- ...control.script,
1939
- ...control.checked === void 0 ? {} : { checked: control.checked },
1940
- ...control.selected === void 0 ? {} : { selected: control.selected }
1941
- }))
1942
- });
1943
- /** The page last selected by any connection; it stays reachable while no connection is open. */
1944
- const selectedOrClosed = Effect.suspend(() => {
1945
- const page = lastSelected === void 0 ? void 0 : pages.get(lastSelected);
1946
- return page === void 0 || page.closed ? Effect.fail(fail("target", Reasons.Closed.make({}))) : Effect.succeed(page);
1947
- });
1948
- return {
1949
- connect,
1950
- control: {
1951
- calls: Effect.sync(() => calls.map((call) => Object.freeze({ ...call }))),
1952
- next: (operation, outcome) => Effect.sync(() => {
1953
- const queue = armed.get(operation) ?? [];
1954
- queue.push(outcome);
1955
- armed.set(operation, queue);
1956
- }),
1957
- gate: makeGate,
1958
- document: {
1959
- current: selectedOrClosed.pipe(Effect.map((page) => toScript(page.document))),
1960
- replace: (next) => checked(DocumentScript, next, "configure").pipe(Effect.flatMap((document) => selectedOrClosed.pipe(Effect.map((page) => {
1961
- page.navigation?.stop();
1962
- commit(page, state(document));
1963
- })))),
1964
- update: (next) => checked(DocumentScript, next, "configure").pipe(Effect.flatMap((document) => selectedOrClosed.pipe(Effect.map((page) => {
1965
- update(page, document);
1966
- })))),
1967
- values: Effect.sync(() => lastSelected === void 0 ? /* @__PURE__ */ new Map() : new Map(pages.get(lastSelected)?.values ?? [])),
1968
- files: Effect.sync(() => lastSelected === void 0 ? /* @__PURE__ */ new Map() : new Map(pages.get(lastSelected)?.files ?? []))
1969
- },
1970
- pointer: Effect.sync(() => latest?.pointer() ?? null),
1971
- capture: { emit: (frame = {}) => selectedOrClosed.pipe(Effect.flatMap((page) => {
1972
- const running = page.capture?.start;
1973
- if (running === void 0) return Effect.fail(fail("capture-consume", Reasons.NotFound.make({})));
1974
- const timestamp = frame.timestamp ?? lastTimestamp + 40;
1975
- const viewport = page.capture?.by.viewport(page.pageId) ?? {
1976
- width: 0,
1977
- height: 0
1978
- };
1979
- lastTimestamp = Math.max(lastTimestamp, timestamp);
1980
- return Effect.sync(() => {
1981
- running.receive({
1982
- data: frame.bytes ?? jpegFrame(),
1983
- timestamp,
1984
- viewportWidth: frame.viewportWidth ?? viewport.width,
1985
- viewportHeight: frame.viewportHeight ?? viewport.height
1986
- });
1987
- });
1988
- })) },
1989
- invoke: (name, input, invokeOptions) => Effect.promise(() => {
1990
- const open = [...live].at(-1);
1991
- return open === void 0 ? Promise.resolve({ ok: false }) : open.invoke(name, input, invokeOptions?.origin);
1992
- }),
1993
- disconnect: Effect.sync(() => {
1994
- for (const connection of [...live]) connection.drop();
1995
- }),
1996
- connections: Effect.sync(() => [...connections])
1997
- }
1998
- };
1999
- };
2000
- //#endregion
2001
- //#region src/internal/testing/Lifetime.ts
2002
- /**
2003
- * A lifetime with nothing outside the connection: no process to terminate and no provider to
2004
- * release. Cleanup runs the same local steps an owned Chromium runs, each settling on its own,
2005
- * and reports the same kind of receipt.
2006
- */
2007
- const scriptedSource = (reference, onCleanup) => (local) => Effect.gen(function* () {
2008
- let latest;
2009
- let closing = false;
2010
- const close = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
2011
- const issues = [];
2012
- const step = (name, action) => cleanupStep(action, 3e3).pipe(Effect.tap((result) => Effect.sync(() => {
2013
- if (result._tag === "Failure") issues.push(ScriptedCleanupIssue.make({
2014
- step: name,
2015
- reason: result.reason
2016
- }));
2017
- })));
2018
- yield* step("fence", local.fence);
2019
- yield* step("capture", local.capture);
2020
- yield* step("initialization", local.initialization);
2021
- const disconnected = yield* step("disconnect", local.disconnect);
2022
- const result = ScriptedCleanupResult.make({
2023
- reference,
2024
- connection: disconnected._tag === "Success" ? disconnected.value : "failed",
2025
- issues
2026
- });
2027
- Object.freeze(result.issues);
2028
- latest = Object.freeze(result);
2029
- return latest;
2030
- })));
2031
- const release = yield* Effect.cached(Effect.gen(function* () {
2032
- closing = true;
2033
- const result = yield* close;
2034
- if (onCleanup !== void 0) yield* reported(Effect.suspend(() => onCleanup(result)));
2035
- return result;
2036
- }).pipe(Effect.uninterruptible));
2037
- yield* Effect.addFinalizer(() => release.pipe(Effect.asVoid));
2038
- return {
2039
- reference,
2040
- connection: () => Effect.suspend(() => closing ? Effect.fail(BrowserError.make({
2041
- operation: "connect",
2042
- reason: Reasons.Closed.make({}),
2043
- outcome: "undispatched"
2044
- })) : Effect.succeed(Redacted.make(`scripted://${reference.id}`))),
2045
- release,
2046
- closeChecked: release.pipe(Effect.filterOrFail((result) => result.connection === "closed" && result.issues.length === 0, () => BrowserError.make({
2047
- operation: "close",
2048
- reason: Reasons.Failed.make({}),
2049
- outcome: "unknown"
2050
- }))),
2051
- cleanupResult: Effect.sync(() => Option.fromNullishOr(latest)),
2052
- controlRetired: Effect.sync(() => latest !== void 0 && latest.connection === "closed")
2053
- };
2054
- });
2055
- //#endregion
2056
- //#region src/Testing.ts
2057
- let references = 0;
2058
- const makeEngine = Effect.fnUntraced(function* (script) {
2059
- const context = yield* Effect.context();
2060
- const browsers = /* @__PURE__ */ new Map();
2061
- const timers = { sleep: (millis) => {
2062
- const controller = new AbortController();
2063
- const done = Effect.runPromiseWith(context)(Effect.sleep(millis), { signal: controller.signal });
2064
- done.catch(() => {});
2065
- return {
2066
- done,
2067
- cancel: () => controller.abort()
2068
- };
2069
- } };
2070
- const attempt = async (request) => {
2071
- const address = Redacted.isRedacted(request.connection) ? String(Redacted.value(request.connection)) : String(request.connection);
2072
- let browser = browsers.get(address);
2073
- if (browser === void 0) {
2074
- browser = makeScriptedBrowser(script, timers);
2075
- browsers.set(address, browser);
2076
- }
2077
- return browser.connect(request.options, request.events);
2078
- };
2079
- return {
2080
- binding: issueBinding({ _tag: "BrowserBinding" }, fromNativeAttempt(attempt)),
2081
- controls: () => [...browsers.values()].map((browser) => browser.control)
2082
- };
2083
- });
2084
- /**
2085
- * Open one scripted browser in the caller's Scope. The session is the real owner over a
2086
- * scripted native engine: admission, budgets, staleness, dispatch evidence, capture, page holds
2087
- * and typed callbacks are the production code paths. Only the page and its outcomes are scripted,
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.
2090
- */
2091
- const open = Effect.fnUntraced(function* (script, options = {}) {
2092
- const fixed = yield* checked(Script, script, "configure");
2093
- const policy = yield* checked(BrowserPolicy, options.policy ?? BrowserPolicy.unrestricted(), "configure");
2094
- const engine = yield* makeEngine(fixed);
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({
2101
- implementation: "scripted",
2102
- binding: engine.binding,
2103
- ...options.automation === void 0 ? {} : { automation: options.automation },
2104
- ...options.viewport === void 0 ? {} : { viewport: options.viewport }
2105
- }).pipe(Effect.provide(Layer.sync(Crypto.Crypto, () => makeSequentialCrypto(namespace))))).acquire(policy, scriptedSource(reference, options.onCleanup), options.bootstrap === void 0 ? {} : { bootstrap: options.bootstrap });
2106
- const connection = yield* acquired.connect;
2107
- const control = engine.controls().at(-1);
2108
- if (control === void 0) return yield* BrowserError.make({
2109
- operation: "connect",
2110
- reason: Reasons.Provider.make({}),
2111
- outcome: "unknown"
2112
- });
2113
- return Object.assign(connection.session, {
2114
- reference,
2115
- control,
2116
- closeChecked: connection.session.closeChecked.pipe(Effect.andThen(acquired.lifetime.closeChecked)),
2117
- close: acquired.close,
2118
- cleanupResult: acquired.lifetime.cleanupResult
2119
- });
2120
- });
2121
- /**
2122
- * The `Crypto` scripted browsers draw their ids and handoff tokens from. Its bytes count up from
2123
- * one on each build, so every value is predictable; it is not random and computes no digest.
2124
- * `open` uses the same counter with its original scripted-reference namespace. A provider
2125
- * Layer built over `binding` still requires a `Crypto`, and
2126
- * this one keeps such a test free of a platform package.
2127
- */
2128
- const sequentialCrypto = Layer.sync(Crypto.Crypto, makeSequentialCrypto);
2129
- /**
2130
- * The same engine for a provider Layer built on `browser-runtime`, such as
2131
- * `BrowserbaseBrowser.layer` with `BrowserBinding.layer(binding)`. Each address the Layer
2132
- * connects to is one browser with one control handle; reconnecting to it finds its pages.
2133
- */
2134
- const binding = Effect.fnUntraced(function* (script) {
2135
- const fixed = yield* checked(Script, script, "configure");
2136
- const engine = yield* makeEngine(fixed);
2137
- return {
2138
- binding: engine.binding,
2139
- browsers: Effect.sync(engine.controls)
2140
- };
2141
- });
2142
- /** A valid 64×48 JPEG: the default bytes of a scripted capture frame, for a caller's own frames. */
2143
- const jpeg = jpegFrame;
2144
- //#endregion
2145
- export { ControlFactsScript, ControlScript, DocumentScript, Script, ScriptableOperation, ScriptedCleanupIssue, ScriptedCleanupResult, ScriptedReference, binding, jpeg, open, sequentialCrypto };
2146
-
2147
- //# sourceMappingURL=Testing.mjs.map