effect-browser 0.2.0-beta.8 → 0.2.0-beta.9
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 +20 -2
- package/dist/{Browser-n-bKgBNN.d.mts → Browser-BF9_sORP.d.mts} +18 -2
- package/dist/Browser.d.mts +1 -1
- package/dist/Browser.mjs.map +1 -1
- package/dist/{BrowserRuntime-CFe9uUTv.d.mts → BrowserRuntime-B6nT1G4X.d.mts} +2 -2
- package/dist/{BrowserRuntime-HJ1yYlFS.mjs → BrowserRuntime-C1r2aSRK.mjs} +131 -52
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +1 -0
- package/dist/BrowserRuntime.d.mts +1 -1
- package/dist/BrowserRuntime.mjs +2 -2
- package/dist/Capture.d.mts +1 -1
- package/dist/Capture.mjs +2 -2
- package/dist/Chromium.d.mts +1 -1
- package/dist/Chromium.mjs +2 -2
- package/dist/{Images-CaaPNeed.mjs → Images-yCG9BmRU.mjs} +2 -2
- package/dist/{Images-CaaPNeed.mjs.map → Images-yCG9BmRU.mjs.map} +1 -1
- package/dist/PageControl.d.mts +1 -1
- package/dist/PageControl.mjs +1 -1
- package/dist/{Plan-D8qQ48Ic.mjs → Plan-Dyxu2q8C.mjs} +33 -2
- package/dist/{Plan-D8qQ48Ic.mjs.map → Plan-Dyxu2q8C.mjs.map} +1 -1
- package/dist/Plan.d.mts +2 -2
- package/dist/Plan.mjs +2 -2
- package/dist/{PublicSession-Bs61Eal8.mjs → PublicSession-DCSMMqOj.mjs} +5 -4
- package/dist/{PublicSession-Bs61Eal8.mjs.map → PublicSession-DCSMMqOj.mjs.map} +1 -1
- package/dist/Testing.d.mts +2 -2
- package/dist/Testing.mjs +3 -3
- package/package.json +2 -2
- package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -316,6 +316,8 @@ Descriptors compare the full exact kind and label, optional destination and stab
|
|
|
316
316
|
|
|
317
317
|
`Plan.recorded(ran)` projects only completely captured successful intent. Literal fill/type values become deterministic named input slots; existing input names and source conditions remain unchanged. `Plan.inputSlots(plan)` lists every named input a plan reads, with its step, action path and whether it is typed `text` or a replacement `value`, so a host binds `inputs` to replay a recording without the literal ever entering the stored plan. Failed or incomplete whole forms and unknown attempts are not reusable plans. After a later failure, `Plan.recorded(yield* operation.attempts, { through: completedStepId })` can explicitly retain an acknowledged completed prefix. Capture is based on checked native facts, and missing or omitted identity facts produce `Incomplete` rather than executable descriptor intent.
|
|
318
318
|
|
|
319
|
+
`Plan.recordedNavigation(operation, { id })` records a navigation started with `startNavigation`, such as the operation an agent-browser `Navigation` receipt holds, as a plan of one `Navigate` step named `id` (`"navigate"` by default). It joins `completed`, which stops nothing. Only a navigation that reached DOMContentLoaded is recorded; one that failed, was stopped or abandoned, or whose outcome is unknown fails `RecordingIncomplete` with `Unacknowledged`, as an unacknowledged run step does. The step holds `operation.request`, the address asked for, not the `NavigationResult.url` a redirect reached, so a replay repeats the request, as a recorded run's `Navigate` step does.
|
|
320
|
+
|
|
319
321
|
`yield* page.settled({ quiet: "100 millis", within: "2 seconds" })` uses one owned wait in the pinned document. It reports quiet DOM mutation, scroll, root geometry and viewport signals. It does not establish network, descendant animation or business completion. Native timers work on hidden pages, but browser throttling can still cause an honest timeout. Cancellation retains wait capacity until native observer disposal is confirmed. A page-initiated navigation that replaces the observed document ends the wait `Stale` and confirms that disposal, so the next wait can start. Plain plans use the same browser inputs as ordinary operations; presentation and recording composition stay with the host.
|
|
320
322
|
|
|
321
323
|
### Replayable session evidence
|
|
@@ -587,7 +589,11 @@ reports `ScheduleMissed` without inventing an attempt; an insufficient pacing bu
|
|
|
587
589
|
offsets, so a slow reply delays one stroke rather than every later one. Before each stroke's
|
|
588
590
|
first key, the rest of the schedule must still fit the deadline at the round trips the browser
|
|
589
591
|
has actually taken; a stroke that could not finish is refused whole, and strokes already
|
|
590
|
-
acknowledged stay `performed`.
|
|
592
|
+
acknowledged stay `performed`. A performed click is held to the same rule. Playwright's click
|
|
593
|
+
checks the node again before its first input event, and a deadline inside those checks would
|
|
594
|
+
report `unknown` for input never sent, so a click the deadline cannot fit at the press's
|
|
595
|
+
measured round trip is refused `TimingBudgetExceeded`, undispatched, and its Page stays open.
|
|
596
|
+
Receipts retain requested/intended/actual start, deadline
|
|
591
597
|
and lateness. Cancellation stops future submissions and keeps readable attempt history with the
|
|
592
598
|
original Effect Cause. Known preparatory work followed by refusal is rejected with its subphase
|
|
593
599
|
receipts; pending input remains unknown, and acknowledged logical input stays performed when a
|
|
@@ -664,6 +670,7 @@ const { url: loaded } = yield * operation.completed;
|
|
|
664
670
|
|
|
665
671
|
The owner's permit is released as soon as the navigation is dispatched. While it loads, reads, checkpoints, holds and every other page proceed, and anything that would change _this_ page fails `busy` and `undispatched`. `navigate` runs on the same machinery, so there is one navigator.
|
|
666
672
|
|
|
673
|
+
- `request` is what was asked for, checked before dispatch. `Plan.recordedNavigation` records it once `completed` succeeds.
|
|
667
674
|
- `completed` belongs to that one navigation: a successor reaching the same URL fails it instead of completing it, and its URL is the page that navigated, not whichever page is selected by then. **Interrupting a waiter stops nothing.** The browser keeps loading and nothing is dispatched again.
|
|
668
675
|
- `stop` asks the browser to stop loading. Its acknowledgement is a known outcome: `completed` then fails `interrupted`, the page holds whatever had loaded, and the session stays usable. It does not undo anything the page already did. Concurrent callers share their active stop attempt. A busy refusal or cancellation before dispatch allows a later request only after native setup and its port retire; pending or unconfirmed retirement keeps setup capacity occupied. Once a stop is dispatched, every later caller receives that attempt's recorded result, including failure or interruption, without sending it again. A completed navigation cannot stop its successor.
|
|
669
676
|
- A main-frame loading timeout asks the same stop coordinator to retire that exact navigation. Acknowledged stop completes it with `Timeout/unknown` and leaves the owner open: partial content remains inspectable and deliberate subsequent input is allowed. An explicit stop racing recovery shares its attempt; a completed predecessor cannot stop a successor. The `unknown` outcome still says nothing about page effects before cancellation.
|
|
@@ -898,6 +905,17 @@ receives the original containment policy. A later failure never weakens what was
|
|
|
898
905
|
|
|
899
906
|
A character the US layout cannot produce is committed as text, the way an input method commits it: the field changes and no key event says so. Plain `type` sends a shifted character as its own key with `shiftKey` false. When a page reads the modifier, send that stroke through `press` with `Shift` held, spelling the key as the page will see it: `{ key: "A", modifiers: ["Shift"] }`. Spelled `"a"`, the engine sends `a` with Shift down, which is what Shift produces with Caps Lock on. Control characters are refused in text because the engine presses Enter for a line break; a named key is always its own `press`. Performed plans hold Shift for uppercase and shifted punctuation and pace complete balanced strokes under one logical action. A stroke remains unresolved until its key and modifiers are released; quiet intervals do not renew the absolute deadline or retain unresolved native replies.
|
|
900
907
|
|
|
908
|
+
Like a plain window, a performed stroke submits its modifiers, its key and their releases in order
|
|
909
|
+
without waiting for each reply, so its key-up is submitted its planned hold after its key-down
|
|
910
|
+
rather than a round trip later, and a held Shift adds no round trip of its own. Delivery jitter
|
|
911
|
+
on the way to a remote browser is not controlled. The owner is still checked before every
|
|
912
|
+
command, nothing more is sent once a reply has failed, and the stroke drains all of its at most ten
|
|
913
|
+
replies before it resolves. Each stroke therefore still costs its releases' round trip, and its
|
|
914
|
+
focus check's when it names an element: against a remote browser, a stroke whose planned interval
|
|
915
|
+
is shorter than those starts late. A slow renderer still handles a stroke's events one at a time,
|
|
916
|
+
so the deadline check charges each stroke what strokes with as many commands have taken, scaled up
|
|
917
|
+
from smaller strokes until one of its size has drained.
|
|
918
|
+
|
|
901
919
|
A press waits for native input acknowledgement, without waiting for resulting navigation. If Enter submits a form, wait for what the next document shows with `waitFor`. A receipt carries the same target, pointer position and interval as any other input, and never says which key was pressed or what was typed. Typing a secret is still more observable than one `fill`, because the page sees every stroke; prefer `fill` for one unless the page requires keys. Both operations are available in the model-facing toolkit in `effect-agent-browser`.
|
|
902
920
|
|
|
903
921
|
The connection endpoint is read through the exact allocated session, so a provider reply that names a different session is refused before any CDP attachment.
|
|
@@ -910,7 +928,7 @@ and reads do not hold it up; the pause refuses them as `Stale`. Work that cannot
|
|
|
910
928
|
such as a navigation still loading, fails the request with `Timeout/undispatched`: no handoff is
|
|
911
929
|
granted, the barrier lifts and automation continues. Resume requires an explicit operator-release signal
|
|
912
930
|
and returns fresh bounded Page inventory under the lifecycle permit; observing a newly acquired
|
|
913
|
-
Page is explicit. Once the pause is installed, a failed Live View authorization does not silently resume automation. Live View URLs are temporary bearer material; iframe styling is not an authorization boundary. Live View is also where browser-window presentation already exists for watching a session as it runs, and it is the provider's: beside each full-screen URL Browserbase issues a bordered one (`debuggerUrl`, "mimic a real browser with borders"), and a navbar that `navbar=false` hides. This package decodes and returns only the full-screen URL. The bordered one carries the same control authority and would be issued under the same rules, so surfacing it is a small host-only addition whenever something needs it; nothing here does yet, so it is not exported.
|
|
931
|
+
Page is explicit. Every Page and Frame issued before the handoff, `initialPage` included, stays retired after resume, though the connection is the same: its operations fail `Stale` and `undispatched`, so issue Pages again from `inventory.pages` with `session.page(info)`. Once the pause is installed, a failed Live View authorization does not silently resume automation. Live View URLs are temporary bearer material; iframe styling is not an authorization boundary. Live View is also where browser-window presentation already exists for watching a session as it runs, and it is the provider's: beside each full-screen URL Browserbase issues a bordered one (`debuggerUrl`, "mimic a real browser with borders"), and a navbar that `navbar=false` hides. This package decodes and returns only the full-screen URL. The bordered one carries the same control authority and would be issued under the same rules, so surfacing it is a small host-only addition whenever something needs it; nothing here does yet, so it is not exported.
|
|
914
932
|
|
|
915
933
|
## Registrations, capabilities and document readiness
|
|
916
934
|
|
|
@@ -88,6 +88,17 @@ declare const inputSlots: (plan: Plan$1 | LivePlan) => ReadonlyArray<InputSlot>;
|
|
|
88
88
|
declare const recorded: (value: Ran | AttemptSnapshot, options?: {
|
|
89
89
|
readonly through?: string;
|
|
90
90
|
}) => Effect.Effect<Plan$1, RecordingIncomplete>;
|
|
91
|
+
/**
|
|
92
|
+
* Project one `startNavigation` operation, such as an agent-browser `Navigation` receipt's, as a
|
|
93
|
+
* plan of one `Navigate` step named `id` ("navigate" by default). It joins `completed`, which
|
|
94
|
+
* stops nothing: only a navigation that reached DOMContentLoaded is acknowledged, and one that
|
|
95
|
+
* failed, was stopped or interrupted, or whose outcome is unknown is refused `Unacknowledged`.
|
|
96
|
+
* The step repeats the request, not the address a redirect reached, as a recorded run's
|
|
97
|
+
* Navigate step does. An `id` that is not a step identifier fails `IncompleteCapture`.
|
|
98
|
+
*/
|
|
99
|
+
declare const recordedNavigation: (operation: NavigationOperation, options?: {
|
|
100
|
+
readonly id?: string;
|
|
101
|
+
}) => Effect.Effect<Plan$1, RecordingIncomplete>;
|
|
91
102
|
//#endregion
|
|
92
103
|
//#region src/Browser.d.ts
|
|
93
104
|
/**
|
|
@@ -277,6 +288,11 @@ interface ElementAdmission {
|
|
|
277
288
|
interface NavigationOperation {
|
|
278
289
|
/** What was navigated, read before dispatch. */
|
|
279
290
|
readonly target: Target;
|
|
291
|
+
/**
|
|
292
|
+
* What was asked for, as checked before dispatch. `Plan.recordedNavigation` records this
|
|
293
|
+
* request, not the address `completed` reports after any redirect.
|
|
294
|
+
*/
|
|
295
|
+
readonly request: StartNavigationRequest;
|
|
280
296
|
/**
|
|
281
297
|
* The document reached DOMContentLoaded. It belongs to this one navigation: a successor that
|
|
282
298
|
* reaches the same URL fails it instead. Interrupting a waiter stops nothing in the browser.
|
|
@@ -367,6 +383,6 @@ declare const scoped: {
|
|
|
367
383
|
<S, E, AE, AR, A, E2, R2>(open: Effect.Effect<S & BrowserSession<E>, AE, AR>, f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>): Effect.Effect<A, AE | E | E2 | InitializationError | BrowserError, Exclude<AR | R2, Scope.Scope>>;
|
|
368
384
|
};
|
|
369
385
|
//#endregion
|
|
370
|
-
export { StepFailed as C, inputSlots as D, encode as E, make as O, RunOptions as S, decode as T, scoped as _, ElementAdmission as a, RecordingIncomplete as b, OpenOptions as c, PageOperations as d, PageStatus as f, checkPage as g, SettledRequest as h, BrowserSession as i, recorded as k, OperationOptions as l, SessionAdmissionStatus as m, AdmissionStatus as n, Frame as o, ResolveOptions as p, AnySession as r, NavigationOperation as s, AdmissionOptions as t, Page as u, InputSlot as v, checkRunOptions as w, RunOperation as x, PlanOperations as y };
|
|
386
|
+
export { recordedNavigation as A, StepFailed as C, inputSlots as D, encode as E, make as O, RunOptions as S, decode as T, scoped as _, ElementAdmission as a, RecordingIncomplete as b, OpenOptions as c, PageOperations as d, PageStatus as f, checkPage as g, SettledRequest as h, BrowserSession as i, recorded as k, OperationOptions as l, SessionAdmissionStatus as m, AdmissionStatus as n, Frame as o, ResolveOptions as p, AnySession as r, NavigationOperation as s, AdmissionOptions as t, Page as u, InputSlot as v, checkRunOptions as w, RunOperation as x, PlanOperations as y };
|
|
371
387
|
export {};
|
|
372
|
-
//# sourceMappingURL=Browser-
|
|
388
|
+
//# sourceMappingURL=Browser-BF9_sORP.d.mts.map
|
package/dist/Browser.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as scoped, a as ElementAdmission, c as OpenOptions, d as PageOperations, f as PageStatus, g as checkPage, h as SettledRequest, i as BrowserSession, l as OperationOptions, m as SessionAdmissionStatus, n as AdmissionStatus, o as Frame, p as ResolveOptions, r as AnySession, s as NavigationOperation, t as AdmissionOptions, u as Page } from "./Browser-
|
|
1
|
+
import { _ as scoped, a as ElementAdmission, c as OpenOptions, d as PageOperations, f as PageStatus, g as checkPage, h as SettledRequest, i as BrowserSession, l as OperationOptions, m as SessionAdmissionStatus, n as AdmissionStatus, o as Frame, p as ResolveOptions, r as AnySession, s as NavigationOperation, t as AdmissionOptions, u as Page } from "./Browser-BF9_sORP.mjs";
|
|
2
2
|
export { AdmissionOptions, AdmissionStatus, AnySession, BrowserSession, ElementAdmission, Frame, NavigationOperation, OpenOptions, OperationOptions, Page, PageOperations, PageStatus, ResolveOptions, SessionAdmissionStatus, SettledRequest, checkPage, scoped };
|
|
3
3
|
export {};
|
package/dist/Browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Browser.mjs","names":[],"sources":["../src/Browser.ts"],"sourcesContent":["import { type Duration, Effect, type Scope, type Stream } from \"effect\";\nimport { dual } from \"effect/Function\";\n\nimport type * as Bootstrap from \"./Bootstrap.ts\";\nimport type {\n BrowserDiagnostics,\n Checkpoint,\n CheckpointOptions,\n FillFormOptions,\n FillFormRequest,\n FillFormResult,\n FillRequest,\n HoverRequest,\n InputReceipt,\n KeyStroke,\n NavigateRequest,\n NavigationResult,\n ObservationOptions,\n ObservedElement,\n PointerMoveRequest,\n PressRequest,\n ReadTextRequest,\n ScreenshotRequest,\n ScreenshotResult,\n ScrollRequest,\n SelectOptions,\n SessionStatus,\n StartNavigationRequest,\n TextResult,\n TypeRequest,\n WaitForElementRequest,\n WheelRequest,\n ControlFacts,\n ActionResult,\n ClickRequest,\n FrameInfo,\n Observation,\n PageInfo,\n Target,\n Viewport,\n} from \"./BrowserData.ts\";\nimport type { BrowserError, BrowserOperation, Containment, InitializationError } from \"./Errors.ts\";\nimport { resolveTargetControlsForSession } from \"./internal/browser/PageRegistry.ts\";\nimport type { PlanOperations } from \"./Plan.ts\";\nimport type { DescriptorEncoded, ResolveGuard, SettledEvidence } from \"./PlanData.ts\";\nimport type { Timeline } from \"./Timeline.ts\";\nimport type { PageEvent, TimelineError } from \"./TimelineData.ts\";\n\n/**\n * Host-only ordinary admission. Omission or zero fails immediately; positive finite duration\n * queues FIFO. Page closure uses reserved fail-fast admission and a deadline-bounded shared close.\n */\nexport interface AdmissionOptions {\n readonly queue?: Duration.Input;\n}\n\n/** Explicit host options, separate from request data and exact-element admission callbacks. */\nexport interface OperationOptions {\n readonly timeoutMillis?: number;\n readonly admission?: AdmissionOptions;\n}\n\n/** Explicit descriptor guard and ordinary host admission; no selector or geometry fallback. */\nexport interface ResolveOptions extends OperationOptions {\n readonly guard?: typeof ResolveGuard.Encoded;\n}\n\n/** Named native quiet signals within the original positive finite deadline. */\nexport interface SettledRequest {\n readonly quiet: Duration.Input;\n readonly within: Duration.Input;\n}\n\n/** A bounded host-memory snapshot; it grants no priority or native authority. */\nexport interface AdmissionStatus {\n readonly active: BrowserOperation | null;\n readonly waiting: number;\n readonly maximum: number;\n readonly oldestWaitMillis: number | null;\n readonly nativePending: number;\n readonly nativeWaitPending: boolean;\n readonly stopSetupPending: boolean;\n}\n\nexport interface SessionAdmissionStatus {\n readonly waiting: number;\n readonly maximum: number;\n readonly nativePending: number;\n readonly nativeMaximum: number;\n readonly nativeWaits: number;\n readonly stopSetups: number;\n readonly registry: AdmissionStatus;\n}\n\n/** Terminal facts remain readable after this page loses live authority. */\nexport interface PageStatus {\n readonly identity: Target;\n readonly phase: \"open\" | \"paused\" | \"closing\" | \"closed\" | \"stale\";\n readonly containment: Containment;\n readonly admission: AdmissionStatus;\n}\n\nexport interface PageOperations {\n readonly navigate: (\n request: NavigateRequest,\n options?: OperationOptions,\n ) => Effect.Effect<NavigationResult, BrowserError>;\n /** `navigate`, left in flight: the same single dispatch, completed by the caller. */\n readonly startNavigation: (\n request: StartNavigationRequest,\n options?: OperationOptions,\n ) => Effect.Effect<NavigationOperation, BrowserError, Scope.Scope>;\n readonly readText: (\n request: ReadTextRequest,\n options?: OperationOptions,\n ) => Effect.Effect<TextResult, BrowserError>;\n readonly click: (\n request: ClickRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fill: (\n request: FillRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** Script in the page: instantaneous, and it raises no wheel event. */\n readonly scroll: (\n request: ScrollRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One real pointer move, in main-frame viewport pixels. */\n readonly pointerMove: (\n request: PointerMoveRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Places the pointer on one exact element where it is, or fails `not-visible` unsent. */\n readonly hover: (\n request: HoverRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real wheel event; the browser decides what under the pointer scrolls. */\n readonly wheel: (\n request: WheelRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real key stroke to whatever has focus, or fails `not-focused` unsent if `into` lacks it. */\n readonly press: (\n request: PressRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Text as the real key strokes that produce it, under the same focus rule as `press`. */\n readonly type: (\n request: TypeRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n readonly screenshot: (\n request: ScreenshotRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ScreenshotResult, BrowserError>;\n /**\n * The one observation whose nodes later actions may name. `scope: \"viewport\"` keeps only text\n * and controls that are on screen and reachable, plus bounded choices of visible native\n * selects; the default reads the whole document. It carries no destination, form or field value.\n */\n readonly observe: (\n options?: ObservationOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<Observation, BrowserError>;\n /**\n * Passive evidence for a recorder, with a picture when asked. It issues no references and\n * leaves the observation above exactly as it was. Host-only: it carries control facts.\n */\n readonly checkpoint: (\n options?: CheckpointOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<Checkpoint, BrowserError>;\n /** Host-only facts about one observed control, read from that exact node just now. */\n readonly controlFacts: (\n reference: ObservedElement,\n options?: OperationOptions,\n ) => Effect.Effect<ControlFacts, BrowserError>;\n /**\n * After a page hold, nothing observed on that page may be acted on unchecked. This checks one\n * reference: still attached, and still the control that was inspected. It never searches by\n * selector or label for a substitute, and it sends nothing.\n */\n readonly revalidateElement: (\n reference: ObservedElement,\n options?: OperationOptions,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n readonly clickElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fillElement: (\n reference: ObservedElement,\n value: string,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One selection using this exact select and its issued option element IDs, with fresh checks. */\n readonly selectOption: (\n reference: ObservedElement,\n options: SelectOptions,\n admission?: ElementAdmission,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly hoverElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** A key stroke sent only if the exact node an observation named already has focus. */\n readonly pressElement: (\n reference: ObservedElement,\n stroke: KeyStroke,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Real typing with `fillElement`'s exactness: that node must already have focus. */\n readonly typeElement: (\n reference: ObservedElement,\n text: string,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /**\n * Set several controls of one observation in order, then optionally click one submit control.\n * Every step acts on the exact observed node after the same fresh checks as `fillElement`,\n * except that a control may have become enabled since it was observed, and each is admitted\n * and charged as its own action. The observation stays usable for this form's own steps only;\n * anything else that changes the page still retires it, and the form retires it when it ends.\n * The first refused or uncertain step ends the form. Submit is sent only after every field\n * succeeded and, unless `verify` is false, still holds what its step left there. Fields set\n * before a stop stay set. It fails only when its first step does; otherwise `stopped` says\n * where it ended and why.\n */\n readonly fillForm: (\n request: FillFormRequest,\n admission?: ElementAdmission,\n options?: FillFormOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<FillFormResult, BrowserError>;\n /**\n * Wait on the original observed node within the host deadline. Hidden includes its detachment;\n * document replacement is stale. Success observes a condition, without authorizing later input.\n */\n readonly waitForElement: (\n request: WaitForElementRequest,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n readonly waitFor: (\n request: {\n readonly selector: string;\n readonly state: \"visible\" | \"hidden\" | \"attached\" | \"detached\";\n },\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /** Navigation observation is registered before the single click dispatch. */\n readonly clickAndWait: (\n request: ClickRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /**\n * Readiness of the current document only. Dependent operations wait for it themselves;\n * this reports it without charging an action, so a caller can decide what to do about a\n * document that predates the registrations.\n */\n readonly ready: (\n options?: OperationOptions,\n ) => Effect.Effect<Bootstrap.ReadinessOutcome, InitializationError>;\n}\n\n/**\n * Issued live authority for one native frame; navigation keeps the frame and retires its references.\n *\n * Pass the issued object itself. Capture, page control, provider transfers, agent tools and\n * `checkPage` authenticate it by identity, so a spread copy, wrapper or Proxy is refused\n * `UnregisteredSession` even though its copied methods still run. Observe work through the\n * timeline and run receipts rather than by wrapping methods.\n */\nexport interface Frame extends PageOperations, PlanOperations {\n readonly identity: Target;\n readonly status: Effect.Effect<PageStatus>;\n /**\n * The original owner's host monotonic clock, the same one `session.monotonicTimeNanos` reads,\n * so a host bound to this Page or Frame can compute a plan's `startAt` without the session.\n */\n readonly monotonicTimeNanos: Effect.Effect<bigint>;\n /** Resolve full descriptor intent against the complete current native candidate inventory. */\n readonly resolve: (\n descriptor: DescriptorEncoded,\n options?: ResolveOptions,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n /**\n * Quiet DOM mutation, scroll, root geometry and viewport signals in one pinned document.\n * This does not establish network, animation or business completion.\n */\n readonly settled: (\n request: SettledRequest,\n options?: OperationOptions,\n ) => Effect.Effect<SettledEvidence, BrowserError>;\n}\n\n/** Issued live authority for one native page on its original connection; pass it as issued, see `Frame`. */\nexport interface Page extends Frame {\n /** Evidence for this issued Page's original domain; navigation does not change its journal. */\n readonly timeline: Timeline;\n /**\n * Read this page's address and title. Like `listFrames`, it is a read of the page and shares\n * its permit; pass `admission.queue` to wait behind work already running there.\n */\n readonly describe: (options?: OperationOptions) => Effect.Effect<PageInfo, BrowserError>;\n readonly listFrames: (\n options?: OperationOptions,\n ) => Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** Issue a child Frame. Like `session.page`, issuing takes the registry permit, not this page's. */\n readonly frame: (\n info: FrameInfo,\n options?: OperationOptions,\n ) => Effect.Effect<Frame, BrowserError>;\n readonly resizeViewport: (\n viewport: Viewport,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /** Reserved fail-fast admission; timeoutMillis bounds joining a close, not admission.queue. */\n readonly close: (options?: OperationOptions) => Effect.Effect<void, BrowserError>;\n}\n\n/**\n * A host's decision about one control, made on facts read from the exact node immediately\n * before input. Returning anything but `true`, or throwing, sends nothing and fails `denied`.\n * It is a plain synchronous function on purpose: it runs while the owner's permit is held, where\n * waiting on a model or a network call would stall other ordinary operations on that Page. It is not an atomic\n * check-and-input transaction, because page script can still run before the native input lands.\n */\nexport interface ElementAdmission {\n readonly admit: (facts: ControlFacts) => boolean;\n}\n\n/**\n * A navigation the browser is still performing. The owner's permit was released when it was\n * dispatched, so while it loads a host may read, `checkpoint`, hold and resume this page, and\n * use any other page. Anything that would change this page fails `busy` until it settles.\n *\n * Leaving its scope unsettled revokes and attempts to close its exact page. Positive closure\n * preserves healthy peers; unconfirmed closure fences the session. Its outcome remains unknown\n * because nothing then knows what the browser did. It is never replayed.\n */\nexport interface NavigationOperation {\n /** What was navigated, read before dispatch. */\n readonly target: Target;\n /**\n * The document reached DOMContentLoaded. It belongs to this one navigation: a successor that\n * reaches the same URL fails it instead. Interrupting a waiter stops nothing in the browser.\n */\n readonly completed: Effect.Effect<NavigationResult, BrowserError>;\n /**\n * Asks the browser to stop loading and waits for this navigation to settle, after which\n * `completed` fails `interrupted`. Success is a known outcome and the session stays usable:\n * the page holds whatever had loaded. It does not undo anything the page already did.\n * An already-completed navigation cannot stop a successor. Concurrent callers share their\n * active attempt. Before dispatch, cancellation or a busy refusal permits a later request\n * once native setup has retired. A dispatched attempt's outcome, including failure or\n * interruption, is retained permanently; an uncertain stop is never replayed.\n */\n readonly stop: Effect.Effect<void, BrowserError>;\n}\n\n/**\n * Host control over one owned browser. This is not a serializable model value: copying a\n * session object cannot copy its capture, page-control or connection authority.\n *\n * Browser operations belong to issued Page and Frame objects. Display selection and owner\n * lifecycle are separate from those exact capabilities.\n */\nexport interface BrowserSession<E = never> {\n /** The original owner's host clock, for same-runtime absolute cue scheduling. */\n readonly monotonicTimeNanos: Effect.Effect<bigint>;\n /** Stable facade: each snapshot/stream captures the current journal when it executes. */\n readonly timeline: Timeline;\n /** Atomic cached Inventory baseline followed by lifecycle evidence on its pinned journal. */\n readonly pages: Stream.Stream<PageEvent, TimelineError>;\n /** The page acquired on the initial connection, independent of later display selection. */\n readonly initialPage: Page;\n /**\n * Authenticate `info` against the live native page and return its canonical issued Page. This\n * takes the registry permit, not the page's own: it never waits for, or fails `busy` on, work\n * already running on that page.\n */\n readonly page: (info: PageInfo, options?: OperationOptions) => Effect.Effect<Page, BrowserError>;\n readonly listPages: (\n options?: OperationOptions,\n ) => Effect.Effect<ReadonlyArray<PageInfo>, BrowserError>;\n /** The implementation which owns this live connection. */\n readonly implementation: string;\n /** Copied host-only state, readable without admission in every lifecycle phase. */\n readonly status: Effect.Effect<SessionStatus>;\n /** Bounded native/policy diagnostics. Typed callback causes remain in bindingDiagnostics. */\n readonly diagnostics: Effect.Effect<BrowserDiagnostics>;\n readonly admission: Effect.Effect<SessionAdmissionStatus>;\n /** Close this scope and require its own ownership-specific cleanup evidence. */\n readonly closeChecked: Effect.Effect<void, BrowserError>;\n /** First fail-session callback cause, preserving the consumer's error type on the host. */\n readonly failure: Effect.Effect<never, E | InitializationError>;\n /** Bounded host-only evidence; consumer causes are never projected into a page reply. */\n readonly bindingDiagnostics: Effect.Effect<Bootstrap.BindingDiagnostics<E>>;\n /**\n * Choose the displayed page, which is also the page `detach` records and `reconnect` resumes on.\n * Nothing else: it never retargets a Page or Frame operation, which keep their own exact\n * authority. Pass a Page this session issued; a closed or stale one is refused unsent.\n */\n readonly selectPage: (\n page: Page,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /**\n * Open a page in its own window, sized like the others, without selecting it, and return the\n * Page issued for it. Chromium paints every window, so any page can be pictured and read at\n * speed, not only the one in front. Registry admission fails immediately by default; pass a\n * positive finite admission.queue to wait FIFO within the operation deadline.\n */\n readonly createPage: (options?: OperationOptions) => Effect.Effect<Page, BrowserError>;\n}\n\n/** Helpers that do not supervise callback failures accept any live browser session. */\nexport type AnySession = BrowserSession<unknown>;\n\n/**\n * Check that `page` is a Page or Frame this session issued, by identity, and that its authority\n * is still current: a foreign or copied object fails `UnregisteredSession`, a closed, stale or\n * detached one fails with the same refusal its own operations would report. Adapters such as\n * agent-browser hosts call it once when they bind, so a wrong target fails before any work.\n */\nexport const checkPage = (\n session: AnySession,\n page: Page | Frame,\n): Effect.Effect<void, BrowserError> =>\n resolveTargetControlsForSession(session, page).pipe(Effect.asVoid);\n\n/** Dependencies are captured at acquisition, not erased into an environment-free service Layer. */\nexport interface OpenOptions<E = never, R = never> {\n readonly bootstrap?: Bootstrap.Plan<E, R>;\n}\n\n/**\n * Acquire, supervise and close one browser in a fresh scope. The callback keeps the concrete\n * Chromium or provider session type. Its scoped work is joined before checked browser cleanup,\n * and cleanup failures remain typed even when the callback has already failed or was cancelled.\n * Acquisition is evaluated once; an uncertain result is never retried.\n */\nexport const scoped: {\n <S extends BrowserSession<unknown>, A, E2, R2>(\n f: (session: S) => Effect.Effect<A, E2, R2>,\n ): <E, AE, AR>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n ) => Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ): Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n} = dual(\n 2,\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ) =>\n Effect.scoped(\n Effect.flatMap(open, (session) =>\n Effect.scoped(\n Effect.raceFirst(\n session.failure,\n Effect.suspend(() => f(session)),\n ),\n ).pipe(Effect.onExit(() => session.closeChecked)),\n ),\n ),\n);\n"],"mappings":";;;;;;;;;;AAkbA,MAAa,aACX,SACA,SAEA,gCAAgC,SAAS,IAAI,CAAC,CAAC,KAAK,OAAO,MAAM;;;;;;;AAanE,MAAa,SAkBT,KACF,IAEE,MACA,MAEA,OAAO,OACL,OAAO,QAAQ,OAAO,YACpB,OAAO,OACL,OAAO,UACL,QAAQ,SACR,OAAO,cAAc,EAAE,OAAO,CAAC,CACjC,CACF,CAAC,CAAC,KAAK,OAAO,aAAa,QAAQ,YAAY,CAAC,CAClD,CACF,CACJ"}
|
|
1
|
+
{"version":3,"file":"Browser.mjs","names":[],"sources":["../src/Browser.ts"],"sourcesContent":["import { type Duration, Effect, type Scope, type Stream } from \"effect\";\nimport { dual } from \"effect/Function\";\n\nimport type * as Bootstrap from \"./Bootstrap.ts\";\nimport type {\n BrowserDiagnostics,\n Checkpoint,\n CheckpointOptions,\n FillFormOptions,\n FillFormRequest,\n FillFormResult,\n FillRequest,\n HoverRequest,\n InputReceipt,\n KeyStroke,\n NavigateRequest,\n NavigationResult,\n ObservationOptions,\n ObservedElement,\n PointerMoveRequest,\n PressRequest,\n ReadTextRequest,\n ScreenshotRequest,\n ScreenshotResult,\n ScrollRequest,\n SelectOptions,\n SessionStatus,\n StartNavigationRequest,\n TextResult,\n TypeRequest,\n WaitForElementRequest,\n WheelRequest,\n ControlFacts,\n ActionResult,\n ClickRequest,\n FrameInfo,\n Observation,\n PageInfo,\n Target,\n Viewport,\n} from \"./BrowserData.ts\";\nimport type { BrowserError, BrowserOperation, Containment, InitializationError } from \"./Errors.ts\";\nimport { resolveTargetControlsForSession } from \"./internal/browser/PageRegistry.ts\";\nimport type { PlanOperations } from \"./Plan.ts\";\nimport type { DescriptorEncoded, ResolveGuard, SettledEvidence } from \"./PlanData.ts\";\nimport type { Timeline } from \"./Timeline.ts\";\nimport type { PageEvent, TimelineError } from \"./TimelineData.ts\";\n\n/**\n * Host-only ordinary admission. Omission or zero fails immediately; positive finite duration\n * queues FIFO. Page closure uses reserved fail-fast admission and a deadline-bounded shared close.\n */\nexport interface AdmissionOptions {\n readonly queue?: Duration.Input;\n}\n\n/** Explicit host options, separate from request data and exact-element admission callbacks. */\nexport interface OperationOptions {\n readonly timeoutMillis?: number;\n readonly admission?: AdmissionOptions;\n}\n\n/** Explicit descriptor guard and ordinary host admission; no selector or geometry fallback. */\nexport interface ResolveOptions extends OperationOptions {\n readonly guard?: typeof ResolveGuard.Encoded;\n}\n\n/** Named native quiet signals within the original positive finite deadline. */\nexport interface SettledRequest {\n readonly quiet: Duration.Input;\n readonly within: Duration.Input;\n}\n\n/** A bounded host-memory snapshot; it grants no priority or native authority. */\nexport interface AdmissionStatus {\n readonly active: BrowserOperation | null;\n readonly waiting: number;\n readonly maximum: number;\n readonly oldestWaitMillis: number | null;\n readonly nativePending: number;\n readonly nativeWaitPending: boolean;\n readonly stopSetupPending: boolean;\n}\n\nexport interface SessionAdmissionStatus {\n readonly waiting: number;\n readonly maximum: number;\n readonly nativePending: number;\n readonly nativeMaximum: number;\n readonly nativeWaits: number;\n readonly stopSetups: number;\n readonly registry: AdmissionStatus;\n}\n\n/** Terminal facts remain readable after this page loses live authority. */\nexport interface PageStatus {\n readonly identity: Target;\n readonly phase: \"open\" | \"paused\" | \"closing\" | \"closed\" | \"stale\";\n readonly containment: Containment;\n readonly admission: AdmissionStatus;\n}\n\nexport interface PageOperations {\n readonly navigate: (\n request: NavigateRequest,\n options?: OperationOptions,\n ) => Effect.Effect<NavigationResult, BrowserError>;\n /** `navigate`, left in flight: the same single dispatch, completed by the caller. */\n readonly startNavigation: (\n request: StartNavigationRequest,\n options?: OperationOptions,\n ) => Effect.Effect<NavigationOperation, BrowserError, Scope.Scope>;\n readonly readText: (\n request: ReadTextRequest,\n options?: OperationOptions,\n ) => Effect.Effect<TextResult, BrowserError>;\n readonly click: (\n request: ClickRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fill: (\n request: FillRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** Script in the page: instantaneous, and it raises no wheel event. */\n readonly scroll: (\n request: ScrollRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One real pointer move, in main-frame viewport pixels. */\n readonly pointerMove: (\n request: PointerMoveRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Places the pointer on one exact element where it is, or fails `not-visible` unsent. */\n readonly hover: (\n request: HoverRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real wheel event; the browser decides what under the pointer scrolls. */\n readonly wheel: (\n request: WheelRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** One real key stroke to whatever has focus, or fails `not-focused` unsent if `into` lacks it. */\n readonly press: (\n request: PressRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Text as the real key strokes that produce it, under the same focus rule as `press`. */\n readonly type: (\n request: TypeRequest,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n readonly screenshot: (\n request: ScreenshotRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ScreenshotResult, BrowserError>;\n /**\n * The one observation whose nodes later actions may name. `scope: \"viewport\"` keeps only text\n * and controls that are on screen and reachable, plus bounded choices of visible native\n * selects; the default reads the whole document. It carries no destination, form or field value.\n */\n readonly observe: (\n options?: ObservationOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<Observation, BrowserError>;\n /**\n * Passive evidence for a recorder, with a picture when asked. It issues no references and\n * leaves the observation above exactly as it was. Host-only: it carries control facts.\n */\n readonly checkpoint: (\n options?: CheckpointOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<Checkpoint, BrowserError>;\n /** Host-only facts about one observed control, read from that exact node just now. */\n readonly controlFacts: (\n reference: ObservedElement,\n options?: OperationOptions,\n ) => Effect.Effect<ControlFacts, BrowserError>;\n /**\n * After a page hold, nothing observed on that page may be acted on unchecked. This checks one\n * reference: still attached, and still the control that was inspected. It never searches by\n * selector or label for a substitute, and it sends nothing.\n */\n readonly revalidateElement: (\n reference: ObservedElement,\n options?: OperationOptions,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n readonly clickElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly fillElement: (\n reference: ObservedElement,\n value: string,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /** One selection using this exact select and its issued option element IDs, with fresh checks. */\n readonly selectOption: (\n reference: ObservedElement,\n options: SelectOptions,\n admission?: ElementAdmission,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n readonly hoverElement: (\n reference: ObservedElement,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** A key stroke sent only if the exact node an observation named already has focus. */\n readonly pressElement: (\n reference: ObservedElement,\n stroke: KeyStroke,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /** Real typing with `fillElement`'s exactness: that node must already have focus. */\n readonly typeElement: (\n reference: ObservedElement,\n text: string,\n admission?: ElementAdmission,\n options?: OperationOptions,\n ) => Effect.Effect<InputReceipt, BrowserError>;\n /**\n * Set several controls of one observation in order, then optionally click one submit control.\n * Every step acts on the exact observed node after the same fresh checks as `fillElement`,\n * except that a control may have become enabled since it was observed, and each is admitted\n * and charged as its own action. The observation stays usable for this form's own steps only;\n * anything else that changes the page still retires it, and the form retires it when it ends.\n * The first refused or uncertain step ends the form. Submit is sent only after every field\n * succeeded and, unless `verify` is false, still holds what its step left there. Fields set\n * before a stop stay set. It fails only when its first step does; otherwise `stopped` says\n * where it ended and why.\n */\n readonly fillForm: (\n request: FillFormRequest,\n admission?: ElementAdmission,\n options?: FillFormOptions,\n operationOptions?: OperationOptions,\n ) => Effect.Effect<FillFormResult, BrowserError>;\n /**\n * Wait on the original observed node within the host deadline. Hidden includes its detachment;\n * document replacement is stale. Success observes a condition, without authorizing later input.\n */\n readonly waitForElement: (\n request: WaitForElementRequest,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n readonly waitFor: (\n request: {\n readonly selector: string;\n readonly state: \"visible\" | \"hidden\" | \"attached\" | \"detached\";\n },\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /** Navigation observation is registered before the single click dispatch. */\n readonly clickAndWait: (\n request: ClickRequest,\n options?: OperationOptions,\n ) => Effect.Effect<ActionResult, BrowserError>;\n /**\n * Readiness of the current document only. Dependent operations wait for it themselves;\n * this reports it without charging an action, so a caller can decide what to do about a\n * document that predates the registrations.\n */\n readonly ready: (\n options?: OperationOptions,\n ) => Effect.Effect<Bootstrap.ReadinessOutcome, InitializationError>;\n}\n\n/**\n * Issued live authority for one native frame; navigation keeps the frame and retires its references.\n *\n * Pass the issued object itself. Capture, page control, provider transfers, agent tools and\n * `checkPage` authenticate it by identity, so a spread copy, wrapper or Proxy is refused\n * `UnregisteredSession` even though its copied methods still run. Observe work through the\n * timeline and run receipts rather than by wrapping methods.\n */\nexport interface Frame extends PageOperations, PlanOperations {\n readonly identity: Target;\n readonly status: Effect.Effect<PageStatus>;\n /**\n * The original owner's host monotonic clock, the same one `session.monotonicTimeNanos` reads,\n * so a host bound to this Page or Frame can compute a plan's `startAt` without the session.\n */\n readonly monotonicTimeNanos: Effect.Effect<bigint>;\n /** Resolve full descriptor intent against the complete current native candidate inventory. */\n readonly resolve: (\n descriptor: DescriptorEncoded,\n options?: ResolveOptions,\n ) => Effect.Effect<ObservedElement, BrowserError>;\n /**\n * Quiet DOM mutation, scroll, root geometry and viewport signals in one pinned document.\n * This does not establish network, animation or business completion.\n */\n readonly settled: (\n request: SettledRequest,\n options?: OperationOptions,\n ) => Effect.Effect<SettledEvidence, BrowserError>;\n}\n\n/** Issued live authority for one native page on its original connection; pass it as issued, see `Frame`. */\nexport interface Page extends Frame {\n /** Evidence for this issued Page's original domain; navigation does not change its journal. */\n readonly timeline: Timeline;\n /**\n * Read this page's address and title. Like `listFrames`, it is a read of the page and shares\n * its permit; pass `admission.queue` to wait behind work already running there.\n */\n readonly describe: (options?: OperationOptions) => Effect.Effect<PageInfo, BrowserError>;\n readonly listFrames: (\n options?: OperationOptions,\n ) => Effect.Effect<ReadonlyArray<FrameInfo>, BrowserError>;\n /** Issue a child Frame. Like `session.page`, issuing takes the registry permit, not this page's. */\n readonly frame: (\n info: FrameInfo,\n options?: OperationOptions,\n ) => Effect.Effect<Frame, BrowserError>;\n readonly resizeViewport: (\n viewport: Viewport,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /** Reserved fail-fast admission; timeoutMillis bounds joining a close, not admission.queue. */\n readonly close: (options?: OperationOptions) => Effect.Effect<void, BrowserError>;\n}\n\n/**\n * A host's decision about one control, made on facts read from the exact node immediately\n * before input. Returning anything but `true`, or throwing, sends nothing and fails `denied`.\n * It is a plain synchronous function on purpose: it runs while the owner's permit is held, where\n * waiting on a model or a network call would stall other ordinary operations on that Page. It is not an atomic\n * check-and-input transaction, because page script can still run before the native input lands.\n */\nexport interface ElementAdmission {\n readonly admit: (facts: ControlFacts) => boolean;\n}\n\n/**\n * A navigation the browser is still performing. The owner's permit was released when it was\n * dispatched, so while it loads a host may read, `checkpoint`, hold and resume this page, and\n * use any other page. Anything that would change this page fails `busy` until it settles.\n *\n * Leaving its scope unsettled revokes and attempts to close its exact page. Positive closure\n * preserves healthy peers; unconfirmed closure fences the session. Its outcome remains unknown\n * because nothing then knows what the browser did. It is never replayed.\n */\nexport interface NavigationOperation {\n /** What was navigated, read before dispatch. */\n readonly target: Target;\n /**\n * What was asked for, as checked before dispatch. `Plan.recordedNavigation` records this\n * request, not the address `completed` reports after any redirect.\n */\n readonly request: StartNavigationRequest;\n /**\n * The document reached DOMContentLoaded. It belongs to this one navigation: a successor that\n * reaches the same URL fails it instead. Interrupting a waiter stops nothing in the browser.\n */\n readonly completed: Effect.Effect<NavigationResult, BrowserError>;\n /**\n * Asks the browser to stop loading and waits for this navigation to settle, after which\n * `completed` fails `interrupted`. Success is a known outcome and the session stays usable:\n * the page holds whatever had loaded. It does not undo anything the page already did.\n * An already-completed navigation cannot stop a successor. Concurrent callers share their\n * active attempt. Before dispatch, cancellation or a busy refusal permits a later request\n * once native setup has retired. A dispatched attempt's outcome, including failure or\n * interruption, is retained permanently; an uncertain stop is never replayed.\n */\n readonly stop: Effect.Effect<void, BrowserError>;\n}\n\n/**\n * Host control over one owned browser. This is not a serializable model value: copying a\n * session object cannot copy its capture, page-control or connection authority.\n *\n * Browser operations belong to issued Page and Frame objects. Display selection and owner\n * lifecycle are separate from those exact capabilities.\n */\nexport interface BrowserSession<E = never> {\n /** The original owner's host clock, for same-runtime absolute cue scheduling. */\n readonly monotonicTimeNanos: Effect.Effect<bigint>;\n /** Stable facade: each snapshot/stream captures the current journal when it executes. */\n readonly timeline: Timeline;\n /** Atomic cached Inventory baseline followed by lifecycle evidence on its pinned journal. */\n readonly pages: Stream.Stream<PageEvent, TimelineError>;\n /** The page acquired on the initial connection, independent of later display selection. */\n readonly initialPage: Page;\n /**\n * Authenticate `info` against the live native page and return its canonical issued Page. This\n * takes the registry permit, not the page's own: it never waits for, or fails `busy` on, work\n * already running on that page.\n */\n readonly page: (info: PageInfo, options?: OperationOptions) => Effect.Effect<Page, BrowserError>;\n readonly listPages: (\n options?: OperationOptions,\n ) => Effect.Effect<ReadonlyArray<PageInfo>, BrowserError>;\n /** The implementation which owns this live connection. */\n readonly implementation: string;\n /** Copied host-only state, readable without admission in every lifecycle phase. */\n readonly status: Effect.Effect<SessionStatus>;\n /** Bounded native/policy diagnostics. Typed callback causes remain in bindingDiagnostics. */\n readonly diagnostics: Effect.Effect<BrowserDiagnostics>;\n readonly admission: Effect.Effect<SessionAdmissionStatus>;\n /** Close this scope and require its own ownership-specific cleanup evidence. */\n readonly closeChecked: Effect.Effect<void, BrowserError>;\n /** First fail-session callback cause, preserving the consumer's error type on the host. */\n readonly failure: Effect.Effect<never, E | InitializationError>;\n /** Bounded host-only evidence; consumer causes are never projected into a page reply. */\n readonly bindingDiagnostics: Effect.Effect<Bootstrap.BindingDiagnostics<E>>;\n /**\n * Choose the displayed page, which is also the page `detach` records and `reconnect` resumes on.\n * Nothing else: it never retargets a Page or Frame operation, which keep their own exact\n * authority. Pass a Page this session issued; a closed or stale one is refused unsent.\n */\n readonly selectPage: (\n page: Page,\n options?: OperationOptions,\n ) => Effect.Effect<void, BrowserError>;\n /**\n * Open a page in its own window, sized like the others, without selecting it, and return the\n * Page issued for it. Chromium paints every window, so any page can be pictured and read at\n * speed, not only the one in front. Registry admission fails immediately by default; pass a\n * positive finite admission.queue to wait FIFO within the operation deadline.\n */\n readonly createPage: (options?: OperationOptions) => Effect.Effect<Page, BrowserError>;\n}\n\n/** Helpers that do not supervise callback failures accept any live browser session. */\nexport type AnySession = BrowserSession<unknown>;\n\n/**\n * Check that `page` is a Page or Frame this session issued, by identity, and that its authority\n * is still current: a foreign or copied object fails `UnregisteredSession`, a closed, stale or\n * detached one fails with the same refusal its own operations would report. Adapters such as\n * agent-browser hosts call it once when they bind, so a wrong target fails before any work.\n */\nexport const checkPage = (\n session: AnySession,\n page: Page | Frame,\n): Effect.Effect<void, BrowserError> =>\n resolveTargetControlsForSession(session, page).pipe(Effect.asVoid);\n\n/** Dependencies are captured at acquisition, not erased into an environment-free service Layer. */\nexport interface OpenOptions<E = never, R = never> {\n readonly bootstrap?: Bootstrap.Plan<E, R>;\n}\n\n/**\n * Acquire, supervise and close one browser in a fresh scope. The callback keeps the concrete\n * Chromium or provider session type. Its scoped work is joined before checked browser cleanup,\n * and cleanup failures remain typed even when the callback has already failed or was cancelled.\n * Acquisition is evaluated once; an uncertain result is never retried.\n */\nexport const scoped: {\n <S extends BrowserSession<unknown>, A, E2, R2>(\n f: (session: S) => Effect.Effect<A, E2, R2>,\n ): <E, AE, AR>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n ) => Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ): Effect.Effect<\n A,\n AE | E | E2 | InitializationError | BrowserError,\n Exclude<AR | R2, Scope.Scope>\n >;\n} = dual(\n 2,\n <S, E, AE, AR, A, E2, R2>(\n open: Effect.Effect<S & BrowserSession<E>, AE, AR>,\n f: (session: S & BrowserSession<E>) => Effect.Effect<A, E2, R2>,\n ) =>\n Effect.scoped(\n Effect.flatMap(open, (session) =>\n Effect.scoped(\n Effect.raceFirst(\n session.failure,\n Effect.suspend(() => f(session)),\n ),\n ).pipe(Effect.onExit(() => session.closeChecked)),\n ),\n ),\n);\n"],"mappings":";;;;;;;;;;AAubA,MAAa,aACX,SACA,SAEA,gCAAgC,SAAS,IAAI,CAAC,CAAC,KAAK,OAAO,MAAM;;;;;;;AAanE,MAAa,SAkBT,KACF,IAEE,MACA,MAEA,OAAO,OACL,OAAO,QAAQ,OAAO,YACpB,OAAO,OACL,OAAO,UACL,QAAQ,SACR,OAAO,cAAc,EAAE,OAAO,CAAC,CACjC,CACF,CAAC,CAAC,KAAK,OAAO,aAAa,QAAQ,YAAY,CAAC,CAClD,CACF,CACJ"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BrowserError, InitializationError } from "./Errors.mjs";
|
|
2
2
|
import { S as InlineFile, T as Inventory, it as Viewport, l as ClickRequest, o as BrowserPolicy, r as AutomationOptions, t as ActionResult } from "./BrowserData-CPIIIHol.mjs";
|
|
3
|
-
import { c as OpenOptions, i as BrowserSession, l as OperationOptions, u as Page } from "./Browser-
|
|
3
|
+
import { c as OpenOptions, i as BrowserSession, l as OperationOptions, u as Page } from "./Browser-BF9_sORP.mjs";
|
|
4
4
|
import { Crypto, Effect, Option, Redacted, Schema, Scope } from "effect";
|
|
5
5
|
//#region src/internal/browser/ConnectionCleanup.d.ts
|
|
6
6
|
/** Evidence about this connection, independent of the lifetime of its browser process. */
|
|
@@ -145,4 +145,4 @@ declare const make: (options: RuntimeOptions) => Effect.Effect<Runtime, BrowserE
|
|
|
145
145
|
//#endregion
|
|
146
146
|
export { reported as S, CleanupStep as _, DownloadEvent as a, cleanupStep as b, Lifetime as c, Runtime as d, RuntimeOptions as f, playwright as g, make as h, Connection as i, PlaywrightOptions as l, TransferOperations as m, Acquisition as n, FileRequest as o, Source as p, BrowserBinding as r, Integration as s, AcquireOptions as t, ResolvedFileSelection as u, ConnectionCleanup as v, noConnection as x, ConnectionState as y };
|
|
147
147
|
export {};
|
|
148
|
-
//# sourceMappingURL=BrowserRuntime-
|
|
148
|
+
//# sourceMappingURL=BrowserRuntime-B6nT1G4X.d.mts.map
|
|
@@ -2,8 +2,8 @@ import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
|
|
|
2
2
|
import { ActionResult, AutomationOptions, BrowserPolicy, ClickRequest, ControlFacts, FrameInfo, Identifier, InlineFiles, ObservedControl, PageExecutionState, PageInfo, PageSuspension, SafeFilename, Selector, Viewport, ViewportEvidence } from "./BrowserData.mjs";
|
|
3
3
|
import { m as snapshot, o as Plan, p as invoke, u as empty } from "./Bootstrap-CeQOWBy6.mjs";
|
|
4
4
|
import { c as resolvePageControlsForSession } from "./PageRegistry-B3q9DS3g.mjs";
|
|
5
|
-
import { C as sanitize, D as capture, E as autonomous, O as span, S as safeDecode, _ as closeWithin, a as acquireSession, b as providerReason, d as move, f as pointer, g as NativeFailure, h as ownerPacing, i as makeSession, m as isPerformed, n as checkedOperationOptions, p as scroll, r as decoded, t as checked, u as keys, v as exitPromise, w as timeout, x as publicError, y as failure } from "./PublicSession-
|
|
6
|
-
import { n as pngGeometry } from "./Images-
|
|
5
|
+
import { C as sanitize, D as capture, E as autonomous, O as span, S as safeDecode, _ as closeWithin, a as acquireSession, b as providerReason, d as move, f as pointer, g as NativeFailure, h as ownerPacing, i as makeSession, m as isPerformed, n as checkedOperationOptions, p as scroll, r as decoded, t as checked, u as keys, v as exitPromise, w as timeout, x as publicError, y as failure } from "./PublicSession-DCSMMqOj.mjs";
|
|
6
|
+
import { n as pngGeometry } from "./Images-yCG9BmRU.mjs";
|
|
7
7
|
import { TimelineDefaults } from "./TimelineData.mjs";
|
|
8
8
|
import { Cause, Crypto, Deferred, Effect, Exit, FiberSet, Redacted, Result, Schema, Scope, Tracer } from "effect";
|
|
9
9
|
//#region src/internal/browser/Binding.ts
|
|
@@ -706,42 +706,73 @@ const keyDescription = (key) => {
|
|
|
706
706
|
/** Performed text holds Shift for uppercase and shifted punctuation, as a person would. */
|
|
707
707
|
const heldModifiers = (key, modifiers, typed) => typed && /^[A-Z~!@#$%^&*()_+{}|:"<>?]$/u.test(key) && !modifiers.includes("Shift") ? [...modifiers, "Shift"] : modifiers;
|
|
708
708
|
const nanos = (milliseconds) => BigInt(Math.round(milliseconds * 1e6));
|
|
709
|
+
/** The commands a stroke submits: inserted text, or its key and held modifiers down and up. */
|
|
710
|
+
const commandsOf = (stroke, modifiers, typed) => keyDescription(stroke.key) === void 0 ? 1 : 2 * (1 + heldModifiers(stroke.key, modifiers, typed).length);
|
|
709
711
|
/**
|
|
710
712
|
* The measured pace of one performed key run. Strokes start at their absolute schedule offsets,
|
|
711
|
-
* and each key is held from its key-down. Before a stroke's first command, the rest
|
|
712
|
-
* schedule must still fit the original deadline at the round trips the browser has
|
|
713
|
-
* taken, so a stroke that could not finish is refused whole, never cut between a key and
|
|
714
|
-
* release.
|
|
713
|
+
* and each key is held from its key-down's submission. Before a stroke's first command, the rest
|
|
714
|
+
* of the schedule must still fit the original deadline at the round trips the browser has
|
|
715
|
+
* actually taken, so a stroke that could not finish is refused whole, never cut between a key and
|
|
716
|
+
* its release.
|
|
717
|
+
*
|
|
718
|
+
* A stroke's commands share their round trip, but a renderer handles them one at a time. So each
|
|
719
|
+
* stroke is measured from its first submission (its span) and from its last (its drain) until its
|
|
720
|
+
* last reply, and charged as strokes of its own command count were. More commands never finish
|
|
721
|
+
* sooner: a larger stroke's measures bound a smaller one's, and a smaller one's are scaled up by
|
|
722
|
+
* command count, which overcharges latency only until a stroke of this size has drained.
|
|
715
723
|
*/
|
|
716
724
|
const makePace = (pacing, strokes, started, focus, modifiers, typed) => {
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
+
const focused = {
|
|
726
|
+
nanos: 0n,
|
|
727
|
+
count: 0
|
|
728
|
+
};
|
|
729
|
+
const drained = /* @__PURE__ */ new Map();
|
|
730
|
+
const mean = (total, count) => count === 0 ? 0 : Number(total / BigInt(count)) / 1e6;
|
|
731
|
+
const expected = (commands, check) => {
|
|
732
|
+
let bound;
|
|
733
|
+
let below;
|
|
734
|
+
for (const size of drained.keys()) {
|
|
735
|
+
if (size >= commands && (bound === void 0 || size < bound)) bound = size;
|
|
736
|
+
if (size < commands && (below === void 0 || size > below)) below = size;
|
|
725
737
|
}
|
|
738
|
+
const size = bound ?? below;
|
|
739
|
+
const measured = size === void 0 ? void 0 : drained.get(size);
|
|
740
|
+
if (size === void 0 || measured === void 0) return {
|
|
741
|
+
span: 0,
|
|
742
|
+
drain: check * Math.max(1, commands / 2)
|
|
743
|
+
};
|
|
744
|
+
const scale = size < commands ? commands / size : 1;
|
|
745
|
+
return {
|
|
746
|
+
span: mean(measured.span, measured.count) * scale,
|
|
747
|
+
drain: mean(measured.drain, measured.count) * scale
|
|
748
|
+
};
|
|
726
749
|
};
|
|
727
|
-
const mean = ({ nanos: total, count }) => count === 0 ? 0 : Number(total / BigInt(count)) / 1e6;
|
|
728
750
|
return {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
751
|
+
observeFocus: (elapsed) => {
|
|
752
|
+
focused.nanos += elapsed;
|
|
753
|
+
focused.count++;
|
|
754
|
+
},
|
|
755
|
+
observeDrain: (commands, span, drain) => {
|
|
756
|
+
const measured = drained.get(commands) ?? {
|
|
757
|
+
span: 0n,
|
|
758
|
+
drain: 0n,
|
|
759
|
+
count: 0
|
|
760
|
+
};
|
|
761
|
+
measured.span += span;
|
|
762
|
+
measured.drain += drain;
|
|
763
|
+
measured.count++;
|
|
764
|
+
drained.set(commands, measured);
|
|
732
765
|
},
|
|
733
766
|
/** Called after stroke `from`'s focus check, before its first command. */
|
|
734
767
|
require: (from) => {
|
|
735
|
-
const check = focus ? mean(
|
|
736
|
-
const command = measured.command.count > 0 ? mean(measured.command) : check;
|
|
768
|
+
const check = focus ? mean(focused.nanos, focused.count) : 0;
|
|
737
769
|
let at = Number(pacing.now() - started) / 1e6;
|
|
738
770
|
for (let index = from; index < strokes.length; index++) {
|
|
739
771
|
const stroke = strokes[index];
|
|
740
772
|
if (stroke === void 0) break;
|
|
741
|
-
const
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
at = Math.max(down + command, down + stroke.holdMillis) + (keyed ? (1 + held) * command : 0);
|
|
773
|
+
const down = Math.max(at, stroke.offsetMillis) + (index === from ? 0 : check);
|
|
774
|
+
const { span, drain } = expected(commandsOf(stroke, modifiers, typed), check);
|
|
775
|
+
at = keyDescription(stroke.key) === void 0 ? down + Math.max(drain, stroke.holdMillis) : down + Math.max(stroke.holdMillis + drain, span);
|
|
745
776
|
}
|
|
746
777
|
pacing.requireBy(started + nanos(at));
|
|
747
778
|
}
|
|
@@ -895,7 +926,11 @@ const makeKeyboard = (targets, elements, receipt) => {
|
|
|
895
926
|
* One paired native stroke remains unresolved until its key and modifiers are all released.
|
|
896
927
|
* Playwright's own keyboard builds each event, so a performed stroke carries exactly what a
|
|
897
928
|
* plain `press` sends, including the editing commands Chromium on macOS needs for Backspace,
|
|
898
|
-
* Delete, the arrows, Home and End.
|
|
929
|
+
* Delete, the arrows, Home and End. Like plain typing's windows, a stroke submits its commands
|
|
930
|
+
* in order without waiting for each reply, so a key is held for its planned time rather than for
|
|
931
|
+
* its key-down's round trip, and a held modifier adds no round trip of its own. A stroke has at
|
|
932
|
+
* most ten replies outstanding, its key and up to four modifiers each down and up, and drains
|
|
933
|
+
* them all before it resolves.
|
|
899
934
|
*/
|
|
900
935
|
const pacedStroke = async (keyboard, stroke, ticket, check, pace, index, element, modifiers = [], typed = true) => {
|
|
901
936
|
const pacing = ownerPacing(ticket);
|
|
@@ -904,38 +939,56 @@ const makeKeyboard = (targets, elements, receipt) => {
|
|
|
904
939
|
if (element !== void 0) {
|
|
905
940
|
const before = pacing.now();
|
|
906
941
|
await requireFocus(element);
|
|
907
|
-
pace.
|
|
942
|
+
pace.observeFocus(pacing.now() - before);
|
|
908
943
|
}
|
|
909
944
|
check();
|
|
910
945
|
pace.require(index);
|
|
911
|
-
const
|
|
946
|
+
const pending = [];
|
|
947
|
+
let failed = false;
|
|
948
|
+
let firstFailure;
|
|
949
|
+
const first = pacing.now();
|
|
950
|
+
let submitted = first;
|
|
951
|
+
const reject = (error) => {
|
|
952
|
+
if (!failed) {
|
|
953
|
+
failed = true;
|
|
954
|
+
firstFailure = error;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
const submit = (command) => {
|
|
958
|
+
if (failed) throw firstFailure;
|
|
912
959
|
check();
|
|
913
960
|
ticket.dispatch();
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
pace.observe("command", pacing.now() - before);
|
|
961
|
+
submitted = pacing.now();
|
|
962
|
+
pending.push(command().then(() => {}, reject));
|
|
917
963
|
};
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
964
|
+
const drain = async () => {
|
|
965
|
+
await Promise.all(pending);
|
|
966
|
+
if (failed) throw firstFailure;
|
|
967
|
+
const replied = pacing.now();
|
|
968
|
+
pace.observeDrain(pending.length, replied - first, replied - submitted);
|
|
921
969
|
ticket.acknowledge?.({
|
|
922
970
|
subphase: "key-burst",
|
|
923
971
|
logicalComplete: false
|
|
924
972
|
});
|
|
925
|
-
|
|
973
|
+
};
|
|
974
|
+
if (keyDescription(stroke.key) === void 0) {
|
|
975
|
+
submit(() => keyboard.insertText(stroke.key));
|
|
976
|
+
await drain();
|
|
977
|
+
await pacing.pauseUntil(first + nanos(stroke.holdMillis));
|
|
926
978
|
return;
|
|
927
979
|
}
|
|
928
980
|
const held = heldModifiers(stroke.key, modifiers, typed);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
981
|
+
try {
|
|
982
|
+
for (const modifier of held) submit(() => keyboard.down(modifier));
|
|
983
|
+
const down = pacing.now();
|
|
984
|
+
submit(() => keyboard.down(stroke.key));
|
|
985
|
+
await pacing.pauseUntil(down + nanos(stroke.holdMillis));
|
|
986
|
+
submit(() => keyboard.up(stroke.key));
|
|
987
|
+
for (const modifier of held.toReversed()) submit(() => keyboard.up(modifier));
|
|
988
|
+
} catch (error) {
|
|
989
|
+
reject(error);
|
|
990
|
+
}
|
|
991
|
+
await drain();
|
|
939
992
|
};
|
|
940
993
|
const pacedKeys = async (keyboard, schedule, ticket, check, element) => {
|
|
941
994
|
const pacing = ownerPacing(ticket);
|
|
@@ -965,7 +1018,7 @@ const makeKeyboard = (targets, elements, receipt) => {
|
|
|
965
1018
|
await requireFocus(element);
|
|
966
1019
|
const roundTrip = pacing.now() - before;
|
|
967
1020
|
const pace = makePace(pacing, schedule.strokes, pacing.now() + 2n * roundTrip, true, [], true);
|
|
968
|
-
pace.
|
|
1021
|
+
pace.observeFocus(roundTrip);
|
|
969
1022
|
pace.require(0);
|
|
970
1023
|
check();
|
|
971
1024
|
ticket.dispatch();
|
|
@@ -3398,11 +3451,20 @@ const makePointer = (targets, elements) => {
|
|
|
3398
3451
|
/**
|
|
3399
3452
|
* Plans and paces the glide to one exact node. A press may first scroll its node into view; a
|
|
3400
3453
|
* hover never scrolls (`scrollIntoView: false`): it aims inside the node's visible part and
|
|
3401
|
-
* refuses a node with none `NotVisible`.
|
|
3454
|
+
* refuses a node with none `NotVisible`. Each read of the node's box is one protocol round trip
|
|
3455
|
+
* on the main frame, and the fastest of them is returned for the caller to charge its input by.
|
|
3402
3456
|
*/
|
|
3403
3457
|
const preparePress = async (page, element, ticket, check, options = {}) => {
|
|
3404
3458
|
const pacing = ownerPacing(ticket);
|
|
3405
3459
|
const { performance } = ticket;
|
|
3460
|
+
let roundTripNanos;
|
|
3461
|
+
const boxOf = async () => {
|
|
3462
|
+
const started = pacing.now();
|
|
3463
|
+
const measured = await element.boundingBox();
|
|
3464
|
+
const elapsed = pacing.now() - started;
|
|
3465
|
+
if (roundTripNanos === void 0 || elapsed < roundTripNanos) roundTripNanos = elapsed;
|
|
3466
|
+
return measured;
|
|
3467
|
+
};
|
|
3406
3468
|
check();
|
|
3407
3469
|
const frame = await element.ownerFrame();
|
|
3408
3470
|
check();
|
|
@@ -3471,7 +3533,7 @@ const makePointer = (targets, elements) => {
|
|
|
3471
3533
|
height: innerHeight
|
|
3472
3534
|
})));
|
|
3473
3535
|
check();
|
|
3474
|
-
let box = await
|
|
3536
|
+
let box = await boxOf();
|
|
3475
3537
|
check();
|
|
3476
3538
|
if (box === null) throw failure(Reasons.NotVisible.make({}), "undispatched");
|
|
3477
3539
|
const outside = layout.clipped || box.x < 0 || box.y < 0 || box.x + box.width > viewport.width || box.y + box.height > viewport.height;
|
|
@@ -3501,7 +3563,7 @@ const makePointer = (targets, elements) => {
|
|
|
3501
3563
|
completedMonotonicNanos: pacing.now()
|
|
3502
3564
|
});
|
|
3503
3565
|
check();
|
|
3504
|
-
const scrolled = await
|
|
3566
|
+
const scrolled = await boxOf();
|
|
3505
3567
|
check();
|
|
3506
3568
|
if (scrolled === null) throw failure(Reasons.NotVisible.make({}), "undispatched");
|
|
3507
3569
|
box = scrolled;
|
|
@@ -3541,7 +3603,7 @@ const makePointer = (targets, elements) => {
|
|
|
3541
3603
|
});
|
|
3542
3604
|
if (schedule.durationMillis > 0) await pacing.pauseUntil(started + BigInt(Math.round(schedule.durationMillis * 1e6)));
|
|
3543
3605
|
check();
|
|
3544
|
-
const beforeInput = await
|
|
3606
|
+
const beforeInput = await boxOf();
|
|
3545
3607
|
check();
|
|
3546
3608
|
if (beforeInput === null || Math.abs(beforeInput.x - box.x) > .01 || Math.abs(beforeInput.y - box.y) > .01 || Math.abs(beforeInput.width - box.width) > .01 || Math.abs(beforeInput.height - box.height) > .01) throw failure(Reasons.Stale.make({}), "undispatched");
|
|
3547
3609
|
await reachablePoint(page, element, schedule.aim, check);
|
|
@@ -3552,7 +3614,8 @@ const makePointer = (targets, elements) => {
|
|
|
3552
3614
|
position: schedule.aim,
|
|
3553
3615
|
relativePosition,
|
|
3554
3616
|
qualification: "checked-exact-node-sample"
|
|
3555
|
-
}
|
|
3617
|
+
},
|
|
3618
|
+
roundTripNanos: roundTripNanos ?? 0n
|
|
3556
3619
|
};
|
|
3557
3620
|
};
|
|
3558
3621
|
const hover = (target, ticket, policy, browserTarget) => sanitize(async () => {
|
|
@@ -3949,6 +4012,20 @@ const settleInPage = (_node, millis) => new Promise((resolve) => {
|
|
|
3949
4012
|
finish();
|
|
3950
4013
|
}, millis);
|
|
3951
4014
|
});
|
|
4015
|
+
/**
|
|
4016
|
+
* The round trips a performed click is charged before dispatch. Over relays adding 18 to 70 ms
|
|
4017
|
+
* each way, Playwright 1.63's positioned click took 17 to 23 round trips in all, 14 to 19 of them
|
|
4018
|
+
* checking the node before its first input event. A deadline inside those checks closes the Page
|
|
4019
|
+
* over input never sent, while a refusal leaves it open, so the charge covers the most checking
|
|
4020
|
+
* measured, at the cost of refusing a click that had a few round trips to spare.
|
|
4021
|
+
*/
|
|
4022
|
+
const performedClickRoundTrips = 20;
|
|
4023
|
+
/**
|
|
4024
|
+
* What a performed click is charged besides its round trips. Playwright's stability check waits
|
|
4025
|
+
* for the node across animation frames, which no round trip measures: without a relay, a whole
|
|
4026
|
+
* click took 57 ms.
|
|
4027
|
+
*/
|
|
4028
|
+
const performedClickFloorMillis = 100;
|
|
3952
4029
|
/** A step that already dispatched reads back within a bounded wait, and never fails for it. */
|
|
3953
4030
|
const bounded = async (work, millis, fallback) => {
|
|
3954
4031
|
let timer;
|
|
@@ -4129,11 +4206,13 @@ const makeActions = (context, targets, observation, isTimeoutError) => {
|
|
|
4129
4206
|
/** What is left of a step's deadline for reading back, keeping a margin for the owner. */
|
|
4130
4207
|
const readBack = (ticket, wanted) => Math.min(wanted, Math.max(0, ticket.remainingMillis() - 250));
|
|
4131
4208
|
const clickElement = (page, element, ticket, capture, check = () => ticket.check(), readmit) => {
|
|
4132
|
-
if (isPerformed(ticket)) return pointer.preparePress(page, element, ticket, check).then(async (
|
|
4209
|
+
if (isPerformed(ticket)) return pointer.preparePress(page, element, ticket, check).then(async (prepared) => {
|
|
4210
|
+
const { roundTripNanos, ...planned } = prepared;
|
|
4133
4211
|
await readmit?.();
|
|
4134
4212
|
check();
|
|
4135
4213
|
const dispatch = async () => {
|
|
4136
4214
|
check();
|
|
4215
|
+
ownerPacing(ticket).requireDuration(performedClickFloorMillis + performedClickRoundTrips * Number(roundTripNanos) / 1e6);
|
|
4137
4216
|
ticket.dispatch();
|
|
4138
4217
|
pointer.invalidate(page, planned.intended.position);
|
|
4139
4218
|
await element.click({
|
|
@@ -6738,4 +6817,4 @@ const make = Effect.fnUntraced(function* (options) {
|
|
|
6738
6817
|
//#endregion
|
|
6739
6818
|
export { stableIdentityOf as a, issueBinding as c, identityOf as i, make as n, MaximumGroupNodes as o, playwright as r, fromNativeAttempt as s, DownloadEvent as t };
|
|
6740
6819
|
|
|
6741
|
-
//# sourceMappingURL=BrowserRuntime-
|
|
6820
|
+
//# sourceMappingURL=BrowserRuntime-C1r2aSRK.mjs.map
|