effect-browser 0.2.0-beta.9 → 0.3.0-beta.5

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 (202) hide show
  1. package/README.md +705 -1184
  2. package/dist/Browser-CM65Uo7f.mjs +437 -0
  3. package/dist/Browser-CM65Uo7f.mjs.map +1 -0
  4. package/dist/Browser.d.mts +115 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +202 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +295 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-DVP2IJWm.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-BERmf_gM.d.mts +117 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +123 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-D4281dIi.mjs +618 -0
  39. package/dist/Page-D4281dIi.mjs.map +1 -0
  40. package/dist/Page-DYTdVhYa.d.mts +593 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-CNNOU1Tx.d.mts +112 -0
  44. package/dist/Plan-Dtrb-8im.mjs +311 -0
  45. package/dist/Plan-Dtrb-8im.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +54 -0
  59. package/dist/Stage.mjs +133 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-BtEG93gR.mjs +420 -0
  62. package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
  63. package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-DAYjATgC.mjs +137 -0
  67. package/dist/context-DAYjATgC.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-Banfm_zc.mjs +5097 -0
  73. package/dist/page-Banfm_zc.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-CcL0K7S0.mjs +262 -0
  76. package/dist/url-CcL0K7S0.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +625 -0
  79. package/src/BrowserError.ts +225 -0
  80. package/src/BrowserEvent.ts +294 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +130 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +520 -0
  89. package/src/Plan.ts +350 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +223 -0
  94. package/src/Supervisor.ts +353 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +607 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/guard.inpage.ts +605 -0
  101. package/src/internal/input/guard.ts +183 -0
  102. package/src/internal/input/keyboard.ts +112 -0
  103. package/src/internal/input/keys.ts +127 -0
  104. package/src/internal/input/perform.ts +322 -0
  105. package/src/internal/input/pointer.ts +153 -0
  106. package/src/internal/input/replies.ts +159 -0
  107. package/src/internal/input/settle.ts +50 -0
  108. package/src/internal/input/style.ts +186 -0
  109. package/src/internal/input/targets.inpage.ts +293 -0
  110. package/src/internal/input/targets.ts +148 -0
  111. package/src/internal/page/bridge.ts +386 -0
  112. package/src/internal/page/context.ts +302 -0
  113. package/src/internal/page/lane.ts +301 -0
  114. package/src/internal/page/navigation.ts +183 -0
  115. package/src/internal/page/openings.ts +87 -0
  116. package/src/internal/page/page.ts +137 -0
  117. package/src/internal/page/url.ts +100 -0
  118. package/src/internal/page/viewport.ts +43 -0
  119. package/src/internal/pictures/capture.ts +778 -0
  120. package/src/internal/pictures/clock.ts +141 -0
  121. package/src/internal/pictures/pictures.ts +520 -0
  122. package/src/internal/pictures/transport.ts +242 -0
  123. package/src/internal/reading/choose.ts +100 -0
  124. package/src/internal/reading/context.inpage.ts +201 -0
  125. package/src/internal/reading/controls.inpage.ts +158 -0
  126. package/src/internal/reading/expect.inpage.ts +70 -0
  127. package/src/internal/reading/match.inpage.ts +92 -0
  128. package/src/internal/reading/names.inpage.ts +306 -0
  129. package/src/internal/reading/outline.inpage.ts +260 -0
  130. package/src/internal/reading/reading.ts +223 -0
  131. package/src/internal/reading/ready.inpage.ts +154 -0
  132. package/src/internal/reading/ready.ts +105 -0
  133. package/src/internal/reading/subjects.inpage.ts +188 -0
  134. package/src/internal/reading/text.inpage.ts +135 -0
  135. package/src/internal/reading/walk.inpage.ts +220 -0
  136. package/src/internal/supervisor/lifecycle.ts +265 -0
  137. package/src/internal/timeline/changes.inpage.ts +216 -0
  138. package/src/internal/timeline/changes.ts +100 -0
  139. package/src/internal/timeline/events.ts +93 -0
  140. package/src/internal/timeline/fold.inpage.ts +123 -0
  141. package/src/internal/timeline/history.inpage.ts +229 -0
  142. package/src/internal/timeline/marks.inpage.ts +207 -0
  143. package/src/internal/timeline/record.inpage.ts +133 -0
  144. package/src/internal/timeline/sight.inpage.ts +122 -0
  145. package/src/internal/timeline/window.ts +96 -0
  146. package/src/internal/timeline/words.ts +65 -0
  147. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  148. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  149. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  150. package/dist/Bootstrap.d.mts +0 -3
  151. package/dist/Bootstrap.mjs +0 -2
  152. package/dist/Browser-BF9_sORP.d.mts +0 -388
  153. package/dist/Browser.mjs.map +0 -1
  154. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  155. package/dist/BrowserData.d.mts +0 -3
  156. package/dist/BrowserData.mjs +0 -695
  157. package/dist/BrowserData.mjs.map +0 -1
  158. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  159. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  160. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  161. package/dist/BrowserRuntime.d.mts +0 -3
  162. package/dist/BrowserRuntime.mjs +0 -3
  163. package/dist/Capture.d.mts +0 -56
  164. package/dist/Capture.mjs +0 -678
  165. package/dist/Capture.mjs.map +0 -1
  166. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  167. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  168. package/dist/CaptureData.d.mts +0 -219
  169. package/dist/CaptureData.mjs +0 -2
  170. package/dist/Errors.d.mts +0 -158
  171. package/dist/Errors.mjs +0 -207
  172. package/dist/Errors.mjs.map +0 -1
  173. package/dist/Images-yCG9BmRU.mjs +0 -76
  174. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  175. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  176. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  177. package/dist/PageControl.d.mts +0 -15
  178. package/dist/PageControl.mjs +0 -34
  179. package/dist/PageControl.mjs.map +0 -1
  180. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  181. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  182. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  183. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  184. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  185. package/dist/PlanData.d.mts +0 -3
  186. package/dist/PlanData.mjs +0 -268
  187. package/dist/PlanData.mjs.map +0 -1
  188. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  189. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  190. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  191. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  192. package/dist/Testing.d.mts +0 -479
  193. package/dist/Testing.mjs +0 -2147
  194. package/dist/Testing.mjs.map +0 -1
  195. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  196. package/dist/Timeline.d.mts +0 -4
  197. package/dist/Timeline.mjs +0 -20
  198. package/dist/Timeline.mjs.map +0 -1
  199. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  200. package/dist/TimelineData.d.mts +0 -3
  201. package/dist/TimelineData.mjs +0 -482
  202. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The page implementation behind `Page.Page`. `Browser` constructs pages and owns the state they
