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
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Sending input to the browser. Each mouse event, key and inserted text goes out on the page's
3
+ * protocol session, stamped with the browser's clock when its run has a mapping, and is published
4
+ * as it is submitted.
5
+ */
6
+ import { Effect, MutableRef, Option } from "effect";
7
+
8
+ import {
9
+ KeyChanged,
10
+ PointerPressed,
11
+ PointerReleased,
12
+ TextInserted,
13
+ WheelScrolled,
14
+ } from "../../BrowserEvent.ts";
15
+ import { type PageContext, reasonOf, undispatched } from "../page/context.ts";
16
+ import * as BrowserClock from "../pictures/clock.ts";
17
+ import type * as Replies from "./replies.ts";
18
+
19
+ export type MouseEvent = {
20
+ readonly type: "mouseMoved" | "mousePressed" | "mouseReleased" | "mouseWheel";
21
+ readonly x: number;
22
+ readonly y: number;
23
+ readonly button?: "none" | "left" | "right" | "middle";
24
+ readonly buttons?: number;
25
+ readonly clickCount?: number;
26
+ readonly deltaX?: number;
27
+ readonly deltaY?: number;
28
+ };
29
+
30
+ export const make = (page: PageContext) => {
31
+ const { id, pointer, publish, now, noteInput, closedBy } = page;
32
+ const { send } = page.protocol;
33
+ // Each run's clock mapping, recorded when it begins.
34
+ const inputClocks = new WeakMap<Replies.Run, BrowserClock.Estimate>();
35
+
36
+ // Every run records its mapping when it begins; a missing one leaves Chromium's own receipt time.
37
+ const stamp = (estimate: BrowserClock.Estimate | undefined, at: number) =>
38
+ estimate === undefined ? {} : { timestamp: BrowserClock.toBrowserSeconds(estimate, at) };
39
+
40
+ const inputCall = <A>(operation: string, effect: Effect.Effect<A, Replies.InputFailure>) =>
41
+ effect.pipe(
42
+ Effect.mapError((error) =>
43
+ undispatched(operation, reasonOf(error.cause, undefined, closedBy())),
44
+ ),
45
+ );
46
+
47
+ const dispatchMouse = (
48
+ event: MouseEvent,
49
+ estimate: BrowserClock.Estimate | undefined,
50
+ submitted?: () => void,
51
+ ): Promise<unknown> => {
52
+ const at = now();
53
+ const point = { x: event.x, y: event.y };
54
+ const button = event.button ?? "left";
55
+
56
+ // Construct boundary data before dispatch: validation must never abandon the native reply.
57
+ const ButtonEvent = event.type === "mousePressed" ? PointerPressed : PointerReleased;
58
+
59
+ const track =
60
+ (event.type === "mousePressed" || event.type === "mouseReleased") && button !== "none"
61
+ ? new ButtonEvent({ at, page: id, ...point, button, clickCount: event.clickCount ?? 1 })
62
+ : event.type === "mouseWheel"
63
+ ? new WheelScrolled({
64
+ at,
65
+ page: id,
66
+ ...point,
67
+ dx: event.deltaX ?? 0,
68
+ dy: event.deltaY ?? 0,
69
+ })
70
+ : undefined;
71
+
72
+ const response = send("Input.dispatchMouseEvent", { ...event, ...stamp(estimate, at) });
73
+
74
+ noteInput();
75
+ if (event.type === "mouseMoved") MutableRef.set(pointer, Option.some(point));
76
+ if (track !== undefined) publish(track);
77
+ submitted?.();
78
+
79
+ return response;
80
+ };
81
+
82
+ const sendMouse = (
83
+ operation: string,
84
+ run: Replies.Run,
85
+ event: MouseEvent,
86
+ submitted?: () => void,
87
+ ) =>
88
+ inputCall(
89
+ operation,
90
+ Effect.gen(function* () {
91
+ const held = `mouse:${event.button ?? "left"}`;
92
+ const estimate = inputClocks.get(run);
93
+
94
+ if (event.type === "mouseReleased") return yield* run.up(held);
95
+ if (event.type === "mousePressed") {
96
+ yield* run.reserve(2);
97
+ yield* run.down(
98
+ held,
99
+ () => dispatchMouse(event, estimate, submitted),
100
+ () =>
101
+ dispatchMouse(
102
+ {
103
+ type: "mouseReleased",
104
+ ...Option.getOrElse(MutableRef.get(pointer), () => ({ x: event.x, y: event.y })),
105
+ button: event.button ?? "left",
106
+ buttons: 0,
107
+ clickCount: event.clickCount ?? 1,
108
+ },
109
+ estimate,
110
+ ),
111
+ );
112
+ } else {
113
+ yield* run.reserve(1);
114
+ yield* run.send(() => dispatchMouse(event, estimate, submitted));
115
+ }
116
+ }),
117
+ );
118
+
119
+ // Keys and text bound for a secret field are recorded without their content.
120
+ const dispatchKey = (
121
+ key: string,
122
+ phase: "down" | "up",
123
+ command: (at: number, estimate: BrowserClock.Estimate | undefined) => Promise<unknown>,
124
+ run: Replies.Run,
125
+ secret = false,
126
+ ) => {
127
+ const at = now();
128
+ const event = new KeyChanged({ at, page: id, key: secret ? "Unidentified" : key, phase });
129
+ const response = command(at, inputClocks.get(run));
130
+
131
+ noteInput();
132
+ publish(event);
133
+
134
+ return response;
135
+ };
136
+
137
+ const dispatchText = (text: string, secret: boolean): Promise<unknown> => {
138
+ const track = new TextInserted({ at: now(), page: id, text: secret ? "•" : text });
139
+ const response = send("Input.insertText", { text });
140
+
141
+ noteInput();
142
+ publish(track);
143
+
144
+ return response;
145
+ };
146
+
147
+ const flush = (operation: string, run: Replies.Run) => inputCall(operation, run.drain);
148
+
149
+ return {
150
+ inputClocks,
151
+ stamp,
152
+ inputCall,
153
+ dispatchMouse,
154
+ sendMouse,
155
+ dispatchKey,
156
+ dispatchText,
157
+ flush,
158
+ };
159
+ };
160
+
161
+ export type Dispatch = ReturnType<typeof make>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * In the page: editing a field. It refuses text that a focused control could act on, focuses a
3
+ * field and chooses a select's options. See `reading/names.inpage.ts` for what a page-side part
4
+ * may use.
5
+ */
6
+ import { Schema } from "effect";
7
+
8
+ import { Subject } from "../../BrowserEvent.ts";
9
+ import type { ContextReader } from "../reading/context.inpage.ts";
10
+ import type { Names } from "../reading/names.inpage.ts";
11
+ import type { Guard } from "./guard.inpage.ts";
12
+
13
+ export type EditResult =
14
+ | { readonly ok: true; readonly detail: string }
15
+ | { readonly error: string; readonly stale?: boolean };
16
+
17
+ /** A focused field, and whether it is secret. */
18
+ export type FocusResult =
19
+ | { readonly ok: true; readonly detail: string; readonly secret: boolean }
20
+ | { readonly error: string; readonly stale?: boolean };
21
+
22
+ /**
23
+ * Whether text may be typed, and whether its field is secret. Typing into focus also names the
24
+ * focused field, when it is one, so the action records what it typed into.
25
+ */
26
+ export type TypeableResult =
27
+ | { readonly ok: true; readonly secret: boolean; readonly subject?: typeof Subject.Encoded }
28
+ | { readonly error: "stale" | "untypeable"; readonly detail: string };
29
+
30
+ export const edit = (names: Names, guard: Guard, placing: ContextReader) => {
31
+ const {
32
+ activeElement,
33
+ clean,
34
+ describe,
35
+ isDisabled,
36
+ isHtml,
37
+ isInput,
38
+ isSecret,
39
+ isSelect,
40
+ isTextArea,
41
+ lookup,
42
+ nameOf,
43
+ roleOf,
44
+ } = names;
45
+
46
+ const { textEntry, typingRefusal } = guard;
47
+
48
+ /** Whether text may be typed into a ref or focus, and, for a `secret`, only into a secret field. */
49
+ const typeable = (ref: string | null, secret: boolean): TypeableResult => {
50
+ const element = ref === null ? activeElement() : lookup(ref);
51
+
52
+ if (element === undefined)
53
+ return {
54
+ error: "stale",
55
+ detail: `${ref ?? "the focused element"} is not on the page any more`,
56
+ };
57
+
58
+ const refusal =
59
+ element === null
60
+ ? secret
61
+ ? "nothing has focus to type a secret into"
62
+ : undefined
63
+ : (typingRefusal(element, ref !== null) ??
64
+ (secret && !isSecret(element)
65
+ ? `${describe(element)} is not a secret field`
66
+ : undefined));
67
+
68
+ if (refusal !== undefined) return { error: "untypeable", detail: refusal };
69
+ if (element === null || ref !== null || !textEntry(element))
70
+ return { ok: true, secret: element !== null && isSecret(element) };
71
+ const role = roleOf(element);
72
+ const tag = element.tagName.toLowerCase();
73
+
74
+ return {
75
+ ok: true,
76
+ secret: isSecret(element),
77
+ subject: { role, name: nameOf(element, role), tag, context: placing.contextOf(element) },
78
+ };
79
+ };
80
+
81
+ const focus = (ref: string, replace: boolean): FocusResult => {
82
+ const element = lookup(ref);
83
+
84
+ if (element === undefined) return { error: `${ref} is not on the page any more`, stale: true };
85
+ if (isDisabled(element)) return { error: `${ref} is disabled` };
86
+ if (!textEntry(element)) return { error: `${ref} is ${describe(element)}, not a text field` };
87
+ if (isHtml(element)) element.focus();
88
+ if (replace) {
89
+ if (isInput(element) || isTextArea(element)) element.select();
90
+ else element.ownerDocument.getSelection()?.selectAllChildren(element);
91
+ }
92
+
93
+ // Decided after focusing: a focus handler can mark the field secret.
94
+ return { ok: true, detail: describe(element), secret: isSecret(element) };
95
+ };
96
+
97
+ /** Focus any element that takes focus, such as a link or a button, for a key pressed on it. */
98
+ const focusOn = (ref: string): EditResult => {
99
+ const element = lookup(ref);
100
+
101
+ if (element === undefined) return { error: `${ref} is not on the page any more`, stale: true };
102
+ if (isDisabled(element)) return { error: `${ref} is disabled` };
103
+ if (isHtml(element)) element.focus();
104
+
105
+ return activeElement() === element
106
+ ? { ok: true, detail: describe(element) }
107
+ : { error: `${ref} is ${describe(element)}, which does not take focus` };
108
+ };
109
+
110
+ const select = (ref: string, values: ReadonlyArray<string>): EditResult => {
111
+ const element = lookup(ref);
112
+
113
+ if (element === undefined) return { error: `${ref} is not on the page any more`, stale: true };
114
+ if (!isSelect(element))
115
+ return {
116
+ error: `${ref} is ${describe(element)}, not a <select>; click it and pick an option`,
117
+ };
118
+ const wanted = values.map((value) => value.trim().toLowerCase());
119
+ const options = Array.from(element.options);
120
+
121
+ const chosen = options.filter(
122
+ (option) =>
123
+ wanted.includes(option.value.toLowerCase()) ||
124
+ wanted.includes(clean(option.text).toLowerCase()),
125
+ );
126
+
127
+ if (chosen.length === 0)
128
+ return {
129
+ error: `no option matches; the options are ${options.map((option) => JSON.stringify(clean(option.text, 40))).join(", ")}`,
130
+ };
131
+ for (const option of options) option.selected = chosen.includes(option);
132
+ element.dispatchEvent(new Event("input", { bubbles: true }));
133
+ element.dispatchEvent(new Event("change", { bubbles: true }));
134
+
135
+ return { ok: true, detail: chosen.map((option) => clean(option.text, 40)).join(", ") };
136
+ };
137
+
138
+ return { focus, focusOn, select, typeable };
139
+ };
140
+
141
+ export type Edit = ReturnType<typeof edit>;
142
+
143
+ export const TypeableResultSchema = Schema.Union([
144
+ Schema.Struct({
145
+ ok: Schema.Literal(true),
146
+ secret: Schema.Boolean,
147
+ subject: Schema.optional(Subject),
148
+ }),
149
+ Schema.Struct({ error: Schema.Literals(["stale", "untypeable"]), detail: Schema.String }),
150
+ ]);
151
+
152
+ const EditFailure = Schema.Struct({ error: Schema.String, stale: Schema.optional(Schema.Boolean) });
153
+
154
+ export const FocusResultSchema = Schema.Union([
155
+ Schema.Struct({ ok: Schema.Literal(true), detail: Schema.String, secret: Schema.Boolean }),
156
+ EditFailure,
157
+ ]);
158
+
159
+ export const EditResultSchema = Schema.Union([
160
+ Schema.Struct({ ok: Schema.Literal(true), detail: Schema.String }),
161
+ EditFailure,
162
+ ]);
@@ -0,0 +1,164 @@
1
+ /**
2
+ * In the page: what the page says about an input's first target, for a judge, and which fields a
3
+ * form would send. It never reads what a field holds. See `reading/names.inpage.ts` for what a page-side part may use.
4
+ */
5
+ import { Schema } from "effect";
6
+
7
+ import type { ContextReader } from "../reading/context.inpage.ts";
8
+ import type { Names } from "../reading/names.inpage.ts";
9
+
10
+ export interface FormField {
11
+ readonly type: string;
12
+ readonly name: string;
13
+ readonly autocomplete?: string | undefined;
14
+ readonly filled: boolean;
15
+ }
16
+
17
+ /** Page text about the first target. It never includes what a field holds. */
18
+ export interface Evidence {
19
+ readonly title: string;
20
+ readonly description?: string | undefined;
21
+ readonly dialog?: string | undefined;
22
+ readonly heading?: string | undefined;
23
+ readonly nearby?: string | undefined;
24
+ readonly form?:
25
+ | {
26
+ readonly method: string;
27
+ readonly action: string;
28
+ readonly fields: ReadonlyArray<FormField>;
29
+ }
30
+ | undefined;
31
+ }
32
+
33
+ type Field = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
34
+
35
+ export const evidence = (names: Names, placing: ContextReader) => {
36
+ const { clean, isDocument, isInput, isSelect, isTextArea, nameOf, parentOf, roleOf, textOf } =
37
+ names;
38
+
39
+ const { groups, headingBefore, textBeside, treeOf } = placing;
40
+
41
+ /** A field a submission would send. */
42
+ const isField = (element: Element): element is Field =>
43
+ ((isInput(element) && !["button", "submit", "reset", "image"].includes(element.type)) ||
44
+ isSelect(element) ||
45
+ isTextArea(element)) &&
46
+ !element.matches(":disabled");
47
+
48
+ const isFilled = (field: Field): boolean =>
49
+ isInput(field) && (field.type === "checkbox" || field.type === "radio")
50
+ ? field.checked
51
+ : isInput(field) && field.type === "file"
52
+ ? (field.files?.length ?? 0) > 0
53
+ : field.value !== "";
54
+
55
+ const dialogOf = (element: Element): Element | undefined => {
56
+ for (let node: Element | null = element; node !== null; node = parentOf(node))
57
+ if (node.tagName === "DIALOG" || /^(?:alert)?dialog$/.test(node.getAttribute("role") ?? ""))
58
+ return node;
59
+
60
+ return undefined;
61
+ };
62
+
63
+ /** What the page says about an input's first target, for a judge. Never a field's value. */
64
+ const evidenceOf = (
65
+ element: Element,
66
+ form: HTMLFormElement | null,
67
+ method: string | undefined,
68
+ action: string | undefined,
69
+ ): Evidence => {
70
+ const name = nameOf(element, roleOf(element));
71
+ const ids = element.getAttribute("aria-describedby");
72
+
73
+ const description = clean(
74
+ ids === null
75
+ ? (element.getAttribute("aria-description") ?? element.getAttribute("title"))
76
+ : ids
77
+ .split(/\s+/)
78
+ .map((id) => element.ownerDocument.getElementById(id)?.textContent ?? "")
79
+ .join(" "),
80
+ 120,
81
+ );
82
+
83
+ const dialog = dialogOf(element);
84
+
85
+ const title = dialog?.querySelector("h1,h2,h3,h4,h5,h6,[role=heading]");
86
+
87
+ const dialogName =
88
+ dialog === undefined
89
+ ? ""
90
+ : clean(nameOf(dialog, "dialog"), 120) ||
91
+ (title === null || title === undefined ? "" : clean(textOf(title), 120));
92
+
93
+ // Each scope stays in the target's own tree: a walker cannot leave a shadow root.
94
+ const root = element.getRootNode();
95
+ const top = isDocument(root) ? (root.body ?? root) : root;
96
+ const inTree = element.closest("dialog,[role=dialog],[role=alertdialog]");
97
+ let nearby = "";
98
+
99
+ for (const scope of new Set([element.closest(groups), inTree, top]))
100
+ if (scope !== null && nearby === "")
101
+ nearby = textBeside(element, scope, true) || textBeside(element, scope, false);
102
+
103
+ const heading = headingBefore(element, treeOf(element));
104
+
105
+ const fields =
106
+ form === null
107
+ ? []
108
+ : Array.from(form.elements)
109
+ .filter(isField)
110
+ .slice(0, 16)
111
+ .map((field) => {
112
+ const autocomplete = clean(field.getAttribute("autocomplete"), 40);
113
+
114
+ return {
115
+ type: isInput(field) ? field.type : field.tagName.toLowerCase(),
116
+ name: clean(
117
+ isInput(field) && field.type === "hidden"
118
+ ? field.getAttribute("name")
119
+ : nameOf(field, roleOf(field)),
120
+ 40,
121
+ ),
122
+ ...(autocomplete === "" ? {} : { autocomplete }),
123
+ filled: isFilled(field),
124
+ };
125
+ });
126
+
127
+ return {
128
+ title: clean(document.title, 120),
129
+ ...(description === "" || description === name ? {} : { description }),
130
+ ...(dialogName === "" ? {} : { dialog: dialogName }),
131
+ ...(heading === undefined ? {} : { heading }),
132
+ ...(nearby === "" ? {} : { nearby }),
133
+ ...(form === null
134
+ ? {}
135
+ : { form: { method: method ?? form.method, action: action ?? form.action, fields } }),
136
+ };
137
+ };
138
+
139
+ return { evidenceOf, isField, isFilled };
140
+ };
141
+
142
+ export type EvidenceReader = ReturnType<typeof evidence>;
143
+
144
+ export const FormFieldSchema = Schema.Struct({
145
+ type: Schema.String,
146
+ name: Schema.String,
147
+ autocomplete: Schema.optional(Schema.String),
148
+ filled: Schema.Boolean,
149
+ });
150
+
151
+ export const EvidenceSchema = Schema.Struct({
152
+ title: Schema.String,
153
+ description: Schema.optional(Schema.String),
154
+ dialog: Schema.optional(Schema.String),
155
+ heading: Schema.optional(Schema.String),
156
+ nearby: Schema.optional(Schema.String),
157
+ form: Schema.optional(
158
+ Schema.Struct({
159
+ method: Schema.String,
160
+ action: Schema.String,
161
+ fields: Schema.Array(FormFieldSchema),
162
+ }),
163
+ ),
164
+ });