effect-browser 0.2.0-beta.9 → 0.3.0-beta.6
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.
- package/README.md +707 -1184
- package/dist/Browser-B5AV7zPS.mjs +437 -0
- package/dist/Browser-B5AV7zPS.mjs.map +1 -0
- package/dist/Browser.d.mts +115 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +202 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +295 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-DVP2IJWm.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-D4281dIi.mjs +618 -0
- package/dist/Page-D4281dIi.mjs.map +1 -0
- package/dist/Page-DYTdVhYa.d.mts +593 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-5MdsRzE5.mjs +314 -0
- package/dist/Plan-5MdsRzE5.mjs.map +1 -0
- package/dist/Plan-CNNOU1Tx.d.mts +112 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +54 -0
- package/dist/Stage.mjs +133 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-BtEG93gR.mjs +420 -0
- package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
- package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-DAYjATgC.mjs +137 -0
- package/dist/context-DAYjATgC.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-BkRcLCWZ.mjs +5097 -0
- package/dist/page-BkRcLCWZ.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-CcL0K7S0.mjs +262 -0
- package/dist/url-CcL0K7S0.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +625 -0
- package/src/BrowserError.ts +225 -0
- package/src/BrowserEvent.ts +294 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +520 -0
- package/src/Plan.ts +354 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +223 -0
- package/src/Supervisor.ts +353 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +183 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +322 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +159 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +302 -0
- package/src/internal/page/lane.ts +301 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +137 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +778 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +154 -0
- package/src/internal/reading/ready.ts +105 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +265 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +93 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
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 the script may have started before: without its context or
|
|
154
|
+
// the library's API in it, the call never ran.
|
|
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.tapError((error) =>
|
|
165
|
+
error.reason._tag === "Failed" &&
|
|
166
|
+
/Cannot find context|__effectBrowser/i.test(error.reason.detail)
|
|
167
|
+
? Effect.void
|
|
168
|
+
: mayHaveRun,
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
Effect.catchIf(contextGone, () =>
|
|
173
|
+
failWith(
|
|
174
|
+
operation,
|
|
175
|
+
new NotActionable({ detail: "the page changed before the approved input could run" }),
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return { preparePolicy, mutate };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
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
|
+
};
|