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

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 (69) hide show
  1. package/README.md +12 -4
  2. package/dist/{Browser-DRPthkNO.mjs → Browser-H63wiekr.mjs} +4 -4
  3. package/dist/Browser-H63wiekr.mjs.map +1 -0
  4. package/dist/Browser.d.mts +1 -1
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/Cdp.mjs +2 -2
  7. package/dist/Chromium.mjs +2 -2
  8. package/dist/Expect.d.mts +1 -1
  9. package/dist/Expect.mjs +2 -2
  10. package/dist/Moment.d.mts +1 -1
  11. package/dist/Moment.mjs +3 -3
  12. package/dist/Moment.mjs.map +1 -1
  13. package/dist/{Page-C_VkBVwI.mjs → Page-CfB6Zn1p.mjs} +3 -7
  14. package/dist/Page-CfB6Zn1p.mjs.map +1 -0
  15. package/dist/{Page-jHmSJeOS.d.mts → Page-ku6j5t-v.d.mts} +18 -13
  16. package/dist/Page.d.mts +1 -1
  17. package/dist/Page.mjs +1 -1
  18. package/dist/{Plan-X-JkACAB.mjs → Plan-C-d5QYS8.mjs} +3 -3
  19. package/dist/{Plan-X-JkACAB.mjs.map → Plan-C-d5QYS8.mjs.map} +1 -1
  20. package/dist/{Plan-DLtRksuu.d.mts → Plan-DJ4hEEna.d.mts} +2 -2
  21. package/dist/Plan.d.mts +1 -1
  22. package/dist/Plan.mjs +1 -1
  23. package/dist/Policy.d.mts +1 -1
  24. package/dist/Policy.mjs +1 -1
  25. package/dist/Presentation.d.mts +1 -1
  26. package/dist/Presentation.mjs +2 -2
  27. package/dist/Stage.d.mts +1 -1
  28. package/dist/Stage.mjs +7 -6
  29. package/dist/Stage.mjs.map +1 -1
  30. package/dist/{Supervisor-BgWP8o9t.mjs → Supervisor-2mPjIYYs.mjs} +4 -3
  31. package/dist/{Supervisor-BgWP8o9t.mjs.map → Supervisor-2mPjIYYs.mjs.map} +1 -1
  32. package/dist/Supervisor.mjs +1 -1
  33. package/dist/{context-rYTk-3Cv.mjs → context-w3NJPRLo.mjs} +15 -8
  34. package/dist/context-w3NJPRLo.mjs.map +1 -0
  35. package/dist/index.d.mts +2 -2
  36. package/dist/index.mjs +4 -4
  37. package/dist/{page-ClEMFnBG.mjs → page-BKwfF6z-.mjs} +169 -112
  38. package/dist/page-BKwfF6z-.mjs.map +1 -0
  39. package/package.json +1 -1
  40. package/src/Moment.ts +3 -6
  41. package/src/Page.ts +13 -11
  42. package/src/Stage.ts +8 -16
  43. package/src/Supervisor.ts +6 -7
  44. package/src/internal/input/actions.ts +28 -64
  45. package/src/internal/input/dispatch.ts +22 -7
  46. package/src/internal/input/edit.inpage.ts +23 -2
  47. package/src/internal/input/evidence.inpage.ts +0 -2
  48. package/src/internal/input/guard.ts +3 -9
  49. package/src/internal/input/perform.ts +14 -10
  50. package/src/internal/input/pointer.ts +3 -6
  51. package/src/internal/input/replies.ts +19 -8
  52. package/src/internal/input/settle.ts +3 -2
  53. package/src/internal/input/targets.inpage.ts +2 -5
  54. package/src/internal/input/targets.ts +5 -21
  55. package/src/internal/page/bridge.ts +2 -2
  56. package/src/internal/page/context.ts +13 -1
  57. package/src/internal/page/navigation.ts +3 -12
  58. package/src/internal/pictures/capture.ts +7 -0
  59. package/src/internal/pictures/motion.ts +65 -0
  60. package/src/internal/pictures/pictures.ts +20 -16
  61. package/src/internal/pictures/utilsBundle.d.ts +9 -0
  62. package/src/internal/reading/reading.ts +5 -9
  63. package/src/internal/reading/ready.ts +66 -14
  64. package/src/internal/timeline/events.ts +5 -8
  65. package/src/internal/timeline/window.ts +3 -8
  66. package/dist/Browser-DRPthkNO.mjs.map +0 -1
  67. package/dist/Page-C_VkBVwI.mjs.map +0 -1
  68. package/dist/context-rYTk-3Cv.mjs.map +0 -1
  69. package/dist/page-ClEMFnBG.mjs.map +0 -1
@@ -1,15 +1,16 @@
1
1
  import { Plan, Point } from "./Motion.mjs";
2
- import { Action, Box, CursorChanged, KeyChanged, Navigated, PageLoaded, PointerPressed, PointerReleased, Subject, SubjectContext, TextInserted, TrackEvent, TrackPerformed, TrackPlanned, WheelScrolled } from "./BrowserEvent.mjs";
2
+ import { Action, Box, CursorChanged, KeyChanged, Navigated, PageLoaded, PointerPressed, PointerReleased, Subject, TextInserted, TrackEvent, TrackPerformed, TrackPlanned, WheelScrolled } from "./BrowserEvent.mjs";
3
3
  import { BrowserError, Failed, InvalidRequest, NavigationFailed, NotActionable, NotFound, PolicyTimeout, StaleRef, Timeout } from "./BrowserError.mjs";
