effect-browser 0.2.0-beta.8 → 0.3.0-beta.5

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 (202) hide show
  1. package/README.md +705 -1166
  2. package/dist/Browser-CM65Uo7f.mjs +437 -0
  3. package/dist/Browser-CM65Uo7f.mjs.map +1 -0
  4. package/dist/Browser.d.mts +115 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +202 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +295 -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-DVP2IJWm.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-D4281dIi.mjs +618 -0
  39. package/dist/Page-D4281dIi.mjs.map +1 -0
  40. package/dist/Page-DYTdVhYa.d.mts +593 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-CNNOU1Tx.d.mts +112 -0
  44. package/dist/Plan-Dtrb-8im.mjs +311 -0
  45. package/dist/Plan-Dtrb-8im.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 +54 -0
  59. package/dist/Stage.mjs +133 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-BtEG93gR.mjs +420 -0
  62. package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
  63. package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-DAYjATgC.mjs +137 -0
  67. package/dist/context-DAYjATgC.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-Banfm_zc.mjs +5097 -0
  73. package/dist/page-Banfm_zc.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-CcL0K7S0.mjs +262 -0
  76. package/dist/url-CcL0K7S0.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +625 -0
  79. package/src/BrowserError.ts +225 -0
  80. package/src/BrowserEvent.ts +294 -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 +520 -0
  89. package/src/Plan.ts +350 -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 +223 -0
  94. package/src/Supervisor.ts +353 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +607 -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/guard.inpage.ts +605 -0
  101. package/src/internal/input/guard.ts +183 -0
  102. package/src/internal/input/keyboard.ts +112 -0
  103. package/src/internal/input/keys.ts +127 -0
  104. package/src/internal/input/perform.ts +322 -0
  105. package/src/internal/input/pointer.ts +153 -0
  106. package/src/internal/input/replies.ts +159 -0
  107. package/src/internal/input/settle.ts +50 -0
  108. package/src/internal/input/style.ts +186 -0
  109. package/src/internal/input/targets.inpage.ts +293 -0
  110. package/src/internal/input/targets.ts +148 -0
  111. package/src/internal/page/bridge.ts +386 -0
  112. package/src/internal/page/context.ts +302 -0
  113. package/src/internal/page/lane.ts +301 -0
  114. package/src/internal/page/navigation.ts +183 -0
  115. package/src/internal/page/openings.ts +87 -0
  116. package/src/internal/page/page.ts +137 -0
  117. package/src/internal/page/url.ts +100 -0
  118. package/src/internal/page/viewport.ts +43 -0
  119. package/src/internal/pictures/capture.ts +778 -0
  120. package/src/internal/pictures/clock.ts +141 -0
  121. package/src/internal/pictures/pictures.ts +520 -0
  122. package/src/internal/pictures/transport.ts +242 -0
  123. package/src/internal/reading/choose.ts +100 -0
  124. package/src/internal/reading/context.inpage.ts +201 -0
  125. package/src/internal/reading/controls.inpage.ts +158 -0
  126. package/src/internal/reading/expect.inpage.ts +70 -0
  127. package/src/internal/reading/match.inpage.ts +92 -0
  128. package/src/internal/reading/names.inpage.ts +306 -0
  129. package/src/internal/reading/outline.inpage.ts +260 -0
  130. package/src/internal/reading/reading.ts +223 -0
  131. package/src/internal/reading/ready.inpage.ts +154 -0
  132. package/src/internal/reading/ready.ts +105 -0
  133. package/src/internal/reading/subjects.inpage.ts +188 -0
  134. package/src/internal/reading/text.inpage.ts +135 -0
  135. package/src/internal/reading/walk.inpage.ts +220 -0
  136. package/src/internal/supervisor/lifecycle.ts +265 -0
  137. package/src/internal/timeline/changes.inpage.ts +216 -0
  138. package/src/internal/timeline/changes.ts +100 -0
  139. package/src/internal/timeline/events.ts +93 -0
  140. package/src/internal/timeline/fold.inpage.ts +123 -0
  141. package/src/internal/timeline/history.inpage.ts +229 -0
  142. package/src/internal/timeline/marks.inpage.ts +207 -0
  143. package/src/internal/timeline/record.inpage.ts +133 -0
  144. package/src/internal/timeline/sight.inpage.ts +122 -0
  145. package/src/internal/timeline/window.ts +96 -0
  146. package/src/internal/timeline/words.ts +65 -0
  147. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  148. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  149. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  150. package/dist/Bootstrap.d.mts +0 -3
  151. package/dist/Bootstrap.mjs +0 -2
  152. package/dist/Browser-n-bKgBNN.d.mts +0 -372
  153. package/dist/Browser.mjs.map +0 -1
  154. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  155. package/dist/BrowserData.d.mts +0 -3
  156. package/dist/BrowserData.mjs +0 -695
  157. package/dist/BrowserData.mjs.map +0 -1
  158. package/dist/BrowserRuntime-CFe9uUTv.d.mts +0 -148
  159. package/dist/BrowserRuntime-HJ1yYlFS.mjs +0 -6741
  160. package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +0 -1
  161. package/dist/BrowserRuntime.d.mts +0 -3
  162. package/dist/BrowserRuntime.mjs +0 -3
  163. package/dist/Capture.d.mts +0 -56
  164. package/dist/Capture.mjs +0 -678
  165. package/dist/Capture.mjs.map +0 -1
  166. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  167. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  168. package/dist/CaptureData.d.mts +0 -219
  169. package/dist/CaptureData.mjs +0 -2
  170. package/dist/Errors.d.mts +0 -158
  171. package/dist/Errors.mjs +0 -207
  172. package/dist/Errors.mjs.map +0 -1
  173. package/dist/Images-CaaPNeed.mjs +0 -76
  174. package/dist/Images-CaaPNeed.mjs.map +0 -1
  175. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  176. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  177. package/dist/PageControl.d.mts +0 -15
  178. package/dist/PageControl.mjs +0 -34
  179. package/dist/PageControl.mjs.map +0 -1
  180. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  181. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  182. package/dist/Plan-D8qQ48Ic.mjs +0 -512
  183. package/dist/Plan-D8qQ48Ic.mjs.map +0 -1
  184. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  185. package/dist/PlanData.d.mts +0 -3
  186. package/dist/PlanData.mjs +0 -268
  187. package/dist/PlanData.mjs.map +0 -1
  188. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  189. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  190. package/dist/PublicSession-Bs61Eal8.mjs +0 -5601
  191. package/dist/PublicSession-Bs61Eal8.mjs.map +0 -1
  192. package/dist/Testing.d.mts +0 -479
  193. package/dist/Testing.mjs +0 -2147
  194. package/dist/Testing.mjs.map +0 -1
  195. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  196. package/dist/Timeline.d.mts +0 -4
  197. package/dist/Timeline.mjs +0 -20
  198. package/dist/Timeline.mjs.map +0 -1
  199. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  200. package/dist/TimelineData.d.mts +0 -3
  201. package/dist/TimelineData.mjs +0 -482
  202. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,607 @@
