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,610 @@
|
|
|
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: {
|
|
279
|
+
replace: typeOptions.into === undefined ? false : typeOptions.replace,
|
|
280
|
+
submit: typeOptions.submit,
|
|
281
|
+
},
|
|
282
|
+
text,
|
|
283
|
+
secret: true,
|
|
284
|
+
},
|
|
285
|
+
settings.actionTimeout,
|
|
286
|
+
preparePolicy("type", { target: typeOptions.into, text }, [typeOptions.into ?? null], {
|
|
287
|
+
submit: typeOptions.submit ?? false,
|
|
288
|
+
}),
|
|
289
|
+
(marks, approval) =>
|
|
290
|
+
Effect.gen(function* () {
|
|
291
|
+
const replace = typeOptions.replace ?? true;
|
|
292
|
+
const into = typeOptions.into;
|
|
293
|
+
|
|
294
|
+
if (into !== undefined && !/^e\d+$/.test(into))
|
|
295
|
+
return yield* failWith(
|
|
296
|
+
"type",
|
|
297
|
+
new InvalidRequest({ detail: `"${into}" is not a ref; refs look like e12` }),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// A typed space or letter can press a focused button or change a control. Refuse
|
|
301
|
+
// those before any input, on every path.
|
|
302
|
+
const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
|
|
303
|
+
|
|
304
|
+
const typeable = yield* (
|
|
305
|
+
approval === undefined
|
|
306
|
+
? bridge.evaluate("type", asked)
|
|
307
|
+
: mutate("type", asked, approval)
|
|
308
|
+
).pipe(Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)));
|
|
309
|
+
|
|
310
|
+
if ("error" in typeable)
|
|
311
|
+
return yield* failWith(
|
|
312
|
+
"type",
|
|
313
|
+
typeable.error === "stale" && into !== undefined
|
|
314
|
+
? new StaleRef({ ref: into })
|
|
315
|
+
: new NotActionable({ detail: typeable.detail }),
|
|
316
|
+
);
|
|
317
|
+
if (typeable.subject !== undefined) yield* marks.on(typeable.subject);
|
|
318
|
+
|
|
319
|
+
const since = yield* currentDocument("type");
|
|
320
|
+
let secret = typeable.secret;
|
|
321
|
+
|
|
322
|
+
if (into !== undefined) {
|
|
323
|
+
const target = yield* targetFor("type", into, approval, marks);
|
|
324
|
+
|
|
325
|
+
yield* marks.at(target.point);
|
|
326
|
+
yield* marks.on(target);
|
|
327
|
+
// Viewers see the pointer click into the field.
|
|
328
|
+
if (marks.style.shown) {
|
|
329
|
+
yield* moveTo("type", marks, target.point, target.cursor);
|
|
330
|
+
if (approval !== undefined)
|
|
331
|
+
yield* approval.check({ presses: [{ index: 0, ...target.point }] });
|
|
332
|
+
yield* marks.sent;
|
|
333
|
+
for (const type of ["mousePressed", "mouseReleased"] as const)
|
|
334
|
+
yield* sendMouse("type", marks.input, {
|
|
335
|
+
type,
|
|
336
|
+
...target.point,
|
|
337
|
+
button: "left",
|
|
338
|
+
buttons: type === "mousePressed" ? 1 : 0,
|
|
339
|
+
clickCount: 1,
|
|
340
|
+
});
|
|
341
|
+
yield* flush("type", marks.input);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Focusing can run page handlers, including navigation, before the script returns.
|
|
345
|
+
yield* marks.sent;
|
|
346
|
+
|
|
347
|
+
const focused = yield* mutate(
|
|
348
|
+
"type",
|
|
349
|
+
scriptCall("focus", into, replace),
|
|
350
|
+
approval,
|
|
351
|
+
).pipe(Effect.flatMap(decodeWith("type", Script.FocusResultSchema)));
|
|
352
|
+
|
|
353
|
+
if ("error" in focused) return yield* Guard.editFailure("type", into, focused);
|
|
354
|
+
secret ||= focused.secret;
|
|
355
|
+
}
|
|
356
|
+
if (!secret) yield* marks.reveal;
|
|
357
|
+
if (approval !== undefined) yield* approval.check({ focused: true });
|
|
358
|
+
yield* marks.sent;
|
|
359
|
+
|
|
360
|
+
// A key's handlers can move focus, and later keys then reach a control nobody approved.
|
|
361
|
+
// Under a guard, the field is approved once: plain text goes in one insertion, which no
|
|
362
|
+
// handler can split, and keys typed one at a time check that the field still has focus,
|
|
363
|
+
// once the keys before are answered, before each space, which could press a button, and
|
|
364
|
+
// after the last key.
|
|
365
|
+
const focusHeld =
|
|
366
|
+
approval === undefined
|
|
367
|
+
? Effect.void
|
|
368
|
+
: flush("type", marks.input).pipe(Effect.andThen(approval.check({ focused: true })));
|
|
369
|
+
|
|
370
|
+
if (text === "" && replace && into !== undefined) {
|
|
371
|
+
yield* sameDocument("type", since);
|
|
372
|
+
yield* keyStroke("type", marks.input, ["Delete"]);
|
|
373
|
+
} else {
|
|
374
|
+
const events = yield* marks.style.typing(text, approval !== undefined);
|
|
375
|
+
const keyed = events.some((event) => event.phase === "down");
|
|
376
|
+
|
|
377
|
+
const checks =
|
|
378
|
+
approval !== undefined && keyed ? (text.match(/\s/gu)?.length ?? 0) + 1 : 0;
|
|
379
|
+
|
|
380
|
+
// Each check is about two round trips, which only keys typed one at a time need.
|
|
381
|
+
const started = now();
|
|
382
|
+
|
|
383
|
+
yield* marks.present((events.at(-1)?.afterMillis ?? 0) + checks * 250);
|
|
384
|
+
for (const event of events) {
|
|
385
|
+
const remaining = started + event.afterMillis - now();
|
|
386
|
+
|
|
387
|
+
if (remaining > 0) yield* Effect.sleep(Duration.millis(remaining));
|
|
388
|
+
if (event.phase === "down" && /\s/u.test(event.key)) yield* focusHeld;
|
|
389
|
+
if (event.phase !== "up") yield* sameDocument("type", since);
|
|
390
|
+
yield* typeEvent(marks.input, event, secret);
|
|
391
|
+
}
|
|
392
|
+
if (keyed) yield* focusHeld;
|
|
393
|
+
}
|
|
394
|
+
// Public Playwright keys preserve platform editing commands. Drain the raw text
|
|
395
|
+
// session before Enter uses Playwright's session, so submit cannot overtake typing.
|
|
396
|
+
yield* flush("type", marks.input);
|
|
397
|
+
if (typeOptions.submit === true) {
|
|
398
|
+
// Typing can move focus or change the form; Enter goes only to the approved field.
|
|
399
|
+
if (approval !== undefined) yield* approval.check({ focused: true });
|
|
400
|
+
yield* sameDocument("type", since);
|
|
401
|
+
const before = mark();
|
|
402
|
+
|
|
403
|
+
yield* keyStroke("type", marks.input, ["Enter"]);
|
|
404
|
+
yield* flush("type", marks.input);
|
|
405
|
+
yield* settle("type", before);
|
|
406
|
+
}
|
|
407
|
+
}),
|
|
408
|
+
style,
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const press = (input: Parts) => {
|
|
413
|
+
const { settings, now } = input.page;
|
|
414
|
+
const { perform, preparePolicy, currentDocument, sameDocument, keyStroke, flush } = input;
|
|
415
|
+
const { mark, settle, targetFor, mutate } = input;
|
|
416
|
+
|
|
417
|
+
return (keys: string, pressOptions: PressOptions = {}, style: Style = plain) =>
|
|
418
|
+
perform(
|
|
419
|
+
"press",
|
|
420
|
+
{ target: keys, options: { times: pressOptions.times, holdMillis: pressOptions.holdMillis } },
|
|
421
|
+
settings.actionTimeout,
|
|
422
|
+
Effect.suspend(() => pressed(keys, pressOptions)).pipe(
|
|
423
|
+
Effect.flatMap((parts) =>
|
|
424
|
+
preparePolicy(
|
|
425
|
+
"press",
|
|
426
|
+
{ target: pressOptions.on, text: parts.join("+") },
|
|
427
|
+
[pressOptions.on ?? null],
|
|
428
|
+
{ keys: parts.join("+") },
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
),
|
|
432
|
+
(marks, approval) =>
|
|
433
|
+
Effect.gen(function* () {
|
|
434
|
+
const parts = yield* pressed(keys, pressOptions);
|
|
435
|
+
const times = Math.max(1, Math.min(50, pressOptions.times ?? 1));
|
|
436
|
+
const hold = pressOptions.holdMillis ?? 0;
|
|
437
|
+
const activates = parts.at(-1) === "Enter" || parts.at(-1) === "Space";
|
|
438
|
+
const since = yield* currentDocument("press");
|
|
439
|
+
const on = pressOptions.on;
|
|
440
|
+
|
|
441
|
+
// The keys go to the element asked for, which must hold focus before the first.
|
|
442
|
+
if (on !== undefined) {
|
|
443
|
+
yield* marks.on(yield* targetFor("press", on, approval, marks));
|
|
444
|
+
|
|
445
|
+
const focused = yield* mutate("press", scriptCall("focusOn", on), approval).pipe(
|
|
446
|
+
Effect.flatMap(decodeWith("press", Script.EditResultSchema)),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if ("error" in focused) return yield* Guard.editFailure("press", on, focused);
|
|
450
|
+
if (approval !== undefined) yield* approval.check({ focused: true });
|
|
451
|
+
}
|
|
452
|
+
const before = mark();
|
|
453
|
+
let due = now();
|
|
454
|
+
|
|
455
|
+
yield* marks.sent;
|
|
456
|
+
for (let index = 0; index < times; index++) {
|
|
457
|
+
// A repeated key stays in the document it began in. Under a guard, a repeated Enter
|
|
458
|
+
// or Space must also reach the approved element after the previous press settled.
|
|
459
|
+
if (index > 0 && approval !== undefined && activates) {
|
|
460
|
+
yield* flush("press", marks.input);
|
|
461
|
+
yield* approval.check({ focused: true });
|
|
462
|
+
}
|
|
463
|
+
yield* sameDocument("press", since);
|
|
464
|
+
yield* keyStroke("press", marks.input, parts, hold);
|
|
465
|
+
if (index + 1 < times) {
|
|
466
|
+
const gap = yield* marks.style.keyGap;
|
|
467
|
+
|
|
468
|
+
yield* marks.present(gap);
|
|
469
|
+
due += hold + gap;
|
|
470
|
+
if (due > now()) yield* Effect.sleep(Duration.millis(due - now()));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
yield* flush("press", marks.input);
|
|
474
|
+
yield* settle("press", before);
|
|
475
|
+
}),
|
|
476
|
+
style,
|
|
477
|
+
);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const scroll = (input: Parts) => {
|
|
481
|
+
const { settings } = input.page;
|
|
482
|
+
const { perform, preparePolicy, viewportFor, targetFor, resolve, moveTo, wheel, flush } = input;
|
|
483
|
+
const { mark, settle } = input;
|
|
484
|
+
|
|
485
|
+
return (scrollOptions: ScrollOptions = {}, style: Style = plain) => {
|
|
486
|
+
const target = scrollOptions.at;
|
|
487
|
+
|
|
488
|
+
const valid =
|
|
489
|
+
Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0)
|
|
490
|
+
? Effect.void
|
|
491
|
+
: failWith("scroll", new InvalidRequest({ detail: "scroll deltas must be finite" }));
|
|
492
|
+
|
|
493
|
+
return perform(
|
|
494
|
+
"scroll",
|
|
495
|
+
{
|
|
496
|
+
target: target === undefined ? undefined : named(target),
|
|
497
|
+
options: { dx: scrollOptions.dx, dy: scrollOptions.dy },
|
|
498
|
+
},
|
|
499
|
+
settings.actionTimeout,
|
|
500
|
+
valid.pipe(
|
|
501
|
+
Effect.andThen(
|
|
502
|
+
// Scrolling the page has no target: whatever sits mid-viewport is not what is scrolled.
|
|
503
|
+
preparePolicy(
|
|
504
|
+
"scroll",
|
|
505
|
+
{ target: typeof target === "string" ? target : undefined },
|
|
506
|
+
target === undefined ? [] : [target],
|
|
507
|
+
),
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
(marks, approval) =>
|
|
511
|
+
Effect.gen(function* () {
|
|
512
|
+
yield* valid;
|
|
513
|
+
// Over CDP the page reports its own viewport, so the read shares the action's deadline.
|
|
514
|
+
const viewport = yield* viewportFor("scroll");
|
|
515
|
+
|
|
516
|
+
const middle = {
|
|
517
|
+
x: Math.round(viewport.width / 2),
|
|
518
|
+
y: Math.round(viewport.height / 2),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const dx = scrollOptions.dx ?? 0;
|
|
522
|
+
|
|
523
|
+
const dy =
|
|
524
|
+
scrollOptions.dy ??
|
|
525
|
+
(scrollOptions.dx === undefined ? Math.round(viewport.height * 0.8) : 0);
|
|
526
|
+
|
|
527
|
+
// A page scroll has no target, but viewers still see the cursor it lands on.
|
|
528
|
+
const resolved =
|
|
529
|
+
target !== undefined
|
|
530
|
+
? yield* targetFor("scroll", target, approval, marks)
|
|
531
|
+
: marks.style.shown
|
|
532
|
+
? yield* resolve("scroll", middle, approval).pipe(
|
|
533
|
+
Effect.orElseSucceed(() => undefined),
|
|
534
|
+
)
|
|
535
|
+
: undefined;
|
|
536
|
+
|
|
537
|
+
const point = resolved?.point ?? middle;
|
|
538
|
+
|
|
539
|
+
yield* marks.at(point);
|
|
540
|
+
if (target !== undefined && resolved !== undefined) yield* marks.on(resolved);
|
|
541
|
+
yield* moveTo("scroll", marks, point, resolved?.cursor);
|
|
542
|
+
const before = mark();
|
|
543
|
+
|
|
544
|
+
yield* marks.sent;
|
|
545
|
+
yield* wheel("scroll", marks, point, dx, dy);
|
|
546
|
+
yield* flush("scroll", marks.input);
|
|
547
|
+
yield* settle("scroll", before);
|
|
548
|
+
}),
|
|
549
|
+
style,
|
|
550
|
+
);
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const select = (input: Parts) => {
|
|
555
|
+
const { settings } = input.page;
|
|
556
|
+
const { perform, preparePolicy, targetFor, mutate } = input;
|
|
557
|
+
|
|
558
|
+
return (ref: string, values: ReadonlyArray<string>) =>
|
|
559
|
+
perform(
|
|
560
|
+
"select",
|
|
561
|
+
{ target: ref, options: { values }, text: values.join(", ") },
|
|
562
|
+
settings.actionTimeout,
|
|
563
|
+
preparePolicy("select", { target: ref, text: values.join(", ") }, [ref]),
|
|
564
|
+
(marks, approval) =>
|
|
565
|
+
Effect.gen(function* () {
|
|
566
|
+
yield* marks.on(yield* targetFor("select", ref, approval, marks));
|
|
567
|
+
|
|
568
|
+
// The script refuses before it changes anything. Only a choice it made, or a script
|
|
569
|
+
// that may have run without an intact answer, may have reached the page.
|
|
570
|
+
const result = yield* mutate(
|
|
571
|
+
"select",
|
|
572
|
+
scriptCall("select", ref, values),
|
|
573
|
+
approval,
|
|
574
|
+
marks.sent,
|
|
575
|
+
).pipe(
|
|
576
|
+
Effect.flatMap((value) =>
|
|
577
|
+
decodeWith(
|
|
578
|
+
"select",
|
|
579
|
+
Script.EditResultSchema,
|
|
580
|
+
)(value).pipe(Effect.tapError(() => marks.sent)),
|
|
581
|
+
),
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if ("error" in result) return yield* Guard.editFailure("select", ref, result);
|
|
585
|
+
yield* marks.sent;
|
|
586
|
+
|
|
587
|
+
return result.detail;
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
export const make = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
|
|
593
|
+
const parts = partsOf(page, bridge, viewport);
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
perform: parts.perform,
|
|
597
|
+
preparePolicy: parts.preparePolicy,
|
|
598
|
+
click: click(parts),
|
|
599
|
+
hover: hover(parts),
|
|
600
|
+
aim: aim(parts),
|
|
601
|
+
drag: drag(parts),
|
|
602
|
+
type: typeText(parts),
|
|
603
|
+
press: press(parts),
|
|
604
|
+
scroll: scroll(parts),
|
|
605
|
+
select: select(parts),
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/** A page's input, each action in any style. */
|
|
610
|
+
export type Input = ReturnType<typeof make>;
|