4
- import { a as make$20, c as estimate, d as toBrowserSeconds, f as toHostTime, h as make$19, i as failWith, m as Current, n as contextGone, o as reasonOf, p as uncertaintyAt, r as decodeWith, s as undispatched, u as monotonicMillis } from "./context-rYTk-3Cv.mjs";
4
+ import { a as make$20, c as undispatched, d as monotonicMillis, f as toBrowserSeconds, g as make$19, h as Current, i as failWith, l as estimate, m as uncertaintyAt, n as contextGone, o as reasonOf, p as toHostTime, r as decodeWith, s as refuse } from "./context-w3NJPRLo.mjs";
5
5
  import { i as redact, n as parse, s as Correlation, t as addresses } from "./url-Cd04Okid.mjs";
6
6
  import { BrowserPaint, CaptureStats, Frame, Image, Screenshot } from "./Frame.mjs";
7
- import { _ as evidence, c as State, g as guard, h as ValidatedInputResultSchema, i as InputRequest, l as Text, m as PreparedInputResultSchema, o as Region, p as redacted, r as Found, s as ResolvedTarget, u as Zoom } from "./Page-C_VkBVwI.mjs";
7
+ import { _ as evidence, c as State, g as guard, h as ValidatedInputResultSchema, i as InputRequest, l as Text, m as PreparedInputResultSchema, o as Region, p as redacted, r as Found, s as ResolvedTarget, u as Zoom } from "./Page-CfB6Zn1p.mjs";
8
8
  import { Snapshot } from "./Snapshot.mjs";
9
9
  import { t as match } from "./match.inpage-xOe9LNO0.mjs";
10
10
  import { r as Changes, t as Change } from "./Change-DNvztNLu.mjs";
11
11
  import { Window } from "./Moment.mjs";
12
12
  import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Option, Queue, Random, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, SynchronizedRef } from "effect";
13
+ import { jpegjs } from "playwright-core/lib/utilsBundle";
13
14
  //#region src/internal/input/edit.inpage.ts
14
15
  /**
15
16
  * In the page: editing a field. It refuses text that a focused control could act on, focuses a
@@ -17,7 +18,7 @@ import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Opti
17
18
  * may use.
18
19
  */
19
20
  const edit = (names, guard, placing) => {
20
- const { activeElement, clean, describe, isDisabled, isHtml, isInput, isSecret, isSelect, isTextArea, lookup, nameOf, roleOf } = names;
21
+ const { activeElement, clean, describe, inCurrentDocument, isDisabled, isHtml, isInput, isSecret, isSelect, isTextArea, lookup, nameOf, roleOf } = names;
21
22
  const { textEntry, typingRefusal } = guard;
22
23
  /** Whether text may be typed into a ref or focus, and, for a `secret`, only into a secret field. */
23
24
  const typeable = (ref, secret) => {
@@ -48,8 +49,21 @@ const edit = (names, guard, placing) => {
48
49
  }
49
50
  };
50
51
  };