1
+ /**
2
+ * The page's actions: click, hover, drag, type, press, scroll and select, each sent in a style,
3
+ * plainly or performed for viewers, and the aim a performed click starts early. Each runs through
4
+ * `perform`, which admits it, puts it to the input guard and records it. A click, a key or a scroll
5
+ * then waits for the page to settle: a task and a frame, and a document if the input asked for one.
6
+ */
7
+ import { Duration, Effect } from "effect";
8
+
9
+ import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
10
+ import type { ClickOptions, PressOptions, ScrollOptions, Target, TypeOptions } from "../../Page.ts";
11
+ import { type Bridge, scriptCall } from "../page/bridge.ts";
12
+ import { decodeWith, failWith, type PageContext } from "../page/context.ts";
13
+ import type { Viewport } from "../page/viewport.ts";
14
+ import * as Dispatch from "./dispatch.ts";
15
+ import * as Script from "./edit.inpage.ts";
16
+ import * as Guard from "./guard.ts";
17
+ import * as Keyboard from "./keyboard.ts";
18
+ import * as Keys from "./keys.ts";
19
+ import * as Perform from "./perform.ts";
20
+ import * as Pointer from "./pointer.ts";
21
+ import * as Settle from "./settle.ts";
22
+ import { plain, type Style } from "./style.ts";
23
+ import * as Targets from "./targets.ts";
24
+
25
+ const buttonMask = { none: 0, left: 1, right: 2, middle: 4 } as const;
26
+
27
+ const partsOf = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
28
+ const dispatch = Dispatch.make(page);
29
+ const pointer = Pointer.make(page, dispatch, viewport);
30
+ const guard = Guard.make(page, bridge);
31
+ const settling = Settle.make(page, bridge);
32
+
33
+ return {
34
+ page,
35
+ bridge,
36
+ ...settling,
37
+ viewportFor: viewport.viewportFor,
38
+ perform: Perform.make(page, dispatch),
39
+ ...guard,
40
+ ...Targets.make(bridge, viewport, guard, pointer, dispatch, settling),
41
+ ...pointer,
42
+ ...Keyboard.make(page, bridge, dispatch),
43
+ sendMouse: dispatch.sendMouse,
44
+ flush: dispatch.flush,
45
+ };
46
+ };
47
+
48
+ type Parts = ReturnType<typeof partsOf>;
49
+
50
+ /** Whether every number given is finite; a number left out is. */
51
+ const finite = (...values: ReadonlyArray<number | undefined>) =>
52
+ values.every((value) => value === undefined || Number.isFinite(value));
53
+
54
+ const named = (target: Target) => (typeof target === "string" ? target : `${target.x},${target.y}`);
55
+
56
+ /** A click's options, once its counts are finite. */
57
+ const clickable = (options: ClickOptions) =>
58
+ finite(options.clickCount, options.holdMillis)
59
+ ? Effect.void
60
+ : failWith("click", new InvalidRequest({ detail: "clickCount and holdMillis must be finite" }));
61
+
62
+ /** The keys a press sends, once they and its options are valid. */
63
+ const pressed = (keys: string, options: PressOptions) => {
64
+ const parts = Keys.parts(keys);
65
+
66
+ if (!finite(options.times, options.holdMillis))
67
+ return failWith("press", new InvalidRequest({ detail: "times and holdMillis must be finite" }));
68
+ if (parts === undefined)
69
+ return failWith(
70
+ "press",
71
+ new InvalidRequest({
72
+ detail: `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`,
73
+ }),
74
+ );
75
+
76
+ return Effect.succeed(parts);
77
+ };
78
+
79
+ const click = (input: Parts) => {
80
+ const { settings } = input.page;
81
+ const { perform, preparePolicy, targetFor, moveTo, sendMouse, flush, mark, settle } = input;
82
+
83
+ return (target: Target, clickOptions: ClickOptions = {}, style: Style = plain) =>
84
+ perform(
85
+ "click",
86
+ {
87
+ target: named(target),
88
+ options: {
89
+ button: clickOptions.button,
90
+ clickCount: clickOptions.clickCount,
91
+ holdMillis: clickOptions.holdMillis,
92
+ },
93
+ },
94
+ settings.actionTimeout,
95
+ Effect.suspend(() => clickable(clickOptions)).pipe(
96
+ Effect.andThen(
97
+ preparePolicy("click", { target: typeof target === "string" ? target : undefined }, [
98
+ target,
99
+ ]),
100
+ ),
101
+ ),
102
+ (marks, approval) =>
103
+ Effect.gen(function* () {
104
+ yield* clickable(clickOptions);
105
+ const resolved = yield* targetFor("click", target, approval, marks);
106
+ const { point } = resolved;
107
+ const button = clickOptions.button ?? "left";
108
+ const count = Math.max(1, Math.min(3, clickOptions.clickCount ?? 1));
109
+
110
+ yield* marks.at(point);
111
+ yield* marks.on(resolved);
112
+ yield* moveTo("click", marks, point, resolved.cursor);
113
+ if (approval !== undefined) yield* approval.check({ presses: [{ index: 0, ...point }] });
114
+ const before = mark();
115
+
116
+ yield* marks.sent;
117
+ for (let index = 1; index <= count; index++) {
118
+ // An earlier click of a multi-click can replace the approved control under the
119
+ // pointer. Under a guard, its handlers run before the next press is checked and sent.
120
+ if (approval !== undefined && index > 1) {
121
+ yield* flush("click", marks.input);
122
+ yield* approval.check({ presses: [{ index: 0, ...point }] }).pipe(
123
+ Effect.catchIf(
124
+ (error) => error.reason._tag === "StaleRef",
125
+ () =>
126
+ failWith(
127
+ "click",
128
+ new NotActionable({
129
+ detail:
130
+ "an earlier click removed the approved target, so no further press was sent",
131
+ }),
132
+ ),
133
+ ),
134
+ );
135
+ }
136
+ yield* sendMouse("click", marks.input, {
137
+ type: "mousePressed",
138
+ ...point,
139
+ button,
140
+ buttons: buttonMask[button],
141
+ clickCount: index,
142
+ });
143
+
144
+ // A hold the caller asks for is the click's own; the style's is presentation.
145
+ const hold = clickOptions.holdMillis ?? (yield* marks.style.hold);
146
+
147
+ if (clickOptions.holdMillis === undefined) yield* marks.present(hold);
148
+ if (hold > 0) yield* Effect.sleep(Duration.millis(hold));
149
+ yield* sendMouse("click", marks.input, {
150
+ type: "mouseReleased",
151
+ ...point,
152
+ button,
153
+ buttons: 0,
154
+ clickCount: index,
155
+ });
156
+ }
157
+ yield* flush("click", marks.input);
158
+ yield* settle("click", before);
159
+
160
+ return resolved;
161
+ }),
162
+ style,
163
+ );
164
+ };
165
+
166
+ const hover = (input: Parts) => {
167
+ const { settings } = input.page;
168
+ const { perform, preparePolicy, targetFor, moveTo, flush } = input;
169
+
170
+ return (target: Target, style: Style = plain) =>
171
+ perform(
172
+ "hover",
173
+ { target: named(target) },
174
+ settings.actionTimeout,
175
+ preparePolicy("hover", { target: typeof target === "string" ? target : undefined }, [target]),
176
+ (marks, approval) =>
177
+ Effect.gen(function* () {
178
+ const resolved = yield* targetFor("hover", target, approval, marks);
179
+ const { point } = resolved;
180
+
181
+ yield* marks.at(point);
182
+ yield* marks.on(resolved);
183
+ yield* moveTo("hover", marks, point, resolved.cursor, "hover");
184
+ yield* flush("hover", marks.input);
185
+ }),
186
+ style,
187
+ );
188
+ };
189
+
190
+ /**
191
+ * The pointer's travel to a target, before its action is asked: no guard holds it, no action is
192
+ * recorded, and nothing is pressed. A guarded page aims nothing, as its input waits for approval.
193
+ */
194
+ const aim = (input: Parts) => {
195
+ const { settings } = input.page;
196
+ const { perform, targetFor, moveTo, flush } = input;
197
+
198
+ return (target: Target, style: Style) =>
199
+ settings.guard === undefined
200
+ ? perform(
201
+ "aim",
202
+ { target: named(target), recorded: false },
203
+ settings.actionTimeout,
204
+ Effect.die("an aim is never put to a guard"),
205
+ (marks) =>
206
+ targetFor("aim", target, undefined, marks).pipe(
207
+ Effect.flatMap(({ point, cursor }) => moveTo("aim", marks, point, cursor)),
208
+ Effect.andThen(flush("aim", marks.input)),
209
+ ),
210
+ style,
211
+ )
212
+ : Effect.void;
213
+ };
214
+
215
+ const drag = (input: Parts) => {
216
+ const { settings } = input.page;
217
+ const { perform, preparePolicy, targetFor, resolve, moveTo, sendMouse, flush } = input;
218
+
219
+ return (from: Target, to: Target, style: Style = plain) =>
220
+ perform(
221
+ "drag",
222
+ { target: `${JSON.stringify(from)} -> ${JSON.stringify(to)}` },
223
+ settings.actionTimeout,
224
+ preparePolicy("drag", {}, [from, to]),
225
+ (marks, approval) =>
226
+ Effect.gen(function* () {
227
+ yield* targetFor("drag", from, approval, marks);
228
+ yield* targetFor("drag", to, approval, marks);
229
+ // Bringing the second endpoint into view may move the first. Both must still be
230
+ // actionable in the final viewport before the first button press.
231
+ const start = yield* resolve("drag", from, approval, false);
232
+ const end = yield* resolve("drag", to, approval, false);
233
+
234
+ yield* marks.at(end.point);
235
+ yield* marks.on(start, end);
236
+ yield* moveTo("drag", marks, start.point, start.cursor);
237
+ // Both ends are checked before the button goes down: once it is down, a release cannot
238
+ // be withheld, and a dragged element under the pointer would hide the drop target.
239
+ if (approval !== undefined)
240
+ yield* approval.check({
241
+ presses: [
242
+ { index: 0, ...start.point },
243
+ { index: 1, ...end.point },
244
+ ],
245
+ });
246
+ yield* marks.sent;
247
+ yield* sendMouse("drag", marks.input, {
248
+ type: "mousePressed",
249
+ ...start.point,
250
+ button: "left",
251
+ buttons: 1,
252
+ clickCount: 1,
253
+ });
254
+ yield* moveTo("drag", marks, end.point, end.cursor, "drag");
255
+ yield* sendMouse("drag", marks.input, {
256
+ type: "mouseReleased",
257
+ ...end.point,
258
+ button: "left",
259
+ buttons: 0,
260
+ clickCount: 1,
261
+ });
262
+ yield* flush("drag", marks.input);
263
+ }),
264
+ style,
265
+ );
266
+ };
267
+
268
+ const typeText = (input: Parts) => {
269
+ const { settings, now } = input.page;
270
+ const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, bridge } = input;
271
+ const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
272
+
273
+ return (text: string, typeOptions: TypeOptions = {}, style: Style = plain) =>
274
+ perform(
275
+ "type",
276
+ {
277
+ target: typeOptions.into,
278
+ options: { replace: typeOptions.replace, submit: typeOptions.submit },
279
+ text,
280
+ secret: true,
281
+ },
282
+ settings.actionTimeout,
283
+ preparePolicy("type", { target: typeOptions.into, text }, [typeOptions.into ?? null], {
284
+ submit: typeOptions.submit ?? false,
285
+ }),
286
+ (marks, approval) =>
287
+ Effect.gen(function* () {
288
+ const replace = typeOptions.replace ?? true;
289
+ const into = typeOptions.into;
290
+
291
+ if (into !== undefined && !/^e\d+$/.test(into))
292
+ return yield* failWith(
293
+ "type",
294
+ new InvalidRequest({ detail: `"${into}" is not a ref; refs look like e12` }),
295
+ );
296
+
297
+ // A typed space or letter can press a focused button or change a control. Refuse
298
+ // those before any input, on every path.
299
+ const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
300
+
301
+ const typeable = yield* (
302
+ approval === undefined
303
+ ? bridge.evaluate("type", asked)
304
+ : mutate("type", asked, approval)
305
+ ).pipe(Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)));
306
+
307
+ if ("error" in typeable)
308
+ return yield* failWith(
309
+ "type",
310
+ typeable.error === "stale" && into !== undefined
311
+ ? new StaleRef({ ref: into })
312
+ : new NotActionable({ detail: typeable.detail }),
313
+ );
314
+ if (typeable.subject !== undefined) yield* marks.on(typeable.subject);
315
+
316
+ const since = yield* currentDocument("type");
317
+ let secret = typeable.secret;
318
+
319
+ if (into !== undefined) {
320
+ const target = yield* targetFor("type", into, approval, marks);
321
+
322
+ yield* marks.at(target.point);
323
+ yield* marks.on(target);
324
+ // Viewers see the pointer click into the field.
325
+ if (marks.style.shown) {
326
+ yield* moveTo("type", marks, target.point, target.cursor);
327
+ if (approval !== undefined)
328
+ yield* approval.check({ presses: [{ index: 0, ...target.point }] });
329
+ yield* marks.sent;
330
+ for (const type of ["mousePressed", "mouseReleased"] as const)
331
+ yield* sendMouse("type", marks.input, {
332
+ type,
333
+ ...target.point,
334
+ button: "left",
335
+ buttons: type === "mousePressed" ? 1 : 0,
336
+ clickCount: 1,
337
+ });
338
+ yield* flush("type", marks.input);
339
+ }
340
+
341
+ // Focusing can run page handlers, including navigation, before the script returns.
342
+ yield* marks.sent;
343
+
344
+ const focused = yield* mutate(
345
+ "type",
346
+ scriptCall("focus", into, replace),
347
+ approval,
348
+ ).pipe(Effect.flatMap(decodeWith("type", Script.FocusResultSchema)));
349
+
350
+ if ("error" in focused) return yield* Guard.editFailure("type", into, focused);
351
+ secret ||= focused.secret;
352
+ }
353
+ if (!secret) yield* marks.reveal;
354
+ if (approval !== undefined) yield* approval.check({ focused: true });
355
+ yield* marks.sent;
356
+
357
+ // A key's handlers can move focus, and later keys then reach a control nobody approved.
358
+ // Under a guard, the field is approved once: plain text goes in one insertion, which no
359
+ // handler can split, and keys typed one at a time check that the field still has focus,
360
+ // once the keys before are answered, before each space, which could press a button, and
361
+ // after the last key.
362
+ const focusHeld =
363
+ approval === undefined
364
+ ? Effect.void
365
+ : flush("type", marks.input).pipe(Effect.andThen(approval.check({ focused: true })));
366
+
367
+ if (text === "" && replace && into !== undefined) {
368
+ yield* sameDocument("type", since);
369
+ yield* keyStroke("type", marks.input, ["Delete"]);
370
+ } else {
371
+ const events = yield* marks.style.typing(text, approval !== undefined);
372
+ const keyed = events.some((event) => event.phase === "down");
373
+
374
+ const checks =
375
+ approval !== undefined && keyed ? (text.match(/\s/gu)?.length ?? 0) + 1 : 0;
376
+
377
+ // Each check is about two round trips, which only keys typed one at a time need.
378
+ const started = now();
379
+
380
+ yield* marks.present((events.at(-1)?.afterMillis ?? 0) + checks * 250);
381
+ for (const event of events) {
382
+ const remaining = started + event.afterMillis - now();
383
+
384
+ if (remaining > 0) yield* Effect.sleep(Duration.millis(remaining));
385
+ if (event.phase === "down" && /\s/u.test(event.key)) yield* focusHeld;
386
+ if (event.phase !== "up") yield* sameDocument("type", since);
387
+ yield* typeEvent(marks.input, event, secret);
388
+ }
389
+ if (keyed) yield* focusHeld;
390
+ }
391
+ // Public Playwright keys preserve platform editing commands. Drain the raw text
392
+ // session before Enter uses Playwright's session, so submit cannot overtake typing.
393
+ yield* flush("type", marks.input);
394
+ if (typeOptions.submit === true) {
395
+ // Typing can move focus or change the form; Enter goes only to the approved field.
396
+ if (approval !== undefined) yield* approval.check({ focused: true });
397
+ yield* sameDocument("type", since);
398
+ const before = mark();
399
+
400
+ yield* keyStroke("type", marks.input, ["Enter"]);
401
+ yield* flush("type", marks.input);
402
+ yield* settle("type", before);
403
+ }
404
+ }),
405
+ style,
406
+ );
407
+ };
408
+
409
+ const press = (input: Parts) => {
410
+ const { settings, now } = input.page;
411
+ const { perform, preparePolicy, currentDocument, sameDocument, keyStroke, flush } = input;
412
+ const { mark, settle, targetFor, mutate } = input;
413
+
414
+ return (keys: string, pressOptions: PressOptions = {}, style: Style = plain) =>
415
+ perform(
416
+ "press",
417
+ { target: keys, options: { times: pressOptions.times, holdMillis: pressOptions.holdMillis } },
418
+ settings.actionTimeout,
419
+ Effect.suspend(() => pressed(keys, pressOptions)).pipe(
420
+ Effect.flatMap((parts) =>
421
+ preparePolicy(
422
+ "press",
423
+ { target: pressOptions.on, text: parts.join("+") },
424
+ [pressOptions.on ?? null],
425
+ { keys: parts.join("+") },
426
+ ),
427
+ ),
428
+ ),
429
+ (marks, approval) =>
430
+ Effect.gen(function* () {
431
+ const parts = yield* pressed(keys, pressOptions);
432
+ const times = Math.max(1, Math.min(50, pressOptions.times ?? 1));
433
+ const hold = pressOptions.holdMillis ?? 0;
434
+ const activates = parts.at(-1) === "Enter" || parts.at(-1) === "Space";
435
+ const since = yield* currentDocument("press");
436
+ const on = pressOptions.on;
437
+
438
+ // The keys go to the element asked for, which must hold focus before the first.
439
+ if (on !== undefined) {
440
+ yield* marks.on(yield* targetFor("press", on, approval, marks));
441
+
442
+ const focused = yield* mutate("press", scriptCall("focusOn", on), approval).pipe(
443
+ Effect.flatMap(decodeWith("press", Script.EditResultSchema)),
444
+ );
445
+
446
+ if ("error" in focused) return yield* Guard.editFailure("press", on, focused);
447
+ if (approval !== undefined) yield* approval.check({ focused: true });
448
+ }
449
+ const before = mark();
450
+ let due = now();
451
+
452
+ yield* marks.sent;
453
+ for (let index = 0; index < times; index++) {
454
+ // A repeated key stays in the document it began in. Under a guard, a repeated Enter
455
+ // or Space must also reach the approved element after the previous press settled.
456
+ if (index > 0 && approval !== undefined && activates) {
457
+ yield* flush("press", marks.input);
458
+ yield* approval.check({ focused: true });
459
+ }
460
+ yield* sameDocument("press", since);
461
+ yield* keyStroke("press", marks.input, parts, hold);
462
+ if (index + 1 < times) {
463
+ const gap = yield* marks.style.keyGap;
464
+
465
+ yield* marks.present(gap);
466
+ due += hold + gap;
467
+ if (due > now()) yield* Effect.sleep(Duration.millis(due - now()));
468
+ }
469
+ }
470
+ yield* flush("press", marks.input);
471
+ yield* settle("press", before);
472
+ }),
473
+ style,
474
+ );
475
+ };
476
+
477
+ const scroll = (input: Parts) => {
478
+ const { settings } = input.page;
479
+ const { perform, preparePolicy, viewportFor, targetFor, resolve, moveTo, wheel, flush } = input;
480
+ const { mark, settle } = input;
481
+
482
+ return (scrollOptions: ScrollOptions = {}, style: Style = plain) => {
483
+ const target = scrollOptions.at;
484
+
485
+ const valid =
486
+ Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0)
487
+ ? Effect.void
488
+ : failWith("scroll", new InvalidRequest({ detail: "scroll deltas must be finite" }));
489
+
490
+ return perform(
491
+ "scroll",
492
+ {
493
+ target: target === undefined ? undefined : named(target),
494
+ options: { dx: scrollOptions.dx, dy: scrollOptions.dy },
495
+ },
496
+ settings.actionTimeout,
497
+ valid.pipe(
498
+ Effect.andThen(
499
+ // Scrolling the page has no target: whatever sits mid-viewport is not what is scrolled.
500
+ preparePolicy(
501
+ "scroll",
502
+ { target: typeof target === "string" ? target : undefined },
503
+ target === undefined ? [] : [target],
504
+ ),
505
+ ),
506
+ ),
507
+ (marks, approval) =>
508
+ Effect.gen(function* () {
509
+ yield* valid;
510
+ // Over CDP the page reports its own viewport, so the read shares the action's deadline.
511
+ const viewport = yield* viewportFor("scroll");
512
+
513
+ const middle = {
514
+ x: Math.round(viewport.width / 2),
515
+ y: Math.round(viewport.height / 2),
516
+ };
517
+
518
+ const dx = scrollOptions.dx ?? 0;
519
+
520
+ const dy =
521
+ scrollOptions.dy ??
522
+ (scrollOptions.dx === undefined ? Math.round(viewport.height * 0.8) : 0);
523
+
524
+ // A page scroll has no target, but viewers still see the cursor it lands on.
525
+ const resolved =
526
+ target !== undefined
527
+ ? yield* targetFor("scroll", target, approval, marks)
528
+ : marks.style.shown
529
+ ? yield* resolve("scroll", middle, approval).pipe(
530
+ Effect.orElseSucceed(() => undefined),
531
+ )
532
+ : undefined;
533
+
534
+ const point = resolved?.point ?? middle;
535
+
536
+ yield* marks.at(point);
537
+ if (target !== undefined && resolved !== undefined) yield* marks.on(resolved);
538
+ yield* moveTo("scroll", marks, point, resolved?.cursor);
539
+ const before = mark();
540
+
541
+ yield* marks.sent;
542
+ yield* wheel("scroll", marks, point, dx, dy);
543
+ yield* flush("scroll", marks.input);
544
+ yield* settle("scroll", before);
545
+ }),
546
+ style,
547
+ );
548
+ };
549
+ };
550
+
551
+ const select = (input: Parts) => {
552
+ const { settings } = input.page;
553
+ const { perform, preparePolicy, targetFor, mutate } = input;
554
+
555
+ return (ref: string, values: ReadonlyArray<string>) =>
556
+ perform(
557
+ "select",
558
+ { target: ref, options: { values }, text: values.join(", ") },
559
+ settings.actionTimeout,
560
+ preparePolicy("select", { target: ref, text: values.join(", ") }, [ref]),
561
+ (marks, approval) =>
562
+ Effect.gen(function* () {
563
+ yield* marks.on(yield* targetFor("select", ref, approval, marks));
564
+
565
+ // The script refuses before it changes anything. Only a choice it made, or a script
566
+ // that may have run without an intact answer, may have reached the page.
567
+ const result = yield* mutate(
568
+ "select",
569
+ scriptCall("select", ref, values),
570
+ approval,
571
+ marks.sent,
572
+ ).pipe(
573
+ Effect.flatMap((value) =>
574
+ decodeWith(
575
+ "select",
576
+ Script.EditResultSchema,
577
+ )(value).pipe(Effect.tapError(() => marks.sent)),
578
+ ),
579
+ );
580
+
581
+ if ("error" in result) return yield* Guard.editFailure("select", ref, result);
582
+ yield* marks.sent;
583
+
584
+ return result.detail;
585
+ }),
586
+ );
587
+ };
588
+
589
+ export const make = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
590
+ const parts = partsOf(page, bridge, viewport);
591
+
592
+ return {
593
+ perform: parts.perform,
594
+ preparePolicy: parts.preparePolicy,
595
+ click: click(parts),
596
+ hover: hover(parts),
597
+ aim: aim(parts),
598
+ drag: drag(parts),
599
+ type: typeText(parts),
600
+ press: press(parts),
601
+ scroll: scroll(parts),
602
+ select: select(parts),
603
+ };
604
+ };
605
+
606
+ /** A page's input, each action in any style. */
607
+ export type Input = ReturnType<typeof make>;