effect-browser 0.3.0-beta.14 → 0.3.0-beta.16

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 (83) hide show
  1. package/README.md +24 -11
  2. package/dist/{Browser-BBSySyjT.mjs → Browser-DRPthkNO.mjs} +4 -4
  3. package/dist/Browser-DRPthkNO.mjs.map +1 -0
  4. package/dist/Browser.d.mts +2 -2
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/{BrowserEvent-BRu2juEz.d.mts → BrowserEvent-dq6FRT8w.d.mts} +9 -9
  7. package/dist/BrowserEvent.d.mts +1 -1
  8. package/dist/BrowserEvent.mjs +7 -12
  9. package/dist/BrowserEvent.mjs.map +1 -1
  10. package/dist/Cdp.mjs +2 -2
  11. package/dist/{Change-iMlcuzVD.d.mts → Change-DqING3tr.d.mts} +2 -2
  12. package/dist/Change.d.mts +1 -1
  13. package/dist/Chromium.mjs +2 -2
  14. package/dist/Expect.d.mts +1 -1
  15. package/dist/Expect.mjs +2 -2
  16. package/dist/Moment.d.mts +1 -1
  17. package/dist/Moment.mjs +1 -1
  18. package/dist/Motion-DND6I-64.d.mts +40 -0
  19. package/dist/Motion.d.mts +2 -40
  20. package/dist/Motion.mjs +1 -2
  21. package/dist/Motion.mjs.map +1 -1
  22. package/dist/{Page-B5cjaUp5.mjs → Page-C_VkBVwI.mjs} +5 -9
  23. package/dist/Page-C_VkBVwI.mjs.map +1 -0
  24. package/dist/{Page-DL9eylp3.d.mts → Page-jHmSJeOS.d.mts} +6 -13
  25. package/dist/Page.d.mts +1 -1
  26. package/dist/Page.mjs +1 -1
  27. package/dist/{Plan-6As6vcwT.d.mts → Plan-DLtRksuu.d.mts} +3 -3
  28. package/dist/{Plan-CXA2BW-G.mjs → Plan-X-JkACAB.mjs} +6 -9
  29. package/dist/Plan-X-JkACAB.mjs.map +1 -0
  30. package/dist/Plan.d.mts +1 -1
  31. package/dist/Plan.mjs +1 -1
  32. package/dist/Policy.d.mts +1 -1
  33. package/dist/Policy.mjs +1 -1
  34. package/dist/Presentation.d.mts +2 -2
  35. package/dist/Presentation.mjs +3 -2
  36. package/dist/Presentation.mjs.map +1 -1
  37. package/dist/Stage.d.mts +1 -1
  38. package/dist/Stage.mjs +3 -3
  39. package/dist/Stage.mjs.map +1 -1
  40. package/dist/{Supervisor-BE2sGHBK.mjs → Supervisor-BgWP8o9t.mjs} +116 -66
  41. package/dist/Supervisor-BgWP8o9t.mjs.map +1 -0
  42. package/dist/{Supervisor-W60pZWHh.d.mts → Supervisor-mVPeEUp8.d.mts} +33 -12
  43. package/dist/Supervisor.d.mts +2 -2
  44. package/dist/Supervisor.mjs +2 -2
  45. package/dist/{context-BAjRj7Xo.mjs → context-rYTk-3Cv.mjs} +68 -3
  46. package/dist/context-rYTk-3Cv.mjs.map +1 -0
  47. package/dist/index.d.mts +6 -6
  48. package/dist/index.mjs +4 -4
  49. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -1
  50. package/dist/{page-B92QnRfT.mjs → page-ClEMFnBG.mjs} +48 -110
  51. package/dist/page-ClEMFnBG.mjs.map +1 -0
  52. package/package.json +1 -1
  53. package/src/Browser.ts +1 -1
  54. package/src/BrowserEvent.ts +7 -12
  55. package/src/Motion.ts +1 -2
  56. package/src/Page.ts +5 -9
  57. package/src/Plan.ts +1 -2
  58. package/src/Presentation.ts +2 -1
  59. package/src/Stage.ts +2 -1
  60. package/src/Supervisor.ts +168 -166
  61. package/src/internal/input/actions.ts +25 -33
  62. package/src/internal/input/dispatch.ts +3 -4
  63. package/src/internal/input/edit.inpage.ts +3 -9
  64. package/src/internal/input/evidence.inpage.ts +2 -20
  65. package/src/internal/input/guard.inpage.ts +9 -29
  66. package/src/internal/input/guard.ts +12 -1
  67. package/src/internal/input/targets.inpage.ts +4 -8
  68. package/src/internal/input/targets.ts +8 -11
  69. package/src/internal/page/context.ts +2 -2
  70. package/src/internal/pictures/capture.ts +4 -4
  71. package/src/internal/pictures/clock.ts +8 -3
  72. package/src/internal/pictures/pictures.ts +2 -1
  73. package/src/internal/reading/controls.inpage.ts +8 -1
  74. package/src/internal/reading/match.inpage.ts +2 -1
  75. package/src/internal/reading/outline.inpage.ts +7 -4
  76. package/src/internal/reading/reading.ts +5 -16
  77. package/src/internal/supervisor/lifecycle.ts +73 -20
  78. package/dist/Browser-BBSySyjT.mjs.map +0 -1
  79. package/dist/Page-B5cjaUp5.mjs.map +0 -1
  80. package/dist/Plan-CXA2BW-G.mjs.map +0 -1
  81. package/dist/Supervisor-BE2sGHBK.mjs.map +0 -1
  82. package/dist/context-BAjRj7Xo.mjs.map +0 -1
  83. package/dist/page-B92QnRfT.mjs.map +0 -1