51
- const focus = (ref, replace) => {
52
- const element = lookup(ref);
52
+ /**
53
+ * The focused field, when it is a text field named `name`: one a page put in place of a field
54
+ * just pressed and gave focus to, as a typeahead does.
55
+ */
56
+ const replacement = (name) => {
57
+ const active = activeElement();
58
+ return active !== null && textEntry(active) && inCurrentDocument(active) && nameOf(active, roleOf(active)) === name ? active : void 0;
59
+ };
60
+ /**
61
+ * Focus a field to type into. `follow` is the field's name when the action pressed it first: a
62
+ * page may answer the press with another field of that name in its place, focused, and the keys
63
+ * follow focus there, as a person's would.
64
+ */
65
+ const focus = (ref, replace, follow) => {
66
+ const element = lookup(ref) ?? (follow === null ? void 0 : replacement(follow));
53
67
  if (element === void 0) return {
54
68
  error: `${ref} is not on the page any more`,
55
69
  stale: true
@@ -334,11 +348,8 @@ const targets = (names, walked, placing) => {
334
348
  };
335
349
  const ResolvedPointSchema = Schema.Struct({
336
350
  ...Point.fields,
351
+ ...Subject.fields,
337
352
  element: Schema.String,
338
- tag: Schema.String,
339
- role: Schema.NullOr(Schema.String),
340
- name: Schema.String,
341
- context: SubjectContext,
342
353
  box: Schema.optional(Box),
343
354
  cursor: Schema.String,
344
355
  href: Schema.optional(Schema.String)
@@ -2074,7 +2085,7 @@ const sight = (names, walked, texts, kept) => {
2074
2085
  */
2075
2086
  const install = (makeNames, makeWalk, makeMatch, makeContext, makeSubjects, makeExpectations, makeText, makeControls, makeFold, makeHistory, makeSight, makeMarks, makeRecord, makeChanges, makeReady, makeOutline, makeTargets, makeEvidence, makeGuard, makeEdit, makeAddresses) => {
2076
2087
  const installed = globalThis.__effectBrowser;
2077
- if (installed !== void 0 && installed.version === 19) return installed;
2088
+ if (installed !== void 0 && installed.version === 20) return installed;
2078
2089
  const named = makeNames();
2079
2090
  const walked = makeWalk(named);
2080
2091
  const placing = makeContext(named, walked);
@@ -2091,7 +2102,7 @@ const install = (makeNames, makeWalk, makeMatch, makeContext, makeSubjects, make
2091
2102
  const edited = makeEdit(named, guarded, placing);
2092
2103
  const readiness = makeReady(walked, texts, kept, recorder);
2093
2104
  const api = {
2094
- version: 19,
2105
+ version: 20,
2095
2106
  snapshot: read.snapshot,
2096
2107
  find: subjected.find,
2097
2108
  until: makeExpectations(named, subjected).until,
@@ -2298,6 +2309,20 @@ const make$18 = Effect.fnUntraced(function* (page) {
2298
2309
  * protocol session, stamped with the browser's clock when its run has a mapping, and is published
2299
2310
  * as it is submitted.
2300
2311
  */
2312
+ const buttonMask = {
2313
+ none: 0,
2314
+ left: 1,
2315
+ right: 2,
2316
+ middle: 4
2317
+ };
2318
+ /** A mouse button going down or up at a point, as the `clickCount`th click. */
2319
+ const buttonAt = (type, point, button = "left", clickCount = 1) => ({
2320
+ type,
2321
+ ...point,
2322
+ button,
2323
+ buttons: type === "mousePressed" ? buttonMask[button] : 0,
2324
+ clickCount
2325
+ });
2301
2326
  const make$17 = (page) => {
2302
2327
  const { id, pointer, publish, now, noteInput, closedBy } = page;
2303
2328
  const { send } = page.protocol;
@@ -2341,16 +2366,10 @@ const make$17 = (page) => {
2341
2366
  if (event.type === "mouseReleased") return yield* run.up(held);
2342
2367
  if (event.type === "mousePressed") {
2343
2368
  yield* run.reserve(2);
2344
- yield* run.down(held, () => dispatchMouse(event, estimate, submitted), () => dispatchMouse({
2345
- type: "mouseReleased",
2346
- ...Option.getOrElse(MutableRef.get(pointer), () => ({
2347
- x: event.x,
2348
- y: event.y
2349
- })),
2350
- button: event.button ?? "left",
2351
- buttons: 0,
2352
- clickCount: event.clickCount ?? 1
2353
- }, estimate));
2369
+ yield* run.down(held, () => dispatchMouse(event, estimate, submitted), () => dispatchMouse(buttonAt("mouseReleased", Option.getOrElse(MutableRef.get(pointer), () => ({
2370
+ x: event.x,
2371
+ y: event.y
2372
+ })), event.button, event.clickCount), estimate));
2354
2373
  } else {
2355
2374
  yield* run.reserve(1);
2356
2375
  yield* run.send(() => dispatchMouse(event, estimate, submitted));
@@ -2411,8 +2430,8 @@ const make$16 = (page, bridge) => {
2411
2430
  const { current, evaluateIn, evaluateWithContext } = bridge;
2412
2431
  const preparePolicy = (action, info, targets, flags = {}) => Effect.gen(function* () {
2413
2432
  for (const target of targets) {
2414
- if (typeof target === "string" && !/^e\d+$/.test(target)) return yield* failWith(action, new InvalidRequest({ detail: `"${target}" is not a ref; refs look like e12` }));
2415
- if (target !== null && typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return yield* failWith(action, new InvalidRequest({ detail: "point coordinates must be finite" }));
2433
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return yield* refuse(action, `"${target}" is not a ref; refs look like e12`);
2434
+ if (target !== null && typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return yield* refuse(action, "point coordinates must be finite");
2416
2435
  }
2417
2436
  const input = {
2418
2437
  action,
@@ -2878,10 +2897,7 @@ const admission = (page, pending, name) => {
2878
2897
  };
2879
2898
  };
2880
2899
  /** Close a run on every exit while preserving its native replies for the page's next input. */
2881
- const useRun = (page, sender, name, run, action) => action.pipe(Effect.matchEffect({
2882
- onFailure: (error) => run.close.pipe(Effect.andThen(sender.flush(name, run).pipe(Effect.ignore)), Effect.andThen(Effect.fail(error))),
2883
- onSuccess: (value) => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.as(value))
2884
- }), Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))), Effect.provideService(Current, run));
2900
+ const useRun = (page, sender, name, run, action, canStop) => action.pipe(Effect.tapError(() => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.ignore)), Effect.tap(() => run.close.pipe(Effect.andThen(sender.flush(name, run)))), Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))), Effect.onError(() => run.recover(page.stopLoading, page.scope).pipe(Effect.when(canStop))), Effect.provideService(Current, run));
2885
2901
  const make$14 = (page, sender) => {
2886
2902
  const { id, settings, mapping, lane, publish, now, noteInput, span, owned } = page;
2887
2903
  const { inputClocks } = sender;
@@ -2916,7 +2932,7 @@ const make$14 = (page, sender) => {
2916
2932
  const useInput = (action) => input.begin.pipe(Effect.tap((current) => Effect.sync(() => {
2917
2933
  ticket.using(current);
2918
2934
  if (estimate !== void 0) inputClocks.set(current, estimate);
2919
- })), Effect.flatMap((run) => useRun(page, sender, name, run, action(run))));
2935
+ })), Effect.flatMap((run) => useRun(page, sender, name, run, action(run), info.input === false ? Effect.succeed(false) : Ref.get(sent))));
2920
2936
  let queuedMillis;
2921
2937
  const dispatch = (action) => Effect.suspend(() => {
2922
2938
  const asked = now();
@@ -3017,7 +3033,7 @@ const make$13 = (page, dispatch, viewport) => {
3017
3033
  y: Math.max(0, Math.min(size.height - 1, previous.y))
3018
3034
  };
3019
3035
  const planned = yield* style.glide(from, to, dragging);
3020
- const invalid = failWith(operation, new InvalidRequest({ detail: "motion must be a bounded, ordered schedule ending at the target" }));
3036
+ const invalid = refuse(operation, "motion must be a bounded, ordered schedule ending at the target");
3021
3037
  const samples = yield* Schema.decodeEffect(Plan)(planned).pipe(Effect.catch(() => invalid));
3022
3038
  if (samples.at(-1)?.x !== to.x || samples.at(-1)?.y !== to.y) return yield* invalid;
3023
3039
  yield* Effect.annotateCurrentSpan("samples", samples.length);
@@ -3114,12 +3130,12 @@ const make$12 = (page, bridge) => {
3114
3130
  });
3115
3131
  /** Wait for the page to settle after input sent since `before`. */
3116
3132
  const settle = (operation, before) => Effect.gen(function* () {
3117
- yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void), Effect.onError(() => page.stopLoading));
3133
+ yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void));
3118
3134
  while (document() > before.document && parsed < document()) yield* Deferred.await(changed);
3119
3135
  }).pipe(Effect.timeoutOrElse({
3120
3136
  duration: settling,
3121
3137
  orElse: () => Effect.fail("unsettled")
3122
- }), Effect.catch(() => Effect.annotateCurrentSpan("settled", false)), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
3138
+ }), Effect.catch(() => page.stopLoading.pipe(Effect.andThen(Effect.annotateCurrentSpan("settled", false)))), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
3123
3139
  return {
3124
3140
  mark,
3125
3141
  settle
@@ -3133,19 +3149,13 @@ const make$12 = (page, bridge) => {
3133
3149
  * viewers watch.
3134
3150
  */
3135
3151
  const pointFailure = (operation, target, result) => failWith(operation, result.error === "stale" && typeof target === "string" ? new StaleRef({ ref: target }) : result.error === "outside" ? new InvalidRequest({ detail: result.detail }) : new NotActionable({ detail: result.detail }));
3136
- const resolvedTarget = (result) => new ResolvedTarget({
3152
+ const resolvedTarget = ({ x, y, box, ...target }) => new ResolvedTarget({
3137
3153
  point: {
3138
- x: result.x,
3139
- y: result.y
3154
+ x,
3155
+ y
3140
3156
  },
3141
- element: result.element,
3142
- tag: result.tag,
3143
- role: result.role,
3144
- name: result.name,
3145
- context: result.context,
3146
- box: result.box,
3147
- cursor: result.cursor,
3148
- ...result.href === void 0 ? {} : { href: result.href }
3157
+ box,
3158
+ ...target
3149
3159
  });
3150
3160
  const make$11 = (viewport, guard, pointer, dispatch, settling) => {
3151
3161
  const { evaluate, mutate } = guard;
@@ -3153,8 +3163,8 @@ const make$11 = (viewport, guard, pointer, dispatch, settling) => {
3153
3163
  const { flush } = dispatch;
3154
3164
  const readPoint = (operation, target, approval, scroll) => evaluate(operation, scriptCall("point", target, scroll), approval).pipe(Effect.flatMap(decodeWith(operation, PointResultSchema)));
3155
3165
  const resolve = (operation, target, approval, scroll = true) => {
3156
- if (typeof target === "string" && !/^e\d+$/.test(target)) return failWith(operation, new InvalidRequest({ detail: target + " is not a ref; refs look like e12" }));
3157
- if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return failWith(operation, new InvalidRequest({ detail: "point coordinates must be finite" }));
3166
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return refuse(operation, target + " is not a ref; refs look like e12");
3167
+ if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return refuse(operation, "point coordinates must be finite");
3158
3168
  return readPoint(operation, target, approval, scroll).pipe(Effect.flatMap((result) => "error" in result ? pointFailure(operation, target, result) : Effect.succeed(resolvedTarget(result))));
3159
3169
  };
3160
3170
  return {
@@ -3197,12 +3207,6 @@ const make$11 = (viewport, guard, pointer, dispatch, settling) => {
3197
3207
  * `perform`, which admits it, puts it to the input guard and records it. A click, a key or a scroll
3198
3208
  * then waits for the page to settle: a task and a frame, and a document if the input asked for one.
3199
3209
  */
3200
- const buttonMask = {
3201
- none: 0,
3202
- left: 1,
3203
- right: 2,
3204
- middle: 4
3205
- };
3206
3210
  const partsOf = (page, bridge, viewport) => {
3207
3211
  const dispatch = make$17(page);
3208
3212
  const pointer = make$13(page, dispatch, viewport);
@@ -3224,13 +3228,15 @@ const partsOf = (page, bridge, viewport) => {
3224
3228
  /** Whether every number given is finite; a number left out is. */
3225
3229
  const finite = (...values) => values.every((value) => value === void 0 || Number.isFinite(value));
3226
3230
  const named = (target) => typeof target === "string" ? target : `${target.x},${target.y}`;
3231
+ /** The ref a target names, if it names one rather than a point. */
3232
+ const refOf = (target) => typeof target === "string" ? target : void 0;
3227
3233
  /** A click's options, once its counts are finite. */
3228
- const clickable = (options) => finite(options.clickCount, options.holdMillis) ? Effect.void : failWith("click", new InvalidRequest({ detail: "clickCount and holdMillis must be finite" }));
3234
+ const clickable = (options) => finite(options.clickCount, options.holdMillis) ? Effect.void : refuse("click", "clickCount and holdMillis must be finite");
3229
3235
  /** The keys a press sends, once they and its options are valid. */
3230
3236
  const pressed = (keys, options) => {
3231
3237
  const parts$1 = parts(keys);
3232
- if (!finite(options.times, options.holdMillis)) return failWith("press", new InvalidRequest({ detail: "times and holdMillis must be finite" }));
3233
- if (parts$1 === void 0) return failWith("press", new InvalidRequest({ detail: `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A` }));
3238
+ if (!finite(options.times, options.holdMillis)) return refuse("press", "times and holdMillis must be finite");
3239
+ if (parts$1 === void 0) return refuse("press", `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`);
3234
3240
  return Effect.succeed(parts$1);
3235
3241
  };
3236
3242
  const click = (input) => {
@@ -3243,7 +3249,7 @@ const click = (input) => {
3243
3249
  clickCount: clickOptions.clickCount,
3244
3250
  holdMillis: clickOptions.holdMillis
3245
3251
  }
3246
- }, settings.actionTimeout, Effect.suspend(() => clickable(clickOptions)).pipe(Effect.andThen(preparePolicy("click", { target: typeof target === "string" ? target : void 0 }, [target]))), (marks, approval) => Effect.gen(function* () {
3252
+ }, settings.actionTimeout, Effect.suspend(() => clickable(clickOptions)).pipe(Effect.andThen(preparePolicy("click", { target: refOf(target) }, [target]))), (marks, approval) => Effect.gen(function* () {
3247
3253
  yield* clickable(clickOptions);
3248
3254
  const resolved = yield* targetFor("click", target, approval, marks);
3249
3255
  const { point } = resolved;
@@ -3263,23 +3269,11 @@ const click = (input) => {
3263
3269
  index: 0,
3264
3270
  ...point
3265
3271
  }] }, flush("click", marks.input)).pipe(Effect.catchIf((error) => error.reason._tag === "StaleRef", () => failWith("click", new NotActionable({ detail: "an earlier click removed the approved target, so no further press was sent" }))));
3266
- yield* sendMouse("click", marks.input, {
3267
- type: "mousePressed",
3268
- ...point,
3269
- button,
3270
- buttons: buttonMask[button],
3271
- clickCount: index
3272
- });
3272
+ yield* sendMouse("click", marks.input, buttonAt("mousePressed", point, button, index));
3273
3273
  const hold = clickOptions.holdMillis ?? (yield* marks.style.hold);
3274
3274
  if (clickOptions.holdMillis === void 0) yield* marks.present(hold);
3275
3275
  if (hold > 0) yield* Effect.sleep(Duration.millis(hold));
3276
- yield* sendMouse("click", marks.input, {
3277
- type: "mouseReleased",
3278
- ...point,
3279
- button,
3280
- buttons: 0,
3281
- clickCount: index
3282
- });
3276
+ yield* sendMouse("click", marks.input, buttonAt("mouseReleased", point, button, index));
3283
3277
  }
3284
3278
  yield* flush("click", marks.input);
3285
3279
  yield* settle("click", before);
@@ -3289,7 +3283,7 @@ const click = (input) => {
3289
3283
  const hover = (input) => {
3290
3284
  const { settings } = input.page;
3291
3285
  const { perform, preparePolicy, targetFor, moveTo, flush } = input;
3292
- return (target, style = plain) => perform("hover", { target: named(target) }, settings.actionTimeout, preparePolicy("hover", { target: typeof target === "string" ? target : void 0 }, [target]), (marks, approval) => Effect.gen(function* () {
3286
+ return (target, style = plain) => perform("hover", { target: named(target) }, settings.actionTimeout, preparePolicy("hover", { target: refOf(target) }, [target]), (marks, approval) => Effect.gen(function* () {
3293
3287
  const resolved = yield* targetFor("hover", target, approval, marks);
3294
3288
  const { point } = resolved;
3295
3289
  yield* marks.at(point);
@@ -3329,21 +3323,9 @@ const drag = (input) => {
3329
3323
  ...end.point
3330
3324
  }] });
3331
3325
  yield* marks.sent;
3332
- yield* sendMouse("drag", marks.input, {
3333
- type: "mousePressed",
3334
- ...start.point,
3335
- button: "left",
3336
- buttons: 1,
3337
- clickCount: 1
3338
- });
3326
+ yield* sendMouse("drag", marks.input, buttonAt("mousePressed", start.point));
3339
3327
  yield* moveTo("drag", marks, end.point, end.cursor, "drag");
3340
- yield* sendMouse("drag", marks.input, {
3341
- type: "mouseReleased",
3342
- ...end.point,
3343
- button: "left",
3344
- buttons: 0,
3345
- clickCount: 1
3346
- });
3328
+ yield* sendMouse("drag", marks.input, buttonAt("mouseReleased", end.point));
3347
3329
  yield* flush("drag", marks.input);
3348
3330
  }), style);
3349
3331
  };
@@ -3365,7 +3347,7 @@ const typeText = (input) => {
3365
3347
  }, [typeOptions.into ?? null], { submit: typeOptions.submit ?? false }), (marks, approval) => Effect.gen(function* () {
3366
3348
  const replace = typeOptions.replace ?? true;
3367
3349
  const into = typeOptions.into;
3368
- if (into !== void 0 && !/^e\d+$/.test(into)) return yield* failWith("type", new InvalidRequest({ detail: `"${into}" is not a ref; refs look like e12` }));
3350
+ if (into !== void 0 && !/^e\d+$/.test(into)) return yield* refuse("type", `"${into}" is not a ref; refs look like e12`);
3369
3351
  const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
3370
3352
  const typeable = yield* evaluate("type", asked, approval).pipe(Effect.flatMap(decodeWith("type", TypeableResultSchema)));
3371
3353
  if ("error" in typeable) return yield* failWith("type", typeable.error === "stale" && into !== void 0 ? new StaleRef({ ref: into }) : new NotActionable({ detail: typeable.detail }));
@@ -3383,17 +3365,12 @@ const typeText = (input) => {
3383
3365
  ...target.point
3384
3366
  }] });
3385
3367
  yield* marks.sent;
3386
- for (const type of ["mousePressed", "mouseReleased"]) yield* sendMouse("type", marks.input, {
3387
- type,
3388
- ...target.point,
3389
- button: "left",
3390
- buttons: type === "mousePressed" ? 1 : 0,
3391
- clickCount: 1
3392
- });
3368
+ for (const type of ["mousePressed", "mouseReleased"]) yield* sendMouse("type", marks.input, buttonAt(type, target.point));
3393
3369
  yield* flush("type", marks.input);
3394
3370
  }
3395
3371
  yield* marks.sent;
3396
- const focused = yield* mutate("type", scriptCall("focus", into, replace), approval).pipe(Effect.flatMap(decodeWith("type", FocusResultSchema)));
3372
+ const follow = marks.style.shown && approval === void 0 && !secret && target.name !== "" ? target.name : null;
3373
+ const focused = yield* mutate("type", scriptCall("focus", into, replace, follow), approval).pipe(Effect.flatMap(decodeWith("type", FocusResultSchema)));
3397
3374
  if ("error" in focused) return yield* editFailure("type", into, focused);
3398
3375
  secret ||= focused.secret;
3399
3376
  }
@@ -3480,14 +3457,14 @@ const scroll = (input) => {
3480
3457
  const { mark, settle } = input;
3481
3458
  return (scrollOptions = {}, style = plain) => {
3482
3459
  const target = scrollOptions.at;
3483
- const valid = Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0) ? Effect.void : failWith("scroll", new InvalidRequest({ detail: "scroll deltas must be finite" }));
3460
+ const valid = Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0) ? Effect.void : refuse("scroll", "scroll deltas must be finite");
3484
3461
  return perform("scroll", {
3485
3462
  target: target === void 0 ? void 0 : named(target),
3486
3463
  options: {
3487
3464
  dx: scrollOptions.dx,
3488
3465
  dy: scrollOptions.dy
3489
3466
  }
3490
- }, settings.actionTimeout, valid.pipe(Effect.andThen(preparePolicy("scroll", { target: typeof target === "string" ? target : void 0 }, target === void 0 ? [] : [target]))), (marks, approval) => Effect.gen(function* () {
3467
+ }, settings.actionTimeout, valid.pipe(Effect.andThen(preparePolicy("scroll", { target: refOf(target) }, target === void 0 ? [] : [target]))), (marks, approval) => Effect.gen(function* () {
3491
3468
  yield* valid;
3492
3469
  const viewport = yield* viewportFor("scroll");
3493
3470
  const middle = {
@@ -3978,6 +3955,9 @@ const make$8 = (options) => Effect.gen(function* () {
3978
3955
  return current.phase !== "Stopped" && current.failure === void 0 && !isClosed();
3979
3956
  };
3980
3957
  if (current === void 0 || !resumable()) return;
3958
+ pictures.track("picture restoration", options.restore(), failed(current));
3959
+ yield* pictures.wait("picture restoration");
3960
+ yield* healthy(current);
3981
3961
  const viewport = yield* readViewport(options);
3982
3962
  if (!resumable()) return;
3983
3963
  sizes.restart(viewport);
@@ -4416,7 +4396,7 @@ const millisOf = (input) => Option.match(Duration.fromInput(input), {
4416
4396
  });
4417
4397
  const reuse = (page, bridge, viewport, capture) => (operation, options) => Effect.gen(function* () {
4418
4398
  const maxAge = millisOf(options.maxAge ?? defaultMaxAge);
4419
- if (!(maxAge >= 0)) return yield* failWith(operation, new InvalidRequest({ detail: "maxAge must be a duration of zero or more" }));
4399
+ if (!(maxAge >= 0)) return yield* refuse(operation, "maxAge must be a duration of zero or more");
4420
4400
  const latest = yield* capture.latest;
4421
4401
  const known = viewport.knownViewport();
4422
4402
  const { activity } = page;
@@ -4441,6 +4421,11 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4441
4421
  watch: bridge.currentDocument("screencast"),
4442
4422
  frameHistory: settings.frameHistory,
4443
4423
  viewport: Effect.suspend(() => viewport.viewportFor("screencast")),
4424
+ restore: () => page.protocol.send("Page.captureScreenshot", {
4425
+ format: "jpeg",
4426
+ quality: 0,
4427
+ optimizeForSpeed: true
4428
+ }),
4444
4429
  imageSize: jpegSize,
4445
4430
  error
4446
4431
  });
@@ -4492,7 +4477,7 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4492
4477
  const zoom = (requested) => lane.read("zoom")(Effect.gen(function* () {
4493
4478
  const region = yield* Schema.decodeEffect(Region)(requested).pipe(Effect.mapError((error) => undispatched("zoom", new InvalidRequest({ detail: error.message }))));
4494
4479
  const size = viewport.knownViewport() ?? (yield* viewport.viewportFor("zoom"));
4495
- if (region.x + region.width > size.width || region.y + region.height > size.height) return yield* failWith("zoom", new InvalidRequest({ detail: "the crop must fit entirely within the viewport" }));
4480
+ if (region.x + region.width > size.width || region.y + region.height > size.height) return yield* refuse("zoom", "the crop must fit entirely within the viewport");
4496
4481
  const image = yield* picture("zoom", 80, region);
4497
4482
  return new Zoom({
4498
4483
  page: id,
@@ -4503,7 +4488,7 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4503
4488
  const captureStats = (options = {}) => {
4504
4489
  if (options.window === void 0) return capture.stats();
4505
4490
  const millis = millisOf(options.window);
4506
- return millis > 0 && millis <= Duration.toMillis(countsKept) ? capture.stats(millis) : failWith("captureStats", new InvalidRequest({ detail: "window must be over zero and at most a minute" }));
4491
+ return millis > 0 && millis <= Duration.toMillis(countsKept) ? capture.stats(millis) : refuse("captureStats", "window must be over zero and at most a minute");
4507
4492
  };
4508
4493
  return {
4509
4494
  capture,
@@ -4564,7 +4549,7 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4564
4549
  firstRef: yield* Ref.get(nextRef)
4565
4550
  };
4566
4551
  const result = yield* evaluate("snapshot", scriptCall("snapshot", request)).pipe(Effect.flatMap(decodeWith("snapshot", SnapshotResultSchema)));
4567
- if ("invalid" in result) return yield* failWith("snapshot", new InvalidRequest({ detail: result.invalid }));
4552
+ if ("invalid" in result) return yield* refuse("snapshot", result.invalid);
4568
4553
  const { text, truncated, url } = result.snapshot;
4569
4554
  yield* counted(result.nextRef);
4570
4555
  yield* Effect.annotateCurrentSpan({
@@ -4600,7 +4585,7 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4600
4585
  maxChars: options.maxChars ?? 12e3,
4601
4586
  unmask: options.unmask ?? false
4602
4587
  };
4603
- return (scope !== "viewport" && !/^e\d+$/.test(scope) ? failWith("text", new InvalidRequest({ detail: `${scope} is neither "viewport" nor a ref such as e12` })) : texts(JSON.stringify(request), Effect.gen(function* () {
4588
+ return (scope !== "viewport" && !/^e\d+$/.test(scope) ? refuse("text", `${scope} is neither "viewport" nor a ref such as e12`) : texts(JSON.stringify(request), Effect.gen(function* () {
4604
4589
  const result = yield* evaluate("text", scriptCall("text", request)).pipe(Effect.flatMap(decodeWith("text", TextResultSchema)));
4605
4590
  if (result === null) return yield* failWith("text", new StaleRef({ ref: scope }));
4606
4591
  yield* Effect.annotateCurrentSpan({
@@ -4624,7 +4609,7 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4624
4609
  };
4625
4610
  const millis = Math.min(Duration.toMillis(timeout), Number.MAX_SAFE_INTEGER);
4626
4611
  const unmet = () => failWith("waitFor", new Timeout({ millis }));
4627
- const look = evaluate("waitFor", scriptCall("waitUntil", request, millis)).pipe(Effect.flatMap(decodeWith("waitFor", ConditionResultSchema)), Effect.flatMap((result) => "invalid" in result ? failWith("waitFor", new InvalidRequest({ detail: result.invalid })) : result.met ? Effect.void : unmet()));
4612
+ const look = evaluate("waitFor", scriptCall("waitUntil", request, millis)).pipe(Effect.flatMap(decodeWith("waitFor", ConditionResultSchema)), Effect.flatMap((result) => "invalid" in result ? refuse("waitFor", result.invalid) : result.met ? Effect.void : unmet()));
4628
4613
  return lane.read("waitFor")(Effect.void).pipe(Effect.andThen(Effect.retry(look, { while: contextGone })), Effect.timeoutOrElse({
4629
4614
  duration: timeout,
4630
4615
  orElse: unmet
@@ -4652,6 +4637,51 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4652
4637
  };
4653
4638
  });
4654
4639
  //#endregion
4640
+ //#region src/internal/pictures/motion.ts
4641
+ /**
4642
+ * Where a page's screen moves, from its screencast frames: each frame decoded and cut into
4643
+ * 16-pixel blocks, a block moving where a few of its pixels, sampled every other one, changed by
4644
+ * more than JPEG's noise in any color channel.
4645
+ * What moved among the frames before an input is the page's background, such as an idle
4646
+ * animation; foreground stillness ignores it.
4647
+ *
4648
+ * Frames are decoded with the JPEG decoder playwright-core bundles and exports as
4649
+ * `playwright-core/lib/utilsBundle`: effect-browser depends on nothing else, and the runtime has
4650
+ * no image decoder. That entry point has no types, so `utilsBundle.d.ts` declares what is used.
4651
+ */
4652
+ const picture = (frame) => jpegjs.decode(frame.data, { useTArray: true });
4653
+ /** A sample changed where any color channel moved more than this, beyond JPEG's noise. */
4654
+ const noise = 32;
4655
+ /** The blocks where two pictures differ, by `row * columns + column`; -1 for different sizes. */
4656
+ const moved = (from, to) => {
4657
+ if (from.width !== to.width || from.height !== to.height) return [-1];
4658
+ const columns = Math.ceil(from.width / 16);
4659
+ const counts = /* @__PURE__ */ new Map();
4660
+ const differs = (at) => Math.abs((from.data[at] ?? 0) - (to.data[at] ?? 0)) > noise;
4661
+ for (let y = 0; y < from.height; y += 2) for (let x = 0; x < from.width; x += 2) {
4662
+ const at = (y * from.width + x) * 4;
4663
+ const block = Math.floor(y / 16) * columns + Math.floor(x / 16);
4664
+ if (differs(at) || differs(at + 1) || differs(at + 2)) counts.set(block, (counts.get(block) ?? 0) + 1);
4665
+ }
4666
+ return [...counts].flatMap(([block, count]) => count >= 3 ? [block] : []);
4667
+ };
4668
+ /** The blocks that moved from one picture to the next and their neighbours; none past a resize. */
4669
+ const background = (pictures) => {
4670
+ const columns = Math.ceil((pictures[0]?.width ?? 0) / 16);
4671
+ const around = (block) => [
4672
+ -1,
4673
+ 0,
4674
+ 1
4675
+ ].flatMap((row) => [
4676
+ -1,
4677
+ 0,
4678
+ 1
4679
+ ].filter((step) => block >= 0 && block % columns + step >= 0).filter((step) => block % columns + step < columns).map((step) => block + row * columns + step));
4680
+ return new Set(pictures.flatMap((to, index) => moved(pictures[index - 1] ?? to, to).flatMap(around)));
4681
+ };
4682
+ /** Whether anything moved from one picture to another outside the background. */
4683
+ const stirs = (from, to, mask) => moved(from, to).some((block) => !mask.has(block));
4684
+ //#endregion
4655
4685
  //#region src/internal/reading/ready.ts
4656
4686
  /**
4657
4687
  * Waiting until a page is ready to be shown. The page's own evidence leads: one page-script call
@@ -4665,7 +4695,10 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4665
4695
  * its connection, and a frame that arrives first starts the spell again. Where the page's changes
4666
4696
  * are recorded, its evidence includes them: nothing in view may have changed for the spell, which a
4667
4697
  * DOM page's changes show better than its paint, and no stall on the way can hide. Content
4668
- * stillness starts that record itself and ignores capture silence. Each new document starts a
4698
+ * stillness starts that record itself and ignores capture silence. Foreground stillness lets
4699
+ * frames keep coming but compares them, so that only motion outside what was moving before the
4700
+ * page's latest input counts; it still waits out unanswered acknowledgements while no frame
4701
+ * comes, and its last look follows the round trip too. Each new document starts a
4669
4702
  * new observation, while retries of destroyed script contexts share the caller's deadline.
4670
4703
  */
4671
4704
  const make$5 = (page, bridge, capture) => (options = {}) => {
@@ -4676,30 +4709,54 @@ const make$5 = (page, bridge, capture) => (options = {}) => {
4676
4709
  }), Effect.flatMap(decodeWith("ready", ReadinessSchema)), Effect.tap((waiting) => Effect.annotateCurrentSpan({ waiting: waiting.join(",") })), Effect.filterOrElse((waiting) => waiting.length === 0, () => failWith("ready", new Timeout({ millis: Duration.toMillis(timeout) }))));
4677
4710
  const quiet = (millis, until) => Effect.gen(function* () {
4678
4711
  const running = yield* capture.active;
4679
- let { received } = yield* capture.stats();
4712
+ const foreground = options.still === "foreground";
4713
+ let counts = yield* capture.stats();
4680
4714
  let stirred = running ? page.now() : Number.POSITIVE_INFINITY;
4715
+ let reference;
4716
+ const decoded = (frame) => Effect.try({
4717
+ try: () => picture(frame),
4718
+ catch: (error) => undispatched("ready", new Failed({ detail: `no picture: ${String(error)}` }))
4719
+ });
4720
+ const { inputAt } = page.activity;
4721
+ const document = Option.getOrUndefined(yield* capture.latest)?.document;
4722
+ const before = (foreground ? yield* capture.recent : []).filter((frame) => frame.hostTime < inputAt && frame.hostTime >= inputAt - 1500 && frame.document === document);
4723
+ const sampled = before.filter((_, index) => index % Math.ceil(before.length / 8) === 0);
4724
+ const mask = background(yield* Effect.forEach(sampled, decoded));
4681
4725
  const look = Effect.gen(function* () {
4682
4726
  const stats = yield* capture.stats();
4683
- if (!Option.exists(yield* capture.latest, capture.current) || stats.received !== received || stats.ackBacklog > 0) {
4684
- received = stats.received;
4685
- stirred = page.now();
4727
+ const latest = Option.getOrUndefined(yield* capture.latest);
4728
+ const arrived = stats.received !== counts.received;
4729
+ const unseen = stats.accepted - counts.accepted;
4730
+ counts = stats;
4731
+ if (latest === void 0 || !capture.current(latest)) stirred = page.now();
4732
+ else if (!foreground ? arrived || stats.ackBacklog > 0 : !arrived && stats.ackBacklog > 0) stirred = page.now();
4733
+ else if (foreground && (unseen > 0 || reference === void 0)) {
4734
+ const frames = (yield* capture.recent).slice(-Math.max(1, Math.min(8, unseen)));
4735
+ if (unseen > frames.length) stirred = page.now();
4736
+ for (const frame of frames) {
4737
+ const seen = yield* decoded(frame);
4738
+ if (reference === void 0 || stirs(reference, seen, mask)) {
4739
+ reference = seen;
4740
+ stirred = page.now();
4741
+ }
4742
+ }
4686
4743
  }
4687
4744
  return page.now() - stirred;
4688
4745
  });
4689
4746
  const spell = Effect.repeat(look, {
4690
- schedule: Schedule.spaced(Duration.millis(25)),
4747
+ schedule: Schedule.fixed(Duration.millis(foreground ? 100 : 25)),
4691
4748
  until: (still) => still >= millis
4692
4749
  });
4693
4750
  const capturing = running ? Effect.never : capture.stream().pipe(Stream.runDrain, Effect.andThen(Effect.never));
4694
4751
  const still = Effect.gen(function* () {
4695
4752
  for (;;) {
4696
4753
  yield* spell;
4697
- const before = received;
4754
+ const before = counts.received;
4698
4755
  yield* Effect.all([settled(until), capture.sync], {
4699
4756
  concurrency: 2,
4700
4757
  discard: true
4701
4758
  });
4702
- if ((yield* look) >= millis && received === before) return;
4759
+ if ((yield* look) >= millis && (foreground || counts.received === before)) return;
4703
4760
  }
4704
4761
  });
4705
4762
  return yield* Effect.raceFirst(capturing, still);
@@ -4798,7 +4855,7 @@ const make$3 = (page, read, frames) => ({ since, until, unmask }) => Effect.gen(
4798
4855
  since,
4799
4856
  until
4800
4857
  }, page.now());
4801
- if (Option.isNone(asked)) return yield* failWith("window", new InvalidRequest({ detail: "a window's bounds must be finite, its reach not negative" }));
4858
+ if (Option.isNone(asked)) return yield* refuse("window", "a window's bounds must be finite, its reach not negative");
4802
4859
  const bounds = asked.value;
4803
4860
  const changes = yield* Effect.result(read({
4804
4861
  since: isWindow(since) ? since.changes ?? bounds.since : isFrame(since) ? since : bounds.since,
@@ -4887,7 +4944,7 @@ const make$2 = (page, perform, preparePolicy) => {
4887
4944
  "about:",
4888
4945
  "data:",
4889
4946
  "file:"
4890
- ].includes(parsed.protocol)) return failWith("navigate", new InvalidRequest({ detail: `"${redact(url)}" is not a URL` }));
4947
+ ].includes(parsed.protocol)) return refuse("navigate", `"${redact(url)}" is not a URL`);
4891
4948
  return navigation("navigate", () => playwright.goto(parsed.href, {
4892
4949
  waitUntil: "domcontentloaded",
4893
4950
  timeout: 0
@@ -5039,4 +5096,4 @@ const make = Effect.fnUntraced(function* (options) {
5039
5096
  //#endregion
5040
5097
  export { performed as a, pause as i, make as n, viewOf as r, internalsOf as t };
5041
5098
 
5042
- //# sourceMappingURL=page-ClEMFnBG.mjs.map
5099
+ //# sourceMappingURL=page-BKwfF6z-.mjs.map