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

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 (205) hide show
  1. package/README.md +861 -1161
  2. package/dist/Browser-B4qncxe7.mjs +628 -0
  3. package/dist/Browser-B4qncxe7.mjs.map +1 -0
  4. package/dist/Browser.d.mts +129 -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-Di2PxyF7.d.mts +119 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +125 -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-B7X6DQUn.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-Akcac63g.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-BAf__WKX.mjs +5075 -0
  73. package/dist/page-BAf__WKX.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 +672 -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 +132 -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 +391 -0
  113. package/src/internal/page/context.ts +314 -0
  114. package/src/internal/page/identity.ts +35 -0
  115. package/src/internal/page/lane.ts +311 -0
  116. package/src/internal/page/navigation.ts +183 -0
  117. package/src/internal/page/openings.ts +87 -0
  118. package/src/internal/page/page.ts +165 -0
  119. package/src/internal/page/url.ts +100 -0
  120. package/src/internal/page/viewport.ts +43 -0
  121. package/src/internal/pictures/capture.ts +776 -0
  122. package/src/internal/pictures/clock.ts +141 -0
  123. package/src/internal/pictures/counts.ts +92 -0
  124. package/src/internal/pictures/pictures.ts +521 -0
  125. package/src/internal/pictures/transport.ts +243 -0
  126. package/src/internal/reading/choose.ts +100 -0
  127. package/src/internal/reading/context.inpage.ts +201 -0
  128. package/src/internal/reading/controls.inpage.ts +158 -0
  129. package/src/internal/reading/expect.inpage.ts +70 -0
  130. package/src/internal/reading/match.inpage.ts +92 -0
  131. package/src/internal/reading/names.inpage.ts +306 -0
  132. package/src/internal/reading/outline.inpage.ts +260 -0
  133. package/src/internal/reading/reading.ts +223 -0
  134. package/src/internal/reading/ready.inpage.ts +164 -0
  135. package/src/internal/reading/ready.ts +111 -0
  136. package/src/internal/reading/subjects.inpage.ts +188 -0
  137. package/src/internal/reading/text.inpage.ts +135 -0
  138. package/src/internal/reading/walk.inpage.ts +220 -0
  139. package/src/internal/supervisor/lifecycle.ts +416 -0
  140. package/src/internal/timeline/changes.inpage.ts +216 -0
  141. package/src/internal/timeline/changes.ts +100 -0
  142. package/src/internal/timeline/events.ts +94 -0
  143. package/src/internal/timeline/fold.inpage.ts +123 -0
  144. package/src/internal/timeline/history.inpage.ts +229 -0
  145. package/src/internal/timeline/marks.inpage.ts +207 -0
  146. package/src/internal/timeline/record.inpage.ts +133 -0
  147. package/src/internal/timeline/sight.inpage.ts +122 -0
  148. package/src/internal/timeline/window.ts +96 -0
  149. package/src/internal/timeline/words.ts +65 -0
  150. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  151. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  152. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  153. package/dist/Bootstrap.d.mts +0 -3
  154. package/dist/Bootstrap.mjs +0 -2
  155. package/dist/Browser-BF9_sORP.d.mts +0 -388
  156. package/dist/Browser.mjs.map +0 -1
  157. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  158. package/dist/BrowserData.d.mts +0 -3
  159. package/dist/BrowserData.mjs +0 -695
  160. package/dist/BrowserData.mjs.map +0 -1
  161. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  163. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  164. package/dist/BrowserRuntime.d.mts +0 -3
  165. package/dist/BrowserRuntime.mjs +0 -3
  166. package/dist/Capture.d.mts +0 -56
  167. package/dist/Capture.mjs +0 -678
  168. package/dist/Capture.mjs.map +0 -1
  169. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  170. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  171. package/dist/CaptureData.d.mts +0 -219
  172. package/dist/CaptureData.mjs +0 -2
  173. package/dist/Errors.d.mts +0 -158
  174. package/dist/Errors.mjs +0 -207
  175. package/dist/Errors.mjs.map +0 -1
  176. package/dist/Images-yCG9BmRU.mjs +0 -76
  177. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  178. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  179. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  180. package/dist/PageControl.d.mts +0 -15
  181. package/dist/PageControl.mjs +0 -34
  182. package/dist/PageControl.mjs.map +0 -1
  183. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  184. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  185. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  186. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  187. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  188. package/dist/PlanData.d.mts +0 -3
  189. package/dist/PlanData.mjs +0 -268
  190. package/dist/PlanData.mjs.map +0 -1
  191. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  192. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  193. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  194. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  195. package/dist/Testing.d.mts +0 -479
  196. package/dist/Testing.mjs +0 -2147
  197. package/dist/Testing.mjs.map +0 -1
  198. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  199. package/dist/Timeline.d.mts +0 -4
  200. package/dist/Timeline.mjs +0 -20
  201. package/dist/Timeline.mjs.map +0 -1
  202. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  203. package/dist/TimelineData.d.mts +0 -3
  204. package/dist/TimelineData.mjs +0 -482
  205. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,293 @@