@@ -32,12 +32,11 @@ const partsOf = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
32
32
 
33
33
  return {
34
34
  page,
35
- bridge,
36
35
  ...settling,
37
36
  viewportFor: viewport.viewportFor,
38
37
  perform: Perform.make(page, dispatch),
39
38
  ...guard,
40
- ...Targets.make(bridge, viewport, guard, pointer, dispatch, settling),
39
+ ...Targets.make(viewport, guard, pointer, dispatch, settling),
41
40
  ...pointer,
42
41
  ...Keyboard.make(page, bridge, dispatch),
43
42
  sendMouse: dispatch.sendMouse,
@@ -110,16 +109,19 @@ const click = (input: Parts) => {
110
109
  yield* marks.at(point);
111
110
  yield* marks.on(resolved);
112
111
  yield* moveTo("click", marks, point, resolved.cursor);
113
- if (approval !== undefined) yield* approval.check({ presses: [{ index: 0, ...point }] });
112
+ yield* Guard.check(approval, { presses: [{ index: 0, ...point }] });
114
113
  const before = mark();
115
114
 
116
115
  yield* marks.sent;
117
116
  for (let index = 1; index <= count; index++) {
118
117
  // An earlier click of a multi-click can replace the approved control under the
119
118
  // 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(
119
+ if (index > 1)
120
+ yield* Guard.check(
121
+ approval,
122
+ { presses: [{ index: 0, ...point }] },
123
+ flush("click", marks.input),
124
+ ).pipe(
123
125
  Effect.catchIf(
124
126
  (error) => error.reason._tag === "StaleRef",
125
127
  () =>
@@ -132,7 +134,6 @@ const click = (input: Parts) => {
132
134
  ),
133
135
  ),
134
136
  );
135
- }
136
137
  yield* sendMouse("click", marks.input, {
137
138
  type: "mousePressed",
138
139
  ...point,
@@ -236,13 +237,12 @@ const drag = (input: Parts) => {
236
237
  yield* moveTo("drag", marks, start.point, start.cursor);
237
238
  // Both ends are checked before the button goes down: once it is down, a release cannot
238
239
  // 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
- });
240
+ yield* Guard.check(approval, {
241
+ presses: [
242
+ { index: 0, ...start.point },
243
+ { index: 1, ...end.point },
244
+ ],
245
+ });
246
246
  yield* marks.sent;
247
247
  yield* sendMouse("drag", marks.input, {
248
248
  type: "mousePressed",
@@ -267,7 +267,7 @@ const drag = (input: Parts) => {
267
267
 
268
268
  const typeText = (input: Parts) => {
269
269
  const { settings, now } = input.page;
270
- const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, bridge } = input;
270
+ const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, evaluate } = input;
271
271
  const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
272
272
 
273
273
  return (text: string, typeOptions: TypeOptions = {}, style: Style = plain) =>
@@ -301,11 +301,9 @@ const typeText = (input: Parts) => {
301
301
  // those before any input, on every path.
302
302
  const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
303
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)));
304
+ const typeable = yield* evaluate("type", asked, approval).pipe(
305
+ Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)),
306
+ );
309
307
 
310
308
  if ("error" in typeable)
311
309
  return yield* failWith(
@@ -327,8 +325,7 @@ const typeText = (input: Parts) => {
327
325
  // Viewers see the pointer click into the field.
328
326
  if (marks.style.shown) {
329
327
  yield* moveTo("type", marks, target.point, target.cursor);
330
- if (approval !== undefined)
331
- yield* approval.check({ presses: [{ index: 0, ...target.point }] });
328
+ yield* Guard.check(approval, { presses: [{ index: 0, ...target.point }] });
332
329
  yield* marks.sent;
333
330
  for (const type of ["mousePressed", "mouseReleased"] as const)
334
331
  yield* sendMouse("type", marks.input, {
@@ -354,7 +351,7 @@ const typeText = (input: Parts) => {
354
351
  secret ||= focused.secret;
355
352
  }
356
353
  if (!secret) yield* marks.reveal;
357
- if (approval !== undefined) yield* approval.check({ focused: true });
354
+ yield* Guard.check(approval, { focused: true });
358
355
  yield* marks.sent;
359
356
 
360
357
  // A key's handlers can move focus, and later keys then reach a control nobody approved.
@@ -362,10 +359,7 @@ const typeText = (input: Parts) => {
362
359
  // handler can split, and keys typed one at a time check that the field still has focus,
363
360
  // once the keys before are answered, before each space, which could press a button, and
364
361
  // 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 })));
362
+ const focusHeld = Guard.check(approval, { focused: true }, flush("type", marks.input));
369
363
 
370
364
  if (text === "" && replace && into !== undefined) {
371
365
  yield* sameDocument("type", since);
@@ -396,7 +390,7 @@ const typeText = (input: Parts) => {
396
390
  yield* flush("type", marks.input);
397
391
  if (typeOptions.submit === true) {
398
392
  // Typing can move focus or change the form; Enter goes only to the approved field.
399
- if (approval !== undefined) yield* approval.check({ focused: true });
393
+ yield* Guard.check(approval, { focused: true });
400
394
  yield* sameDocument("type", since);
401
395
  const before = mark();
402
396
 
@@ -447,7 +441,7 @@ const press = (input: Parts) => {
447
441
  );
448
442
 
449
443
  if ("error" in focused) return yield* Guard.editFailure("press", on, focused);
450
- if (approval !== undefined) yield* approval.check({ focused: true });
444
+ yield* Guard.check(approval, { focused: true });
451
445
  }
452
446
  const before = mark();
453
447
  let due = now();
@@ -456,10 +450,8 @@ const press = (input: Parts) => {
456
450
  for (let index = 0; index < times; index++) {
457
451
  // A repeated key stays in the document it began in. Under a guard, a repeated Enter
458
452
  // 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
- }
453
+ if (index > 0 && activates)
454
+ yield* Guard.check(approval, { focused: true }, flush("press", marks.input));
463
455
  yield* sameDocument("press", since);
464
456
  yield* keyStroke("press", marks.input, parts, hold);
465
457
  if (index + 1 < times) {
@@ -12,20 +12,19 @@ import {
12
12
  TextInserted,
13
13
  WheelScrolled,
14
14
  } from "../../BrowserEvent.ts";
15
+ import type { Point } from "../../Motion.ts";
15
16
  import { type PageContext, reasonOf, undispatched } from "../page/context.ts";
16
17
  import * as BrowserClock from "../pictures/clock.ts";
17
18
  import type * as Replies from "./replies.ts";
18
19
 
19
- export type MouseEvent = {
20
+ export interface MouseEvent extends Point {
20
21
  readonly type: "mouseMoved" | "mousePressed" | "mouseReleased" | "mouseWheel";
21
- readonly x: number;
22
- readonly y: number;
23
22
  readonly button?: "none" | "left" | "right" | "middle";
24
23
  readonly buttons?: number;
25
24
  readonly clickCount?: number;
26
25
  readonly deltaX?: number;
27
26
  readonly deltaY?: number;
28
- };
27
+ }
29
28
 
30
29
  export const make = (page: PageContext) => {
31
30
  const { id, pointer, publish, now, noteInput, closedBy } = page;
@@ -10,22 +10,16 @@ import type { ContextReader } from "../reading/context.inpage.ts";
10
10
  import type { Names } from "../reading/names.inpage.ts";
11
11
  import type { Guard } from "./guard.inpage.ts";
12
12
 
13
- export type EditResult =
14
- | { readonly ok: true; readonly detail: string }
15
- | { readonly error: string; readonly stale?: boolean };
13
+ export type EditResult = typeof EditResultSchema.Encoded;
16
14
 
17
15
  /** 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 };
16
+ export type FocusResult = typeof FocusResultSchema.Encoded;
21
17
 
22
18
  /**
23
19
  * Whether text may be typed, and whether its field is secret. Typing into focus also names the
24
20
  * focused field, when it is one, so the action records what it typed into.
25
21
  */
26
- export type TypeableResult =
27
- | { readonly ok: true; readonly secret: boolean; readonly subject?: typeof Subject.Encoded }
28
- | { readonly error: "stale" | "untypeable"; readonly detail: string };
22
+ export type TypeableResult = typeof TypeableResultSchema.Encoded;
29
23
 
30
24
  export const edit = (names: Names, guard: Guard, placing: ContextReader) => {
31
25
  const {
@@ -7,28 +7,10 @@ import { Schema } from "effect";
7
7
  import type { ContextReader } from "../reading/context.inpage.ts";
8
8
  import type { Names } from "../reading/names.inpage.ts";
9
9
 
10
- export interface FormField {
11
- readonly type: string;
12
- readonly name: string;
13
- readonly autocomplete?: string | undefined;
14
- readonly filled: boolean;
15
- }
10
+ export type FormField = typeof FormFieldSchema.Encoded;
16
11
 
17
12
  /** 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
- }
13
+ export type Evidence = typeof EvidenceSchema.Encoded;
32
14
 
33
15
  type Field = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
34
16
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Schema } from "effect";
7
7
 
8
+ import type { Point } from "../../Motion.ts";
8
9
  import type { Names } from "../reading/names.inpage.ts";
9
10
  import { type Evidence, type EvidenceReader, EvidenceSchema } from "./evidence.inpage.ts";
10
11
  import type { Targets } from "./targets.inpage.ts";
@@ -23,48 +24,26 @@ export type Fact = typeof Fact.Type;
23
24
 
24
25
  export interface InputPlan {
25
26
  readonly action: string;
26
- readonly targets: ReadonlyArray<string | { readonly x: number; readonly y: number } | null>;
27
+ readonly targets: ReadonlyArray<string | Point | null>;
27
28
  readonly submit: boolean;
28
29
  readonly keys: string | null;
29
30
  readonly destination: string | null;
30
31
  }
31
32
 
32
- export interface InspectedTarget {
33
- readonly ref: string;
34
- readonly element: string;
35
- readonly role: string | null;
36
- readonly name: string;
37
- readonly cursor: string;
38
- readonly href?: string | undefined;
39
- /** A field the DOM marks as holding a password, a one-time code or a card's details. */
40
- readonly secret: boolean;
41
- readonly fingerprint: string;
42
- }
33
+ export type InspectedTarget = NonNullable<PreparedInput["targets"][number]>;
43
34
 
44
- export interface PreparedInput {
45
- readonly url: string;
46
- readonly targets: ReadonlyArray<InspectedTarget | null>;
47
- readonly facts: ReadonlyArray<Fact>;
48
- readonly destination?: string | undefined;
49
- readonly evidence: Evidence;
50
- }
35
+ export type PreparedInput = Exclude<PreparedInputResult, InputFailure>;
51
36
 
52
37
  /** A refusal; a stale target is named by its index in the plan. */
53
- export interface InputFailure {
54
- readonly error: string;
55
- readonly detail: string;
56
- readonly index?: number | undefined;
57
- }
38
+ export type InputFailure = typeof InputPreparationError.Encoded;
58
39
 
59
- export type PreparedInputResult = PreparedInput | InputFailure;
40
+ export type PreparedInputResult = typeof PreparedInputResultSchema.Encoded;
60
41
 
61
- export type ValidatedInputResult = { readonly ok: true } | InputFailure;
42
+ export type ValidatedInputResult = typeof ValidatedInputResultSchema.Encoded;
62
43
 
63
44
  /** A point where a target, by its index in the plan, is about to receive a press. */
64
- export interface Press {
45
+ export interface Press extends Point {
65
46
  readonly index: number;
66
- readonly x: number;
67
- readonly y: number;
68
47
  }
69
48
 
70
49
  export interface ValidationOptions {
@@ -587,6 +566,7 @@ export const PreparedInputResultSchema = Schema.Union([
587
566
  name: Schema.String,
588
567
  cursor: Schema.String,
589
568
  href: Schema.optional(Schema.String),
569
+ /** A field the DOM marks as holding a password, a one-time code or a card's details. */
590
570
  secret: Schema.Boolean,
591
571
  fingerprint: Schema.String,
592
572
  }),
@@ -22,6 +22,13 @@ export interface PolicyPlan {
22
22
  readonly validate: Effect.Effect<Approval, BrowserError>;
23
23
  }
24
24
 
25
+ /** Recheck an approval, after any pending input it must see; neither runs without a guard. */
26
+ export const check = (
27
+ approval: Approval | undefined,
28
+ options: Script.ValidationOptions = {},
29
+ before: Effect.Effect<void, BrowserError> = Effect.void,
30
+ ) => (approval === undefined ? Effect.void : before.pipe(Effect.andThen(approval.check(options))));
31
+
25
32
  // A ref that names nothing is stale on every path, named by the target that failed.
26
33
  export const inputFailure = (
27
34
  operation: string,
@@ -178,7 +185,11 @@ export const make = (page: PageContext, bridge: Bridge) => {
178
185
  ),
179
186
  );
180
187
 
181
- return { preparePolicy, mutate };
188
+ /** A read may follow the current document only when no approval binds it to one. */
189
+ const evaluate = (operation: string, call: string, approval: Approval | undefined) =>
190
+ approval === undefined ? bridge.evaluate(operation, call) : mutate(operation, call, approval);
191
+
192
+ return { preparePolicy, mutate, evaluate };
182
193
  };
183
194
 
184
195
  export type Guard = ReturnType<typeof make>;
@@ -6,6 +6,7 @@
6
6
  import { Schema } from "effect";
7
7
 
8
8
  import { Box, SubjectContext } from "../../BrowserEvent.ts";
9
+ import { Point } from "../../Motion.ts";
9
10
  import type { ContextReader } from "../reading/context.inpage.ts";
10
11
  import type { Names } from "../reading/names.inpage.ts";
11
12
  import type { Walk } from "../reading/walk.inpage.ts";
@@ -78,10 +79,7 @@ export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
78
79
  };
79
80
  };
80
81
 
81
- const point = (
82
- target: string | { readonly x: number; readonly y: number },
83
- scroll = true,
84
- ): PointResult => {
82
+ const point = (target: string | Point, scroll = true): PointResult => {
85
83
  if (typeof target !== "string") {
86
84
  const { x, y } = target;
87
85
 
@@ -263,8 +261,7 @@ export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
263
261
  export type Targets = ReturnType<typeof targets>;
264
262
 
265
263
  const ResolvedPointSchema = Schema.Struct({
266
- x: Schema.Finite,
267
- y: Schema.Finite,
264
+ ...Point.fields,
268
265
  element: Schema.String,
269
266
  tag: Schema.String,
270
267
  role: Schema.NullOr(Schema.String),
@@ -285,8 +282,7 @@ export const PointResultSchema = Schema.Union([
285
282
 
286
283
  export const ScrollPlanSchema = Schema.NullOr(
287
284
  Schema.Struct({
288
- x: Schema.Finite,
289
- y: Schema.Finite,
285
+ ...Point.fields,
290
286
  dx: Schema.Finite,
291
287
  dy: Schema.Finite,
292
288
  }),
@@ -7,11 +7,11 @@ import { Effect } from "effect";
7
7
 
8
8
  import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
9
9
  import { ResolvedTarget, type Target } from "../../Page.ts";
10
- import { type Bridge, scriptCall } from "../page/bridge.ts";
10
+ import { scriptCall } from "../page/bridge.ts";
11
11
  import { decodeWith, failWith } from "../page/context.ts";
12
12
  import type { Viewport } from "../page/viewport.ts";
13
13
  import type { Dispatch } from "./dispatch.ts";
14
- import type { Approval, Guard } from "./guard.ts";
14
+ import { type Approval, check, type Guard } from "./guard.ts";
15
15
  import type { InputMarks } from "./perform.ts";
16
16
  import type { Pointer } from "./pointer.ts";
17
17
  import type { Settle } from "./settle.ts";
@@ -45,15 +45,13 @@ const resolvedTarget = (result: Script.ResolvedPoint): ResolvedTarget =>
45
45
  });
46
46
 
47
47
  export const make = (
48
- bridge: Bridge,
49
48
  viewport: Viewport,
50
49
  guard: Guard,
51
50
  pointer: Pointer,
52
51
  dispatch: Dispatch,
53
52
  settling: Settle,
54
53
  ) => {
55
- const { evaluate } = bridge;
56
- const { mutate } = guard;
54
+ const { evaluate, mutate } = guard;
57
55
  const { moveTo, wheel } = pointer;
58
56
  const { flush } = dispatch;
59
57
 
@@ -63,10 +61,9 @@ export const make = (
63
61
  approval: Approval | undefined,
64
62
  scroll: boolean,
65
63
  ) =>
66
- (approval === undefined
67
- ? evaluate(operation, scriptCall("point", target, scroll))
68
- : mutate(operation, scriptCall("point", target, scroll), approval)
69
- ).pipe(Effect.flatMap(decodeWith(operation, Script.PointResultSchema)));
64
+ evaluate(operation, scriptCall("point", target, scroll), approval).pipe(
65
+ Effect.flatMap(decodeWith(operation, Script.PointResultSchema)),
66
+ );
70
67
 
71
68
  const resolve = (operation: string, target: Target, approval?: Approval, scroll = true) => {
72
69
  if (typeof target === "string" && !/^e\d+$/.test(target))
@@ -127,7 +124,7 @@ export const make = (
127
124
  );
128
125
  yield* flush(operation, marks.input);
129
126
  yield* settling.settle(operation, before);
130
- if (approval !== undefined) yield* approval.check();
127
+ yield* check(approval);
131
128
  }
132
129
 
133
130
  // Wheels may be prevented or the target may need unsupported nested/frame geometry. One
@@ -137,7 +134,7 @@ export const make = (
137
134
 
138
135
  yield* resolve(operation, target, approval);
139
136
  yield* settling.settle(operation, before);
140
- if (approval !== undefined) yield* approval.check();
137
+ yield* check(approval);
141
138
 
142
139
  return yield* resolve(operation, target, approval, false);
143
140
  });
@@ -22,7 +22,7 @@ import type { BrowserEvent } from "../../BrowserEvent.ts";
22
22
  import type { InputGuard, Point } from "../../Page.ts";
23
23
  import type { Gate } from "../input/gate.ts";
24
24
  import { Current } from "../input/replies.ts";
25
- import type * as BrowserClock from "../pictures/clock.ts";
25
+ import * as BrowserClock from "../pictures/clock.ts";
26
26
  import * as Lane from "./lane.ts";
27
27
  import type { Openings } from "./openings.ts";
28
28
  import * as Url from "./url.ts";
@@ -168,7 +168,7 @@ export const decodeWith =
168
168
 
169
169
  export const make = (options: MakeOptions, scope: Scope.Scope) => {
170
170
  const { id, clock } = options;
171
- const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
171
+ const now = () => BrowserClock.monotonicMillis(clock);
172
172
  // Deadlines and pacing guard the page's lane, so page operations run on the owner's clock; a
173
173
  // caller's clock, such as a TestClock, cannot stall or stretch them.
174
174
  const owned = Effect.provideService(Clock.Clock, clock);
@@ -18,7 +18,7 @@ import {
18
18
  import { type BrowserError, InvalidRequest } from "../../BrowserError.ts";
19
19
  import { BrowserPaint, CaptureStats, Frame, type ScreencastOptions } from "../../Frame.ts";
20
20
  import { undispatched } from "../page/context.ts";
21
- import { type Estimate, toHostTime, uncertaintyAt } from "./clock.ts";
21
+ import { type Estimate, monotonicMillis, toHostTime, uncertaintyAt } from "./clock.ts";
22
22
  import { tally } from "./counts.ts";
23
23
  import * as Quiescence from "./quiescence.ts";
24
24
  import type { NativeFrame, Transport } from "./transport.ts";
@@ -154,7 +154,7 @@ const timed = (
154
154
  hostTime,
155
155
  uncertaintyMillis: uncertaintyAt(estimate, hostTime),
156
156
  }),
157
- receivedAt: Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6,
157
+ receivedAt: monotonicMillis(options.clock),
158
158
  width: size.width,
159
159
  height: size.height,
160
160
  ...generation.transport.frameTag(),
@@ -222,7 +222,7 @@ const sizeFilter = (
222
222
  Queue.offerUnsafe(wake, undefined);
223
223
  };
224
224
 
225
- const now = () => Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6;
225
+ const now = () => monotonicMillis(options.clock);
226
226
 
227
227
  const adopt = (adopted: Run) => {
228
228
  run = undefined;
@@ -470,7 +470,7 @@ export const make = (options: Options) =>
470
470
  let latest = Option.none<Frame>();
471
471
  let history: ReadonlyArray<Frame> = [];
472
472
  const keepMillis = Duration.toMillis(options.frameHistory);
473
- const now = () => Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6;
473
+ const now = () => monotonicMillis(options.clock);
474
474
  const counts = tally(now);
475
475
  // Frames delivered, which number them so that a reader can tell what it missed.
476
476
  let delivered = 0;
@@ -1,8 +1,13 @@
1
1
  /**
2
- * The browser's clock read on the owner's: estimates of their offset from timed probes, how an
3
- * estimate ages, and the one mapping a browser's pages share. `pictures.ts` takes the probes.
2
+ * Host monotonic milliseconds and the browser's clock read on the owner's: estimates of their
3
+ * offset from timed probes, how an estimate ages, and the mapping a browser's pages share.
4
+ * `pictures.ts` takes the probes.
4
5
  */
5
- import { Duration, Effect, Option } from "effect";
6
+ import { type Clock, Duration, Effect, Option } from "effect";
7
+
8
+ /** Read the captured owner's clock, including from native callbacks, without changing its origin. */
9
+ export const monotonicMillis = (clock: Clock.Clock): number =>
10
+ Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
6
11
 
7
12
  export interface Probe {
8
13
  readonly hostStart: number;
@@ -8,6 +8,7 @@ import { Duration, Effect, Option, Schedule, Schema } from "effect";
8
8
 
9
9
  import { BrowserError, Failed, InvalidRequest } from "../../BrowserError.ts";
10
10
  import { Frame, Image, Screenshot } from "../../Frame.ts";
11
+ import type { Point } from "../../Motion.ts";
11
12
  import { type FrameOptions, Region, type ScreenshotOptions, Zoom } from "../../Page.ts";
12
13
  import { type Bridge, scriptCall } from "../page/bridge.ts";
13
14
  import { decodeWith, failWith, type PageContext, reasonOf, undispatched } from "../page/context.ts";
@@ -219,7 +220,7 @@ const camera = (
219
220
  operation: string,
220
221
  format: Format,
221
222
  quality: number,
222
- clip?: Capture.Size & { readonly x: number; readonly y: number; readonly scale: number },
223
+ clip?: Capture.Size & Point & { readonly scale: number },
223
224
  ) =>
224
225
  take(operation, format, () =>
225
226
  send("Page.captureScreenshot", {
@@ -3,6 +3,8 @@
3
3
  * read to; and a condition `Page.waitFor` waits for. See `names.inpage.ts` for what a page-side part
4
4
  * may use.
5
5
  */
6
+ import { Schema } from "effect";
7
+
6
8
  import type { Control } from "../../Snapshot.ts";
7
9
  import type { Names } from "./names.inpage.ts";
8
10
  import type { Subjects } from "./subjects.inpage.ts";
@@ -17,7 +19,7 @@ export interface ConditionRequest {
17
19
  }
18
20
 
19
21
  /** Whether the condition held by the deadline, or why the selector is not CSS. */
20
- export type ConditionResult = { readonly met: boolean } | { readonly invalid: string };
22
+ export type ConditionResult = typeof ConditionResultSchema.Encoded;
21
23
 
22
24
  export const controls = (names: Names, walked: Walk, subjects: Subjects, texts: Texts) => {
23
25
  const { clean, isDisabled, isHtml, isInput, isSelect, isTextArea, refFor, textOf } = names;
@@ -156,3 +158,8 @@ export const controls = (names: Names, walked: Walk, subjects: Subjects, texts:
156
158
  };
157
159
 
158
160
  export type Controls = ReturnType<typeof controls>;
161
+
162
+ export const ConditionResultSchema = Schema.Union([
163
+ Schema.Struct({ met: Schema.Boolean }),
164
+ Schema.Struct({ invalid: Schema.String }),
165
+ ]);
@@ -10,6 +10,7 @@
10
10
  * its row, column, label or heading. Replay chooses among candidates by the same rule.
11
11
  */
12
12
  import type { SubjectContext } from "../../BrowserEvent.ts";
13
+ import type { Point } from "../../Motion.ts";
13
14
 
14
15
  /** A string, or a pattern sent to the page as its source and flags. */
15
16
  export type Wanted = string | { readonly source: string; readonly flags: string };
@@ -19,7 +20,7 @@ export interface FindRequest {
19
20
  readonly name: Wanted | null;
20
21
  readonly text: Wanted | null;
21
22
  readonly near: string | null;
22
- readonly at: { readonly x: number; readonly y: number } | null;
23
+ readonly at: Point | null;
23
24
  readonly scope: "viewport" | "document";
24
25
  readonly firstRef: number;
25
26
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Schema } from "effect";
6
6
 
7
- import type { Control, Snapshot } from "../../Snapshot.ts";
7
+ import { type Control, Snapshot } from "../../Snapshot.ts";
8
8
  import type { addresses } from "../page/url.ts";
9
9
  import type { Controls } from "./controls.inpage.ts";
10
10
  import type { Names } from "./names.inpage.ts";
@@ -25,9 +25,7 @@ export interface SnapshotRequest {
25
25
  * The outline as a `Snapshot` reads it, and the next ref the page may give, or why `within` is not
26
26
  * a selector.
27
27
  */
28
- export type SnapshotResult =
29
- | { readonly snapshot: typeof Snapshot.Encoded; readonly nextRef: number }
30
- | { readonly invalid: string };
28
+ export type SnapshotResult = typeof SnapshotResultSchema.Encoded;
31
29
 
32
30
  /** The rule every address the library reports goes by, which a link's address goes by too. */
33
31
  type Url = ReturnType<typeof addresses>;
@@ -254,6 +252,11 @@ export const outline = (
254
252
 
255
253
  export type Outline = ReturnType<typeof outline>;
256
254
 
255
+ export const SnapshotResultSchema = Schema.Union([
256
+ Schema.Struct({ snapshot: Snapshot, nextRef: Schema.Finite }),
257
+ Schema.Struct({ invalid: Schema.String }),
258
+ ]);
259
+
257
260
  export const ViewportResultSchema = Schema.Struct({
258
261
  width: Schema.Finite,
259
262
  height: Schema.Finite,
@@ -11,28 +11,17 @@ import { Snapshot, type SnapshotOptions } from "../../Snapshot.ts";
11
11
  import { type Bridge, scriptCall } from "../page/bridge.ts";
12
12
  import { contextGone, decodeWith, failWith, type PageContext } from "../page/context.ts";
13
13
  import * as Url from "../page/url.ts";
14
- import type { ConditionRequest } from "./controls.inpage.ts";
14
+ import { type ConditionRequest, ConditionResultSchema } from "./controls.inpage.ts";
15
15
  import type { Want } from "./expect.inpage.ts";
16
16
  import type { FindRequest, Wanted } from "./match.inpage.ts";
17
- import type { SnapshotRequest } from "./outline.inpage.ts";
17
+ import { type SnapshotRequest, SnapshotResultSchema } from "./outline.inpage.ts";
18
18
  import { type TextRequest, TextResultSchema } from "./text.inpage.ts";
19
19
 
20
- // What `snapshot` and `find` read back, each with the next ref the page may give, and why a
21
- // snapshot's selector is not CSS.
22
- const SnapshotResult = Schema.Union([
23
- Schema.Struct({ snapshot: Snapshot, nextRef: Schema.Finite }),
24
- Schema.Struct({ invalid: Schema.String }),
25
- ]);
26
-
20
+ // What `find` reads back, with the next ref the page may give.
27
21
  const FindResults = Schema.Struct({ found: Schema.Array(Found), nextRef: Schema.Finite });
28
22
 
29
23
  const UntilResult = Schema.Struct({ met: Schema.Boolean, mark: Schema.String });
30
24
 
31
- const ConditionResult = Schema.Union([
32
- Schema.Struct({ met: Schema.Boolean }),
33
- Schema.Struct({ invalid: Schema.String }),
34
- ]);
35
-
36
25
  // A pattern crosses into the page as its source and flags.
37
26
  const wanted = (value: string | RegExp | undefined): Wanted | null =>
38
27
  value === undefined
@@ -76,7 +65,7 @@ export const make = Effect.fnUntraced(function* (page: PageContext, bridge: Brid
76
65
  const request: SnapshotRequest = { ...asked, firstRef: yield* Ref.get(nextRef) };
77
66
 
78
67
  const result = yield* evaluate("snapshot", scriptCall("snapshot", request)).pipe(
79
- Effect.flatMap(decodeWith("snapshot", SnapshotResult)),
68
+ Effect.flatMap(decodeWith("snapshot", SnapshotResultSchema)),
80
69
  );
81
70
 
82
71
  if ("invalid" in result)
@@ -167,7 +156,7 @@ export const make = Effect.fnUntraced(function* (page: PageContext, bridge: Brid
167
156
  const unmet = () => failWith("waitFor", new Timeout({ millis }));
168
157
 
169
158
  const look = evaluate("waitFor", scriptCall("waitUntil", request, millis)).pipe(
170
- Effect.flatMap(decodeWith("waitFor", ConditionResult)),
159
+ Effect.flatMap(decodeWith("waitFor", ConditionResultSchema)),
171
160
  Effect.flatMap((result) =>
172
161
  "invalid" in result
173
162
  ? failWith("waitFor", new InvalidRequest({ detail: result.invalid }))