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,184 @@
1
+ /**
2
+ * The input guard's host side. It asks the page what an action would touch, builds the request a
3
+ * policy decides on, binds an approval to the document and targets it saw, and runs the page
4
+ * script's changes in the approved document only.
5
+ */
6
+ import { Effect } from "effect";
7
+
8
+ import { type BrowserError, InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
9
+ import { InputRequest, redacted } from "../../Page.ts";
10
+ import { type Bridge, scriptCall } from "../page/bridge.ts";
11
+ import { contextGone, decodeWith, failWith, type PageContext } from "../page/context.ts";
12
+ import * as Url from "../page/url.ts";
13
+ import * as Script from "./guard.inpage.ts";
14
+
15
+ export interface Approval {
16
+ readonly contextId: number;
17
+ readonly check: (options?: Script.ValidationOptions) => Effect.Effect<void, BrowserError>;
18
+ }
19
+
20
+ export interface PolicyPlan {
21
+ readonly request: InputRequest;
22
+ readonly validate: Effect.Effect<Approval, BrowserError>;
23
+ }
24
+
25
+ // A ref that names nothing is stale on every path, named by the target that failed.
26
+ export const inputFailure = (
27
+ operation: string,
28
+ targets: Script.InputPlan["targets"],
29
+ failure: Script.InputFailure,
30
+ ) => {
31
+ const target = failure.index === undefined ? undefined : targets[failure.index];
32
+
33
+ return failWith(
34
+ operation,
35
+ failure.error === "outside"
36
+ ? new InvalidRequest({ detail: failure.detail })
37
+ : failure.error === "stale" && typeof target === "string"
38
+ ? new StaleRef({ ref: target })
39
+ : new NotActionable({ detail: failure.detail }),
40
+ );
41
+ };
42
+
43
+ export const editFailure = (
44
+ operation: string,
45
+ ref: string,
46
+ failure: { readonly error: string; readonly stale?: boolean | undefined },
47
+ ) =>
48
+ failWith(
49
+ operation,
50
+ failure.stale === true ? new StaleRef({ ref }) : new NotActionable({ detail: failure.error }),
51
+ );
52
+
53
+ export const make = (page: PageContext, bridge: Bridge) => {
54
+ const { id } = page;
55
+ const { current, evaluateIn, evaluateWithContext } = bridge;
56
+
57
+ const preparePolicy = (
58
+ action: string,
59
+ info: { readonly target?: string | undefined; readonly text?: string | undefined },
60
+ targets: Script.InputPlan["targets"],
61
+ flags: {
62
+ readonly submit?: boolean;
63
+ readonly keys?: string;
64
+ readonly destination?: string;
65
+ } = {},
66
+ ): Effect.Effect<PolicyPlan, BrowserError> =>
67
+ Effect.gen(function* () {
68
+ for (const target of targets) {
69
+ if (typeof target === "string" && !/^e\d+$/.test(target))
70
+ return yield* failWith(
71
+ action,
72
+ new InvalidRequest({ detail: `"${target}" is not a ref; refs look like e12` }),
73
+ );
74
+ if (
75
+ target !== null &&
76
+ typeof target !== "string" &&
77
+ (!Number.isFinite(target.x) || !Number.isFinite(target.y))
78
+ )
79
+ return yield* failWith(
80
+ action,
81
+ new InvalidRequest({ detail: "point coordinates must be finite" }),
82
+ );
83
+ }
84
+
85
+ const input: Script.InputPlan = {
86
+ action,
87
+ targets,
88
+ submit: flags.submit ?? false,
89
+ keys: flags.keys ?? null,
90
+ destination: flags.destination ?? null,
91
+ };
92
+
93
+ const { contextId, value } = yield* evaluateWithContext(
94
+ action,
95
+ scriptCall("prepareInput", input),
96
+ );
97
+
98
+ const prepared = yield* decodeWith(action, Script.PreparedInputResultSchema)(value);
99
+
100
+ if ("error" in prepared) return yield* inputFailure(action, targets, prepared);
101
+ const first = prepared.targets[0];
102
+ const target = targets[0];
103
+ const { title, description, dialog, heading, nearby, form } = prepared.evidence;
104
+
105
+ // A guard may hand the request to a judge's model, so its addresses keep no secrets.
106
+ const request = new InputRequest({
107
+ page: id,
108
+ url: Url.redact(prepared.url),
109
+ title,
110
+ action,
111
+ target: info.target,
112
+ text:
113
+ info.text !== undefined && action === "type" && first?.secret === true
114
+ ? redacted
115
+ : info.text,
116
+ element: first?.element,
117
+ role: first?.role,
118
+ name: first?.name,
119
+ description,
120
+ href: first?.href === undefined ? undefined : Url.redact(first.href),
121
+ point: target !== null && typeof target === "object" ? target : undefined,
122
+ destination:
123
+ prepared.destination === undefined ? undefined : Url.redact(prepared.destination),
124
+ facts: prepared.facts,
125
+ context: { dialog, heading, nearby },
126
+ form,
127
+ });
128
+
129
+ const check = (options: Script.ValidationOptions = {}) =>
130
+ evaluateIn(action, scriptCall("validateInput", input, prepared, options), contextId).pipe(
131
+ Effect.catchIf(contextGone, () =>
132
+ failWith(
133
+ action,
134
+ new NotActionable({ detail: "the page changed while input policy was pending" }),
135
+ ),
136
+ ),
137
+ Effect.flatMap(decodeWith(action, Script.ValidatedInputResultSchema)),
138
+ Effect.flatMap((result) =>
139
+ "error" in result ? inputFailure(action, targets, result) : Effect.void,
140
+ ),
141
+ );
142
+
143
+ const validate = check().pipe(
144
+ Effect.as<Approval>({
145
+ contextId,
146
+ check,
147
+ }),
148
+ );
149
+
150
+ return { request, validate };
151
+ });
152
+
153
+ // `mayHaveRun` hears of a failure or interruption after the script may have started. A known
154
+ // missing context or API means it never ran; acquiring the context itself sends no mutation.
155
+ const mutate = (
156
+ operation: string,
157
+ call: string,
158
+ approval: Approval | undefined,
159
+ mayHaveRun: Effect.Effect<void> = Effect.void,
160
+ ) =>
161
+ (approval === undefined ? current(operation) : Effect.succeed(approval.contextId)).pipe(
162
+ Effect.flatMap((contextId) =>
163
+ evaluateIn(operation, call, contextId).pipe(
164
+ Effect.onInterrupt(() => mayHaveRun),
165
+ Effect.tapError((error) =>
166
+ error.reason._tag === "Failed" &&
167
+ /Cannot find context|__effectBrowser/i.test(error.reason.detail)
168
+ ? Effect.void
169
+ : mayHaveRun,
170
+ ),
171
+ ),
172
+ ),
173
+ Effect.catchIf(contextGone, () =>
174
+ failWith(
175
+ operation,
176
+ new NotActionable({ detail: "the page changed before the approved input could run" }),
177
+ ),
178
+ ),
179
+ );
180
+
181
+ return { preparePolicy, mutate };
182
+ };
183
+
184
+ export type Guard = ReturnType<typeof make>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Keys and typing: a key chord held and released, each typed event, and the document a run of
3
+ * keys must stay in.
4
+ */
5
+ import { Duration, Effect } from "effect";
6
+
7
+ import { NotActionable } from "../../BrowserError.ts";
8
+ import type { Bridge } from "../page/bridge.ts";
9
+ import { failWith, type PageContext } from "../page/context.ts";
10
+ import type { Dispatch } from "./dispatch.ts";
11
+ import * as Keys from "./keys.ts";
12
+ import type * as Replies from "./replies.ts";
13
+
14
+ export const make = (page: PageContext, bridge: Bridge, dispatch: Dispatch) => {
15
+ const { playwright } = page;
16
+ const { send } = page.protocol;
17
+ const { currentDocument } = bridge;
18
+ const { stamp, inputCall, dispatchKey, dispatchText } = dispatch;
19
+
20
+ // A multi-key action stops before its next key once the page has moved to another document. The
21
+ // page's own session counts main-frame commits as the browser reports them, so a key sent within
22
+ // about one protocol round trip of a commit can still reach the new document.
23
+ const sameDocument = (operation: string, since: number) =>
24
+ currentDocument(operation).pipe(
25
+ Effect.flatMap((now) =>
26
+ now === since
27
+ ? Effect.void
28
+ : failWith(
29
+ operation,
30
+ new NotActionable({
31
+ detail: "the page moved to another document, so the remaining keys were not sent",
32
+ }),
33
+ ),
34
+ ),
35
+ );
36
+
37
+ const keyStroke = (
38
+ operation: string,
39
+ run: Replies.Run,
40
+ parts: ReadonlyArray<string>,
41
+ holdMillis = 0,
42
+ ) =>
43
+ Effect.gen(function* () {
44
+ yield* inputCall(operation, run.reserve(parts.length * 2));
45
+ for (const part of parts)
46
+ yield* inputCall(
47
+ operation,
48
+ run.down(
49
+ `playwright:${part}`,
50
+ () => dispatchKey(part, "down", () => playwright.keyboard.down(part), run),
51
+ () => dispatchKey(part, "up", () => playwright.keyboard.up(part), run),
52
+ ),
53
+ );
54
+ if (holdMillis > 0) yield* Effect.sleep(Duration.millis(holdMillis));
55
+ for (const part of parts.toReversed())
56
+ yield* inputCall(operation, run.up(`playwright:${part}`));
57
+ });
58
+
59
+ const typeEvent = (
60
+ run: Replies.Run,
61
+ event: { readonly phase: "down" | "up" | "insert"; readonly key: string },
62
+ secret: boolean,
63
+ ) =>
64
+ inputCall(
65
+ "type",
66
+ Effect.gen(function* () {
67
+ if (event.phase === "insert") {
68
+ yield* run.reserve(1);
69
+ yield* run.send(() => dispatchText(event.key, secret));
70
+
71
+ return;
72
+ }
73
+
74
+ const description = Keys.description(event.key);
75
+
76
+ if (description === undefined) return yield* Effect.die("invalid planned key");
77
+ const { key, code, keyCode, text } = description;
78
+ const held = "raw:" + code;
79
+
80
+ if (event.phase === "up") return yield* run.up(held);
81
+
82
+ // The key going down carries its text; going up, only the key.
83
+ const keyEvent = (phase: "down" | "up") => () =>
84
+ dispatchKey(
85
+ key,
86
+ phase,
87
+ (at, estimate) =>
88
+ send("Input.dispatchKeyEvent", {
89
+ type: phase === "down" ? "keyDown" : "keyUp",
90
+ ...stamp(estimate, at),
91
+ modifiers: 0,
92
+ windowsVirtualKeyCode: keyCode,
93
+ code,
94
+ key,
95
+ location: 0,
96
+ ...(phase === "down"
97
+ ? { commands: [], text, unmodifiedText: text, autoRepeat: false, isKeypad: false }
98
+ : {}),
99
+ }),
100
+ run,
101
+ secret,
102
+ );
103
+
104
+ yield* run.reserve(2);
105
+ yield* run.down(held, keyEvent("down"), keyEvent("up"));
106
+ }),
107
+ );
108
+
109
+ return { currentDocument, sameDocument, keyStroke, typeEvent };
110
+ };
111
+
112
+ export type Keyboard = ReturnType<typeof make>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Key aliases for Playwright and printable US key descriptions for pipelined Chromium typing.
3
+ */
4
+
5
+ const aliases: Record<string, string> = {
6
+ ctrl: "Control",
7
+ control: "Control",
8
+ cmd: "Meta",
9
+ command: "Meta",
10
+ meta: "Meta",
11
+ win: "Meta",
12
+ super: "Meta",
13
+ mod: "ControlOrMeta",
14
+ alt: "Alt",
15
+ option: "Alt",
16
+ shift: "Shift",
17
+ enter: "Enter",
18
+ return: "Enter",
19
+ esc: "Escape",
20
+ escape: "Escape",
21
+ tab: "Tab",
22
+ space: "Space",
23
+ spacebar: "Space",
24
+ " ": "Space",
25
+ backspace: "Backspace",
26
+ delete: "Delete",
27
+ del: "Delete",
28
+ insert: "Insert",
29
+ home: "Home",
30
+ end: "End",
31
+ pageup: "PageUp",
32
+ pagedown: "PageDown",
33
+ up: "ArrowUp",
34
+ down: "ArrowDown",
35
+ left: "ArrowLeft",
36
+ right: "ArrowRight",
37
+ arrowup: "ArrowUp",
38
+ arrowdown: "ArrowDown",
39
+ arrowleft: "ArrowLeft",
40
+ arrowright: "ArrowRight",
41
+ };
42
+
43
+ const named = new Set(Object.values(aliases));
44
+
45
+ /** Parse and validate the whole chord before sending any down events. */
46
+ export const parts = (keys: string): ReadonlyArray<string> | undefined => {
47
+ const tokens: Array<string> = [];
48
+ let building = "";
49
+
50
+ // Like Playwright, a plus without a preceding token is the literal plus key: Control++.
51
+ for (const character of keys) {
52
+ if (character === "+" && building.trim() !== "") {
53
+ tokens.push(building);
54
+ building = "";
55
+ } else building += character;
56
+ }
57
+ tokens.push(building);
58
+ if (tokens.length > 4) return undefined;
59
+ const out: Array<string> = [];
60
+
61
+ for (const token of tokens) {
62
+ const part = token === " " ? "Space" : token.trim();
63
+ const lower = part.toLowerCase();
64
+ const alias = Object.hasOwn(aliases, lower) ? aliases[lower] : undefined;
65
+
66
+ if (alias !== undefined) out.push(alias);
67
+ else if (
68
+ named.has(part) ||
69
+ /^F([1-9]|1[0-2])$/.test(part) ||
70
+ /^(Key[A-Z]|Digit[0-9]|Numpad[0-9])$/.test(part) ||
71
+ description(part) !== undefined
72
+ )
73
+ out.push(part);
74
+ else return undefined;
75
+ }
76
+
77
+ return out;
78
+ };
79
+
80
+ interface Description {
81
+ readonly code: string;
82
+ readonly keyCode: number;
83
+ readonly key: string;
84
+ readonly text: string;
85
+ }
86
+
87
+ const punctuation: ReadonlyArray<readonly [string, string, number]> = [
88
+ ["`~", "Backquote", 192],
89
+ ["-_", "Minus", 189],
90
+ ["=+", "Equal", 187],
91
+ ["\\|", "Backslash", 220],
92
+ ["[{", "BracketLeft", 219],
93
+ ["]}", "BracketRight", 221],
94
+ [";:", "Semicolon", 186],
95
+ ["'\"", "Quote", 222],
96
+ [",<", "Comma", 188],
97
+ [".>", "Period", 190],
98
+ ["/?", "Slash", 191],
99
+ [" ", "Space", 32],
100
+ ];
101
+
102
+ /**
103
+ * Match Playwright's pinned US layout for literal typing. Uppercase and shifted punctuation
104
+ * carry their literal text without synthesizing Shift. Newlines also use insertText so a text
105
+ * payload cannot submit a form; only an explicit Enter or submit option may do that.
106
+ */
107
+ export const description = (character: string): Description | undefined => {
108
+ // Prose correction uses the same raw key path so its deletion has a matched release.
109
+ if (character === "Backspace")
110
+ return { code: "Backspace", keyCode: 8, key: "Backspace", text: "" };
111
+ if (character.length !== 1) return undefined;
112
+ if (/^[A-Za-z]$/.test(character)) {
113
+ const upper = character.toUpperCase();
114
+
115
+ return { code: `Key${upper}`, keyCode: upper.charCodeAt(0), key: character, text: character };
116
+ }
117
+ const digit = "0123456789".indexOf(character);
118
+ const number = digit === -1 ? ")!@#$%^&*(".indexOf(character) : digit;
119
+
120
+ if (number !== -1)
121
+ return { code: `Digit${number}`, keyCode: 48 + number, key: character, text: character };
122
+ const symbol = punctuation.find(([characters]) => characters.includes(character));
123
+
124
+ return symbol === undefined
125
+ ? undefined
126
+ : { code: symbol[1], keyCode: symbol[2], key: character, text: character };
127
+ };