3
+ * share (the clock mapping and event publication), so construction stays internal. A page is
4
+ * assembled here from its domains: the script bridge, pictures, reading, its timeline, input and
5
+ * navigation.
6
+ * What a presenter and a stage need of a page, and no caller should, it keeps out of sight.
7
+ */
8
+ import { type Effect as Eff, Effect, Option } from "effect";
9
+
10
+ import type { BrowserError } from "../../BrowserError.ts";
11
+ import { type Page, State } from "../../Page.ts";
12
+ import * as Actions from "../input/actions.ts";
13
+ import * as Pictures from "../pictures/pictures.ts";
14
+ import * as Reading from "../reading/reading.ts";
15
+ import * as Ready from "../reading/ready.ts";
16
+ import * as Changes from "../timeline/changes.ts";
17
+ import * as Window from "../timeline/window.ts";
18
+ import * as Bridge from "./bridge.ts";
19
+ import * as Context from "./context.ts";
20
+ import * as Navigation from "./navigation.ts";
21
+ import * as Url from "./url.ts";
22
+ import * as Viewport from "./viewport.ts";
23
+
24
+ export interface Internals {
25
+ /** The page's input, in any style. */
26
+ readonly input: Actions.Input;
27
+ /** Completes once the input asked of the page so far has ended, failing `Busy` if it lasts. */
28
+ readonly inputEnded: Eff.Effect<void, BrowserError>;
29
+ /** The page's documents so far, counted as `Navigated` counts them. */
30
+ readonly document: () => number;
31
+ /** Wait, in one call, until what a query finds is there, is not, or shows something else. */
32
+ readonly until: Reading.Reading["until"];
33
+ }
34
+
35
+ /** A page this library built: it keeps its internals where only this module reaches them. */
36
+ class Built {
37
+ readonly #internals: Internals;
38
+
39
+ constructor(internals: Internals) {
40
+ this.#internals = internals;
41
+ }
42
+
43
+ static internalsOf(page: Page): Internals | undefined {
44
+ return page instanceof Built ? page.#internals : undefined;
45
+ }
46
+ }
47
+
48
+ /** What a page this library built keeps out of sight; nothing for any other `Page`. */
49
+ export const internalsOf = (page: Page) => Built.internalsOf(page);
50
+
51
+ /** `view`, made from `page` with some of its operations replaced, keeping what `page` keeps out of sight. */
52
+ export const viewOf = <A extends Page>(page: Page, view: A): A => {
53
+ const internals = internalsOf(page);
54
+
55
+ return internals === undefined ? view : Object.assign(new Built(internals), view);
56
+ };
57
+
58
+ export const make = Effect.fnUntraced(function* (options: Context.MakeOptions) {
59
+ const page = Context.make(options, yield* Effect.scope);
60
+ const { id, playwright, native, owned, within, lane, span } = page;
61
+ const bridge = yield* Bridge.make(page);
62
+ const viewport = Viewport.make(page, bridge);
63
+ const input = Actions.make(page, bridge, viewport);
64
+ const pictures = yield* Pictures.make(page, bridge, viewport);
65
+ const reading = yield* Reading.make(page, bridge);
66
+ const navigation = Navigation.make(page, input.perform, input.preparePolicy);
67
+ const { capture } = pictures;
68
+ const changes = Changes.make(page, bridge, pictures.estimate);
69
+ const titles = lane.shared<string>("title", true);
70
+ const viewports = lane.shared<Effect.Success<Page["viewport"]>>("viewport", true);
71
+
72
+ // What the page's parts already know, asking it nothing: its documents as its own session saw
73
+ // them, the newest frame, and the viewport's text as last read since the document began.
74
+ const state = Effect.map(capture.latest, (latest) => {
75
+ const { document, url } = bridge.frameTag();
76
+ const committedAt = page.activity.documentAt;
77
+
78
+ return new State({
79
+ page: id,
80
+ at: page.now(),
81
+ url,
82
+ document,
83
+ committedAt: Number.isFinite(committedAt) ? committedAt : undefined,
84
+ load: bridge.loaded(),
85
+ frame: Option.getOrUndefined(latest),
86
+ text: reading.viewedSince(committedAt),
87
+ });
88
+ });
89
+
90
+ const assembled: Page = {
91
+ id,
92
+ playwright,
93
+ url: Effect.sync(() => Url.redact(playwright.url())),
94
+ title: titles("", navigation.title.pipe(within("title"))).pipe(span("Page.title"), owned),
95
+ goto: navigation.goto,
96
+ back: navigation.back,
97
+ reload: navigation.reload,
98
+ bringToFront: native("bringToFront", () => playwright.bringToFront()),
99
+ // Playwright answers once the page has closed, and at once for one already closed.
100
+ close: native("close", () => playwright.close()),
101
+ snapshot: reading.snapshot,
102
+ screenshot: pictures.screenshot,
103
+ frame: pictures.frame,
104
+ zoom: pictures.zoom,
105
+ viewport: viewports("", viewport.viewportFor("viewport").pipe(within("viewport"))).pipe(
106
+ span("Page.viewport"),
107
+ owned,
108
+ ),
109
+ find: reading.find,
110
+ text: reading.text,
111
+ changes,
112
+ window: Window.make(page, changes, capture.recent),
113
+ state,
114
+ click: (target, options) => input.click(target, options),
115
+ hover: (target) => input.hover(target),
116
+ drag: (from, to) => input.drag(from, to),
117
+ type: (text, options) => input.type(text, options),
118
+ press: (keys, options) => input.press(keys, options),
119
+ scroll: (options) => input.scroll(options),
120
+ select: input.select,
121
+ waitFor: reading.waitFor,
122
+ ready: Ready.make(page, bridge, capture),
123
+ screencast: capture.stream,
124
+ captureStats: pictures.captureStats,
125
+ recentFrames: capture.recent,
126
+ recentEvents: options.recentEvents,
127
+ };
128
+
129
+ const internals: Internals = {
130
+ input,
131
+ inputEnded: lane.read("inputEnded")(Effect.void).pipe(owned),
132
+ document: () => bridge.frameTag().document,
133
+ until: reading.until,
134
+ };
135
+
136
+ return Object.assign(new Built(internals), assembled);
137
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Addresses: reading one the way a person types it, for `Page.goto` and the browser tools alike,
3
+ * and reporting one without the values that are credentials, by one rule that the page script runs
4
+ * too, for the outline's links.
5
+ */
6
+
7
+ const scheme = /^[a-z][a-z0-9+.-]*:/i;
8
+
9
+ // `localhost:3000` and `example.com:8080/a` look like a scheme to the URL parser; a port decides.
10
+ const hostAndPort = /^[^\s/?#@:]+:\d+(?:[/?#]|$)/;
11
+
12
+ const loopback = /^(?:localhost|127(?:\.\d+){3}|\[::1\])(?:[:/?#]|$)/i;
13
+
14
+ /** The URL `input` names. Without a scheme it is a web address: HTTPS, or HTTP on loopback. */
15
+ export const parse = (input: string): URL | null => {
16
+ const text = input.trim();
17
+
18
+ if (scheme.test(text) && !hostAndPort.test(text)) return URL.parse(text);
19
+
20
+ return URL.parse(`${loopback.test(text) ? "http" : "https"}://${text}`);
21
+ };
22
+
23
+ /**
24
+ * The rule that reports an address without its credentials. The page script runs it too, so it
25
+ * stays self-contained, as `names.inpage.ts` says a page-side part must.
26
+ */
27
+ export const addresses = () => {
28
+ // What a credential's value reads as, as a secret field's text does (`Page.redacted`).
29
+ const withheld = "••••••••" as const;
30
+
31
+ // A parameter's name, compared in lowercase without separators, says it is a credential: a token,
32
+ // secret, password, signature, assertion, session id or one-time code, or a code or key of a kind
33
+ // that signs someone in or signs a request, such as `verification_code` or `api_key`.
34
+ const credential =
35
+ /token|secret|passw|passcode|passphrase|signature|credential|assertion|verifier|bearer|jwt|hmac|apikey|accesskey|privatekey|sess(?:ion)?id|saml(?:response|art)|(?:sig|otp)$|(?:auth|api|access|private|secret|signing|client|encryption|licen[cs]e|session|master|app|device|verification|verify|confirmation|confirm|activation|activate|reset|recovery|login|security|magic|onetime|mfa|2fa|sms|email|otp|oob)(?:code|key)$|^(?:auth|authorization|pwd|pass)$/;
36
+
37
+ // These names as often say what an address is about, such as a stock code, a support ticket or a
38
+ // conference session. Under them a value is a credential when it looks generated, at least 16
39
+ // characters with letters and digits, as tokens, session ids and authorization codes do.
40
+ const ambiguous = /^(?:code|key|session|sid|ticket)$/;
41
+
42
+ const secret = (name: string, value: string) => {
43
+ const bare = name.toLowerCase().replace(/[^a-z0-9]/g, "");
44
+
45
+ return (
46
+ value !== "" &&
47
+ (credential.test(bare) ||
48
+ (ambiguous.test(bare) && value.length >= 16 && /\d/.test(value) && /[a-z]/i.test(value)))
49
+ );
50
+ };
51
+
52
+ // Parameters as written, each credential's value withheld: a query's, a fragment's written as a
53
+ // query, or a path's segments', as a Java session id is.
54
+ const masked = (parameters: string, separator: string) =>
55
+ parameters
56
+ .split(separator)
57
+ .map((parameter) => {
58
+ const [name = "", value = ""] = [...new URLSearchParams(parameter)][0] ?? [];
59
+
60
+ return secret(name, value)
61
+ ? `${parameter.slice(0, parameter.indexOf("=") + 1)}${withheld}`
62
+ : parameter;
63
+ })
64
+ .join(separator);
65
+
66
+ /**
67
+ * `url` as the library reports it: without userinfo, and with each credential's value withheld,
68
+ * so the rest keeps its identity, such as a chart's `?ticker=ETH`. An address without a host,
69
+ * such as `data:` or `about:blank`, is content or a fixed name, and stays as it is.
70
+ */
71
+ const redact = (url: string): string => {
72
+ const parsed = URL.parse(url);
73
+
74
+ if (parsed === null || parsed.host === "") return url;
75
+ parsed.username = "";
76
+ parsed.password = "";
77
+ parsed.pathname = masked(parsed.pathname, ";");
78
+ parsed.search = masked(parsed.search.slice(1), "&");
79
+ if (parsed.hash.includes("=")) {
80
+ const fragment = parsed.hash.slice(1);
81
+ const question = fragment.indexOf("?");
82
+ // A route before the parameters, as in `#/inbox?token=…`, stays.
83
+ const start = question >= 0 && question < fragment.indexOf("=") ? question + 1 : 0;
84
+
85
+ parsed.hash = fragment.slice(0, start) + masked(fragment.slice(start), "&");
86
+ }
87
+
88
+ return parsed.href.replaceAll(encodeURIComponent(withheld), withheld);
89
+ };
90
+
91
+ return { withheld, redact };
92
+ };
93
+
94
+ export const { withheld, redact } = addresses();
95
+
96
+ /** An address inside text, as a failure's message quotes one. */
97
+ export const quoted = /[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi;
98
+
99
+ /** `text` with each address in it redacted. */
100
+ export const redactWithin = (text: string) => text.replace(quoted, (url) => redact(url));
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The viewport's size in CSS pixels. Playwright knows it only for a context it created; over CDP it
3
+ * reports none, so the page answers, and so does a picture taken at one device pixel per CSS
4
+ * pixel. Pointer bounds, page scrolls, crops, capture size and frame reuse all read this one
5
+ * source; the latest answer serves checks that cannot wait for the page.
6
+ */
7
+ import { Effect } from "effect";
8
+
9
+ import { ViewportResultSchema } from "../reading/outline.inpage.ts";
10
+ import { type Bridge, scriptCall } from "./bridge.ts";
11
+ import { decodeWith, type PageContext } from "./context.ts";
12
+
13
+ export const make = (page: PageContext, bridge: Bridge) => {
14
+ const { playwright } = page;
15
+ let measuredViewport: { readonly width: number; readonly height: number } | null = null;
16
+
17
+ const knownViewport = () => playwright.viewportSize() ?? measuredViewport;
18
+
19
+ const viewportFor = (operation: string) =>
20
+ Effect.suspend(() => {
21
+ const known = playwright.viewportSize();
22
+
23
+ return known === null
24
+ ? bridge.evaluate(operation, scriptCall("viewport")).pipe(
25
+ Effect.flatMap(decodeWith(operation, ViewportResultSchema)),
26
+ Effect.tap((measured) =>
27
+ Effect.sync(() => {
28
+ measuredViewport = measured;
29
+ }),
30
+ ),
31
+ )
32
+ : Effect.succeed(known);
33
+ });
34
+
35
+ /** The size of an unscaled picture of the whole viewport, which is the viewport's own. */
36
+ const remember = (size: { readonly width: number; readonly height: number }) => {
37
+ measuredViewport = { width: size.width, height: size.height };
38
+ };
39
+
40
+ return { knownViewport, viewportFor, remember };
41
+ };
42
+
43
+ export type Viewport = ReturnType<typeof make>;