1
+ /**
2
+ * In the page: where input lands. It resolves a ref or a point to the control under it, in
3
+ * top-document viewport pixels, with the subject's context, and plans a wheel scroll that brings a
4
+ * ref into view. See `reading/names.inpage.ts` for what a page-side part may use.
5
+ */
6
+ import { Schema } from "effect";
7
+
8
+ import { Box, SubjectContext } from "../../BrowserEvent.ts";
9
+ import type { ContextReader } from "../reading/context.inpage.ts";
10
+ import type { Names } from "../reading/names.inpage.ts";
11
+ import type { Walk } from "../reading/walk.inpage.ts";
12
+
13
+ export type ResolvedPoint = typeof ResolvedPointSchema.Type;
14
+ export type PointResult = typeof PointResultSchema.Type;
15
+
16
+ export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
17
+ const { describe, isDisabled, lookup, nameOf, parentOf, roleOf } = names;
18
+ const { boxOf, controlOf, hitAt } = walked;
19
+ const { contextOf } = placing;
20
+
21
+ /** Whether `node` is `ancestor` or inside it, across shadow roots and same-origin frames. */
22
+ const within = (ancestor: Element, node: Element): boolean => {
23
+ for (let current: Element | null = node; current !== null;) {
24
+ if (current === ancestor) return true;
25
+ current = parentOf(current) ?? current.ownerDocument.defaultView?.frameElement ?? null;
26
+ }
27
+
28
+ return false;
29
+ };
30
+
31
+ // A press at a point reaches the element when the top-document hit is the element, inside it,
32
+ // or one of its containers (whose activation the element's own facts already cover).
33
+ const receives = (element: Element, hit: Element): boolean =>
34
+ within(element, hit) || within(hit, element);
35
+
36
+ // HTML links, SVG links and image-map areas all navigate; SVG keeps lowercase tag names.
37
+ const hrefAttribute = (element: Element): string | null =>
38
+ element.localName === "a" || element.localName === "area"
39
+ ? (element.getAttribute("href") ??
40
+ element.getAttributeNS("http://www.w3.org/1999/xlink", "href"))
41
+ : null;
42
+
43
+ const linkOf = (element: Element): Element | undefined => {
44
+ for (let node: Element | null = element; node !== null; node = parentOf(node))
45
+ if (hrefAttribute(node) !== null) return node;
46
+
47
+ return undefined;
48
+ };
49
+
50
+ const details = (
51
+ element: Element,
52
+ hit: Element,
53
+ x: number,
54
+ y: number,
55
+ ): Omit<ResolvedPoint, "context"> => {
56
+ const link = linkOf(element);
57
+ let href: string | undefined;
58
+
59
+ if (link !== undefined) {
60
+ try {
61
+ href = new URL(hrefAttribute(link) ?? "", link.ownerDocument.baseURI).href;
62
+ } catch {
63
+ href = undefined;
64
+ }
65
+ }
66
+ const role = roleOf(element);
67
+ const view = hit.ownerDocument.defaultView ?? window;
68
+
69
+ return {
70
+ x,
71
+ y,
72
+ element: describe(element),
73
+ tag: element.tagName.toLowerCase(),
74
+ role,
75
+ name: nameOf(element, role),
76
+ cursor: view.getComputedStyle(hit).cursor,
77
+ ...(href === undefined ? {} : { href }),
78
+ };
79
+ };
80
+
81
+ const point = (
82
+ target: string | { readonly x: number; readonly y: number },
83
+ scroll = true,
84
+ ): PointResult => {
85
+ if (typeof target !== "string") {
86
+ const { x, y } = target;
87
+
88
+ if (
89
+ !Number.isFinite(x) ||
90
+ !Number.isFinite(y) ||
91
+ x < 0 ||
92
+ y < 0 ||
93
+ x >= window.innerWidth ||
94
+ y >= window.innerHeight
95
+ )
96
+ return { error: "outside", detail: `(${x}, ${y}) is outside the viewport` };
97
+ const hit = hitAt(document, x, y);
98
+
99
+ if (hit === null) return { error: "offscreen", detail: `nothing is painted at (${x}, ${y})` };
100
+ const control = controlOf(hit);
101
+
102
+ return { ...details(control, hit, x, y), context: contextOf(control), box: boxOf(control) };
103
+ }
104
+ const ref = target;
105
+ const element = lookup(ref);
106
+
107
+ if (element === undefined)
108
+ return { error: "stale", detail: `${ref} is not on the page any more` };
109
+ if (isDisabled(element)) return { error: "disabled", detail: `${ref} is disabled` };
110
+ const rect = element.getBoundingClientRect();
111
+
112
+ if (rect.width === 0 && rect.height === 0)
113
+ return { error: "hidden", detail: `${ref} has no size on the page` };
114
+
115
+ // Input arrives in top-document viewport pixels, so geometry and occlusion are measured
116
+ // there: a frame's own viewport cannot say whether the frame is scrolled away or covered.
117
+ let box = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
118
+ let visible = { ...box };
119
+ let outside = false;
120
+
121
+ for (let inner: Element = element; ;) {
122
+ const view = inner.ownerDocument.defaultView ?? window;
123
+
124
+ outside ||=
125
+ box.left < 0 || box.top < 0 || box.right > view.innerWidth || box.bottom > view.innerHeight;
126
+ visible = {
127
+ left: Math.max(visible.left, 0),
128
+ top: Math.max(visible.top, 0),
129
+ right: Math.min(visible.right, view.innerWidth),
130
+ bottom: Math.min(visible.bottom, view.innerHeight),
131
+ };
132
+
133
+ // A field can be inside the viewport and still clipped by a nested scrolling panel.
134
+ for (let ancestor = parentOf(inner); ancestor !== null; ancestor = parentOf(ancestor)) {
135
+ const style = view.getComputedStyle(ancestor);
136
+ const clip = ancestor.getBoundingClientRect();
137
+
138
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
139
+ visible.left = Math.max(visible.left, clip.left + ancestor.clientLeft);
140
+ visible.right = Math.min(
141
+ visible.right,
142
+ clip.left + ancestor.clientLeft + ancestor.clientWidth,
143
+ );
144
+ }
145
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
146
+ visible.top = Math.max(visible.top, clip.top + ancestor.clientTop);
147
+ visible.bottom = Math.min(
148
+ visible.bottom,
149
+ clip.top + ancestor.clientTop + ancestor.clientHeight,
150
+ );
151
+ }
152
+ }
153
+
154
+ const frame = view.frameElement;
155
+
156
+ if (frame === null) break;
157
+ const frameBox = frame.getBoundingClientRect();
158
+ const dx = frameBox.left + frame.clientLeft;
159
+ const dy = frameBox.top + frame.clientTop;
160
+
161
+ box = {
162
+ left: box.left + dx,
163
+ top: box.top + dy,
164
+ right: box.right + dx,
165
+ bottom: box.bottom + dy,
166
+ };
167
+ visible = {
168
+ left: Math.max(visible.left + dx, dx),
169
+ top: Math.max(visible.top + dy, dy),
170
+ right: Math.min(visible.right + dx, dx + frame.clientWidth),
171
+ bottom: Math.min(visible.bottom + dy, dy + frame.clientHeight),
172
+ };
173
+ inner = frame;
174
+ }
175
+
176
+ if (outside || visible.right <= visible.left || visible.bottom <= visible.top) {
177
+ if (scroll) {
178
+ element.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
179
+
180
+ return point(target, false);
181
+ }
182
+
183
+ return {
184
+ error: "offscreen",
185
+ detail: outside
186
+ ? ref + " is outside the viewport"
187
+ : `${ref} could not be scrolled into view`,
188
+ };
189
+ }
190
+ const x = (visible.left + visible.right) / 2;
191
+ const y = (visible.top + visible.bottom) / 2;
192
+ const hit = hitAt(document, x, y);
193
+
194
+ if (hit !== null && !receives(element, hit))
195
+ return { error: "covered", detail: `${ref} is covered by ${describe(hit)}` };
196
+
197
+ return {
198
+ ...details(element, hit ?? element, Math.round(x), Math.round(y)),
199
+ context: contextOf(element),
200
+ };
201
+ };
202
+
203
+ // Suggest one visible wheel origin. Unsupported frame geometry and fully clipped panels use
204
+ // the bounded instant fallback; guessing their coordinates could scroll an unrelated control.
205
+ const scrollPlan = (ref: string) => {
206
+ const element = lookup(ref);
207
+
208
+ if (element === undefined || element.ownerDocument !== document) return null;
209
+ const target = element.getBoundingClientRect();
210
+ const centerX = (target.left + target.right) / 2;
211
+ const centerY = (target.top + target.bottom) / 2;
212
+
213
+ for (let ancestor = parentOf(element); ancestor !== null; ancestor = parentOf(ancestor)) {
214
+ const style = window.getComputedStyle(ancestor);
215
+
216
+ const horizontal =
217
+ /^(auto|scroll)$/.test(style.overflowX) && ancestor.scrollWidth > ancestor.clientWidth;
218
+
219
+ const vertical =
220
+ /^(auto|scroll)$/.test(style.overflowY) && ancestor.scrollHeight > ancestor.clientHeight;
221
+
222
+ if (!horizontal && !vertical) continue;
223
+ const box = ancestor.getBoundingClientRect();
224
+ const left = Math.max(0, box.left + ancestor.clientLeft);
225
+ const top = Math.max(0, box.top + ancestor.clientTop);
226
+
227
+ const right = Math.min(
228
+ window.innerWidth,
229
+ box.left + ancestor.clientLeft + ancestor.clientWidth,
230
+ );
231
+
232
+ const bottom = Math.min(
233
+ window.innerHeight,
234
+ box.top + ancestor.clientTop + ancestor.clientHeight,
235
+ );
236
+
237
+ if (right <= left || bottom <= top) continue;
238
+ const x = (left + right) / 2;
239
+ const y = (top + bottom) / 2;
240
+ const dx = horizontal && (target.left < left || target.right > right) ? centerX - x : 0;
241
+ const dy = vertical && (target.top < top || target.bottom > bottom) ? centerY - y : 0;
242
+
243
+ if (dx === 0 && dy === 0) continue;
244
+ const hit = hitAt(document, x, y);
245
+
246
+ if (hit !== null && (hit === ancestor || ancestor.contains(hit))) return { x, y, dx, dy };
247
+ }
248
+
249
+ const dx =
250
+ target.left < 0 || target.right > window.innerWidth ? centerX - window.innerWidth / 2 : 0;
251
+
252
+ const dy =
253
+ target.top < 0 || target.bottom > window.innerHeight ? centerY - window.innerHeight / 2 : 0;
254
+
255
+ return dx === 0 && dy === 0
256
+ ? null
257
+ : { x: window.innerWidth / 2, y: window.innerHeight / 2, dx, dy };
258
+ };
259
+
260
+ return { controlOf, details, hitAt, hrefAttribute, linkOf, point, receives, scrollPlan, within };
261
+ };
262
+
263
+ export type Targets = ReturnType<typeof targets>;
264
+
265
+ const ResolvedPointSchema = Schema.Struct({
266
+ x: Schema.Finite,
267
+ y: Schema.Finite,
268
+ element: Schema.String,
269
+ tag: Schema.String,
270
+ role: Schema.NullOr(Schema.String),
271
+ name: Schema.String,
272
+ context: SubjectContext,
273
+ box: Schema.optional(Box),
274
+ cursor: Schema.String,
275
+ href: Schema.optional(Schema.String),
276
+ });
277
+
278
+ export const PointResultSchema = Schema.Union([
279
+ ResolvedPointSchema,
280
+ Schema.Struct({
281
+ error: Schema.Literals(["stale", "hidden", "offscreen", "disabled", "covered", "outside"]),
282
+ detail: Schema.String,
283
+ }),
284
+ ]);
285
+
286
+ export const ScrollPlanSchema = Schema.NullOr(
287
+ Schema.Struct({
288
+ x: Schema.Finite,
289
+ y: Schema.Finite,
290
+ dx: Schema.Finite,
291
+ dy: Schema.Finite,
292
+ }),
293
+ );
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Where an action's input goes: a ref or a point resolved to a control and a viewport point,
3
+ * scrolled into view first when it is outside the viewport: at once, or with the wheel where
4
+ * viewers watch.
5
+ */
6
+ import { Effect } from "effect";
7
+
8
+ import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
9
+ import { ResolvedTarget, type Target } from "../../Page.ts";
10
+ import { type Bridge, scriptCall } from "../page/bridge.ts";
11
+ import { decodeWith, failWith } from "../page/context.ts";
12
+ import type { Viewport } from "../page/viewport.ts";
13
+ import type { Dispatch } from "./dispatch.ts";
14
+ import type { Approval, Guard } from "./guard.ts";
15
+ import type { InputMarks } from "./perform.ts";
16
+ import type { Pointer } from "./pointer.ts";
17
+ import type { Settle } from "./settle.ts";
18
+ import * as Script from "./targets.inpage.ts";
19
+
20
+ const pointFailure = (
21
+ operation: string,
22
+ target: Target,
23
+ result: Extract<Script.PointResult, { readonly error: string }>,
24
+ ) =>
25
+ failWith(
26
+ operation,
27
+ result.error === "stale" && typeof target === "string"
28
+ ? new StaleRef({ ref: target })
29
+ : result.error === "outside"
30
+ ? new InvalidRequest({ detail: result.detail })
31
+ : new NotActionable({ detail: result.detail }),
32
+ );
33
+
34
+ const resolvedTarget = (result: Script.ResolvedPoint): ResolvedTarget =>
35
+ new ResolvedTarget({
36
+ point: { x: result.x, y: result.y },
37
+ element: result.element,
38
+ tag: result.tag,
39
+ role: result.role,
40
+ name: result.name,
41
+ context: result.context,
42
+ box: result.box,
43
+ cursor: result.cursor,
44
+ ...(result.href === undefined ? {} : { href: result.href }),
45
+ });
46
+
47
+ export const make = (
48
+ bridge: Bridge,
49
+ viewport: Viewport,
50
+ guard: Guard,
51
+ pointer: Pointer,
52
+ dispatch: Dispatch,
53
+ settling: Settle,
54
+ ) => {
55
+ const { evaluate } = bridge;
56
+ const { mutate } = guard;
57
+ const { moveTo, wheel } = pointer;
58
+ const { flush } = dispatch;
59
+
60
+ const readPoint = (
61
+ operation: string,
62
+ target: Target,
63
+ approval: Approval | undefined,
64
+ scroll: boolean,
65
+ ) =>
66
+ (approval === undefined
67
+ ? evaluate(operation, scriptCall("point", target, scroll))
68
+ : mutate(operation, scriptCall("point", target, scroll), approval)
69
+ ).pipe(Effect.flatMap(decodeWith(operation, Script.PointResultSchema)));
70
+
71
+ const resolve = (operation: string, target: Target, approval?: Approval, scroll = true) => {
72
+ if (typeof target === "string" && !/^e\d+$/.test(target))
73
+ return failWith(
74
+ operation,
75
+ new InvalidRequest({ detail: target + " is not a ref; refs look like e12" }),
76
+ );
77
+ if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y)))
78
+ return failWith(
79
+ operation,
80
+ new InvalidRequest({ detail: "point coordinates must be finite" }),
81
+ );
82
+
83
+ return readPoint(operation, target, approval, scroll).pipe(
84
+ Effect.flatMap((result) =>
85
+ "error" in result
86
+ ? pointFailure(operation, target, result)
87
+ : Effect.succeed(resolvedTarget(result)),
88
+ ),
89
+ );
90
+ };
91
+
92
+ const targetFor = Effect.fnUntraced(function* (
93
+ operation: string,
94
+ target: Target,
95
+ approval: Approval | undefined,
96
+ marks: InputMarks,
97
+ ) {
98
+ if (!marks.style.shown || typeof target !== "string")
99
+ return yield* resolve(operation, target, approval);
100
+
101
+ // Inspection and policy approval happen before this point. Every retry is geometry only;
102
+ // scrolling never obtains a fresh approval or silently follows a replacement document.
103
+ for (let attempt = 0; attempt < 3; attempt++) {
104
+ const result = yield* readPoint(operation, target, approval, false);
105
+
106
+ if (!("error" in result)) return resolvedTarget(result);
107
+ if (result.error !== "offscreen") return yield* pointFailure(operation, target, result);
108
+
109
+ const plan = yield* mutate(operation, scriptCall("scrollPlan", target), approval).pipe(
110
+ Effect.flatMap(decodeWith(operation, Script.ScrollPlanSchema)),
111
+ );
112
+
113
+ if (plan === null) break;
114
+ const size = yield* viewport.viewportFor(operation);
115
+ const point = { x: plan.x, y: plan.y };
116
+
117
+ yield* marks.at(point);
118
+ yield* moveTo(operation, marks, point);
119
+ const before = settling.mark();
120
+
121
+ yield* wheel(
122
+ operation,
123
+ marks,
124
+ point,
125
+ Math.max(-size.width * 0.9, Math.min(size.width * 0.9, plan.dx)),
126
+ Math.max(-size.height * 0.9, Math.min(size.height * 0.9, plan.dy)),
127
+ );
128
+ yield* flush(operation, marks.input);
129
+ yield* settling.settle(operation, before);
130
+ if (approval !== undefined) yield* approval.check();
131
+ }
132
+
133
+ // Wheels may be prevented or the target may need unsupported nested/frame geometry. One
134
+ // explicit fallback preserves reachability without a distance-dependent protocol loop.
135
+ yield* marks.touched;
136
+ const before = settling.mark();
137
+
138
+ yield* resolve(operation, target, approval);
139
+ yield* settling.settle(operation, before);
140
+ if (approval !== undefined) yield* approval.check();
141
+
142
+ return yield* resolve(operation, target, approval, false);
143
+ });
144
+
145
+ return { resolve, targetFor };
146
+ };
147
+
148
+ export type Targets = ReturnType<typeof make>;