// `PageLike` over a browser page. The adapter itself: `@ultimat3/testing` declares the surface an // e2e test drives and `@ultimat3/scraping` owns the only driver that can drive one, and neither // may import the other — so the join is here, in the one package allowed to know about both. import { finiteCount } from '@ultimat3/core'; import { evaluateClosure } from './e2e-evaluate'; import { e2eLocator } from './e2e-locator'; import type { E2eSelection } from './e2e-selection'; import { callBudget, waitForServiceWorker } from './e2e-service-worker'; import type { LocatorLike, PageLike } from './test-types'; /** * What this adapter needs of a browser: four required members, every one of them on `ScrapePage`. * Declared structurally rather than as `ScrapePage` so a test can stand one up in six lines — the * same bargain `cdp-port.ts` makes about puppeteer, one layer up. */ export interface E2eBrowserPage { url(): string; goto(url: string, options?: { readonly timeout?: number | undefined }): Promise; evaluate(expression: string): Promise; click(selector: string): Promise; /** * The browser's own network condition, which `E2eFixtures.offline()`/`online()` forward to. * `ScrapePage` has it (`page-over-target.ts`), reaching `CdpPageLike.setOfflineMode` through * `cdp-target.ts`'s guard. * * OPTIONAL for the reason the four above are structural: this port is the shape of somebody * ELSE's object, and requiring it would cost every six-line double a type error for a capability * a test that never goes offline does not need. Absent, `e2e-driver.ts` keeps refusing by name — * a coded refusal, never a silent no-op, because an `offline()` that did nothing would let the * app's ONLINE page pass an offline test. */ offline?(enabled: boolean): Promise; } export interface E2ePageOptions { readonly page: E2eBrowserPage; /** Every `goto('/feed')` in an e2e suite is app-relative; this is what makes one absolute. */ readonly baseUrl: string; /** Per-navigation deadline, handed to the driver rather than enforced here. */ readonly timeoutMs?: number | undefined; /** * How long `waitForServiceWorker()` waits before refusing, the handover reload included. Bounded * IN THE PAGE. One test raises its own with `waitForServiceWorker({ timeoutMs })`. */ readonly serviceWorkerTimeoutMs?: number | undefined; } /** Named once, so both refusals below name the call an app's test preload actually makes. */ const SUBJECT = 'installE2eDriver'; export const DEFAULT_E2E_TIMEOUT_MS = 30_000; export const DEFAULT_SERVICE_WORKER_TIMEOUT_MS = 10_000; /** Every in-page expression here answers JSON text, for the reason `cdp-snapshot.ts` states. */ const readField = (raw: unknown, key: string): unknown => { const decoded = typeof raw === 'string' ? (JSON.parse(raw) as unknown) : raw; return typeof decoded === 'object' && decoded !== null ? (decoded as Record)[key] : undefined; }; const TITLE = '(() => JSON.stringify({ title: document.title }))()'; /** * Where the server's SECOND flush begins: every hole a streamed page reveals is written as one * chunk opening with this tag (`revealChunk` in `@ultimat3/render`'s `render-stream.ts`, which the * barrel does not export). Everything before it is the first flush — the head and the shell. */ export const REVEAL_MARK = '