From ce5ab9bcf6280f7ab21115babf9c056cba794ea4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 13 May 2026 23:15:12 +0000 Subject: [PATCH 01/14] webview: navigate/reload/goBack/goForward accept {waitUntil, timeout} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit navigate() (and reload/goBack/goForward) previously settled only on Page.loadEventFired. A page that holds a subresource connection open (SSE, long-poll, a hung image) never fires `load`, so `await view.navigate()` hung forever. Now accepts Playwright-shaped options: await view.navigate(url, { waitUntil: "load" | "domcontentloaded", // default "load" timeout: 30000, // ms; 0 disables }); Chrome backend: Page.setLifecycleEventsEnabled is sent in the attach chain; Page.frameNavigated stashes the main frame id + loaderId; the Page.lifecycleEvent handler settles the Navigate slot when frameId/loaderId match and name == DOMContentLoaded (or load). Subframe events and the about:blank replay never match because m_frameId/m_loaderId are only set after the user URL commits. Timeout is a parent-side RunLoop::dispatchAfter timer. It captures a per-view navigation generation counter; settleSlot bumps the counter, so a late fire no-ops instead of rejecting the next navigation. No explicit cancel, no RefPtr lifetime dance. WebKit backend: parses the same options. `timeout` applies (same parent-side timer). WKNavigationDelegate has no DOMContentLoaded hook, so `domcontentloaded` degrades to `load` there — noted in the type docs. Tests: new webview-navigate-options.test.ts drives a mock CDP WebSocket server so the full lifecycleEvent path is exercised without a Chrome binary; webview-chrome.test.ts gets real-Chrome coverage (todo-gated where Chrome is absent). --- packages/bun-types/bun.d.ts | 42 ++- src/runtime/webview/ChromeBackend.cpp | 87 ++++- src/runtime/webview/JSWebView.cpp | 99 +++++- src/runtime/webview/JSWebView.h | 41 ++- src/runtime/webview/JSWebViewConstructor.cpp | 4 +- src/runtime/webview/JSWebViewPrototype.cpp | 74 ++++- test/js/bun/webview/webview-chrome.test.ts | 184 ++++++++++ .../webview/webview-navigate-options.test.ts | 314 ++++++++++++++++++ 8 files changed, 811 insertions(+), 34 deletions(-) create mode 100644 test/js/bun/webview/webview-navigate-options.test.ts diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 37d9bc45fcf..1c7f4d26de6 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -8478,6 +8478,30 @@ declare module "bun" { modifiers?: Modifier[]; } + interface NavigateOptions { + /** + * When to consider the navigation finished: + * + * - `"load"` — wait for the window `load` event (all subresources + * finished). Matches Playwright's default. + * - `"domcontentloaded"` — wait for `DOMContentLoaded`. Use this for + * pages that hold a connection open (SSE, long-polling, a hung + * subresource) and so never fire `load`. + * + * With the Chrome backend this subscribes to CDP + * `Page.lifecycleEvent`. The WebKit backend has no separate + * DOMContentLoaded delegate hook, so `"domcontentloaded"` behaves + * like `"load"` there — use `timeout` to bound the wait instead. + * @default "load" + */ + waitUntil?: "load" | "domcontentloaded"; + /** + * Maximum time to wait in milliseconds. `0` disables the timeout. + * @default 30000 + */ + timeout?: number; + } + /** * Browser backend selection. * @@ -8704,16 +8728,22 @@ declare module "bun" { onNavigationFailed: ((error: Error) => void) | null; /** - * Navigate to a URL. Resolves when the main frame's load completes - * (WKNavigationDelegate `didFinishNavigation`). + * Navigate to a URL. Resolves when the navigation reaches the + * `options.waitUntil` milestone (default: the window `load` event), + * or rejects after `options.timeout` ms. * * @example * ```ts * await view.navigate("https://example.com"); * await view.navigate("data:text/html,