Skip to content

Install the JS bridge at document start on every desktop backend - #61

Merged
kdroidFilter merged 2 commits into
mainfrom
fix/desktop-bridge-document-start
Sep 3, 2026
Merged

Install the JS bridge at document start on every desktop backend#61
kdroidFilter merged 2 commits into
mainfrom
fix/desktop-bridge-document-start

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Fixes #60.

Problem

The desktop window.kmpJsBridge object was injected from Compose after load (WebView.kt), gated on a LoadingState.Finished / lastLoadedUrl change — and on desktop both signals come from a 120 ms poller, not from real navigation callbacks like Android (onPageFinished) and iOS (nav delegate).

Two failure modes followed:

  • Race. Finished is a data object, so snapshotFlow conflates it. A load that starts and finishes inside one tick (in-memory HTML, data: URLs, cached pages) or that keeps the same document URL (loadHtml without baseUrl stays on about:blank) produced no emission → the bridge was never re-injected into the new document.
  • Availability window. Even when it worked, the bridge only appeared after load, so the page's own startup code (DOMContentLoaded, framework boot) saw undefined.

Windows already worked around this with a document-start shim that hardcoded kmpJsBridge; macOS and Linux only got the window.ipc transport shim.

The existing suite could not catch any of this: every load used a unique baseUrl (https://suite.local/<marker>), which always changes lastLoadedUrl.

Fix

  • The JS half of the bridge now has a single source of truth: jsBridgeObjectScript(name, postMessageBody) in commonMain, used both by IWebView.injectJsBridge() and by the desktop factory.
  • WebViewFactoryParam carries the jsBridgeName; the desktop factory builds the bootstrap and passes it to nativeCreate, which installs it as a document-start user script on all three backends (WKUserScript, webkit_user_content_manager_add_script, AddScriptToExecuteOnDocumentCreated), all frames.
  • Windows no longer hardcodes the name, so a custom WebViewJsBridge(jsBridgeName = …) now works on every desktop backend.
  • Post-load injection stays as a harmless idempotent fallback (guarded on typeof window.<name> === 'undefined').
  • README: the bridge section said "Desktop (Linux WebKit)", predating the macOS/Windows backends — corrected, and it now states the document-start behaviour.

Tests

New suite cases, both red against the previous behaviour and green with the fix (verified locally on macOS by rebuilding the dylib with the injection neutralised):

case covers
B10 bridge callable from an inline script, i.e. while the document is still parsing
B11 bridge survives 3 successive loads that keep the same URL (baseUrl = null)

Before: passed=68 failed=2B10: bridge absent while the document was parsing, B11: timed out.
After: passed=70 failed=0 allGreen=true.

Plus 3 unit tests for jsBridgeObjectScript (custom name, postMessage routing, callback contract) in the shared commonTest suite.

CI runs the visual suite on Linux (WebKit2GTK), Windows (WebView2) and macOS (WKWebView) with freshly built natives — the JNI signature changed, so all three are rebuilt from source here.

The desktop bridge object was injected from Compose after load, gated on a
LoadingState/lastLoadedUrl change derived from a 120 ms poller. A navigation
that starts and finishes inside one tick, or that keeps the same document URL
(loadHtml without baseUrl stays on about:blank), produced no emission, so
window.kmpJsBridge was never re-injected. Page startup scripts never saw it
either, since injection only happened once the load had finished. Windows
already worked around this with a hardcoded document-start shim.

The JS half of the bridge now lives in one place (jsBridgeObjectScript) and is
handed to nativeCreate, which installs it as a document-start user script on
WebKit2GTK, WKWebView and WebView2 — so it exists before the first page
statement of every document, honours a custom jsBridgeName on all three
backends, and no longer depends on poller timing.

Suite cases B10/B11 cover it: calling the bridge from an inline script, and
three successive loads that keep the same URL. Both fail against the previous
behaviour (verified on macOS) and pass with the fix (70/70).

Fixes #60
Android / iOS / WasmJs still inject the bridge after load, so B10/B11 report
Skipped there instead of Failed — the catalog stays identical across
platforms and the matrix stays honest.
@kdroidFilter
kdroidFilter merged commit 7e066ba into main Sep 3, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No kmpJsBridge on Mac? (Tao backend)

1 participant