feat(tauri): support child webview handles - #532
Conversation
Greptile SummaryThis PR adds child
Confidence Score: 5/5Safe to merge; the child webview feature is well-scoped to macOS and the embedded provider, with non-macOS paths unchanged in behavior. The
|
| Filename | Overview |
|---|---|
| packages/tauri-plugin-webdriver/src/platform/executor.rs | Central refactor: window()→webview(), child-relative get_window_rect, and split set_window_rect for primary vs. child paths. Child path skips the fullscreen/maximized-exit guard that the primary path performs. |
| packages/tauri-plugin-webdriver/src/server/mod.rs | Switches window enumeration from webview_windows() to webviews() with an is_webview_exposed filter; adds well-tested platform predicates for child exposure and close protection; includes unit tests for all platform combinations. |
| packages/tauri-plugin-webdriver/src/server/handlers/window.rs | Adds no_such_window guard to get_window_handle, routes closeWindow through is_standalone_webview/close_protection_is_required, and uses session.switch_to_window() to atomically advance the browsing context generation. |
| packages/tauri-plugin-webdriver/src/server/handlers/frame.rs | Refactors frame context updates to use commit_frame_context with generation check, preventing stale commits after concurrent window switches; adds comprehensive unit tests; uses .expect() for an element-frame invariant that would panic the server if violated. |
| packages/tauri-plugin-webdriver/src/webdriver/session.rs | Adds browsing_context_generation counter and switch_to_window method that atomically advances the generation, clears frame context, and returns an error on overflow; well-tested. |
| packages/tauri-service/src/service.ts | Adds afterCommand hook and pendingExternalWindowSwitches tracking to suppress focus recovery and update the window label only after a switchToWindow succeeds; handles concurrent switches to the same/different handles and session isolation correctly. |
| packages/tauri-service/src/window.ts | Introduces AsyncLocalStorage-based internalWindowSwitchContext to mark service-internal focus-recovery switches, preventing them from being recorded as explicit user window selections. |
| e2e/test/tauri/child-webview.spec.ts | New E2E suite covering child lifecycle, sibling switching, frame context reset, bound semantics, close rejection, handle removal, and session isolation; gated to macOS + embedded provider. |
| fixtures/e2e-apps/tauri/src-tauri/src/main.rs | Adds three Tauri commands (create_child_webview, remove_child_webview, create_disposable_webview_window) and conditionally disables the automation plugin when using the embedded server. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Test as E2E Test
participant Svc as TauriWorkerService
participant WD as WebDriver Server (Rust)
participant App as Tauri AppState
Test->>Svc: beforeCommand("switchToWindow", ["child-left"])
Note over Svc: pendingExternalWindowSwitches["session"]["child-left"] = 1
Svc->>WD: "POST /session/{id}/window {handle:"child-left"}"
WD->>App: has_window_label("child-left")
App->>App: app.webviews().filter(is_webview_exposed)
App-->>WD: true (macOS: child exposed)
WD->>WD: session.switch_to_window("child-left") browsing_context_generation++ frame_context.clear()
WD-->>Svc: null (success)
Svc->>Svc: afterCommand("switchToWindow", ["child-left"], null)
Note over Svc: pendingCount 1→0 suppressActiveWindowFocus() setCurrentWindowLabel("child-left")
Test->>Svc: "browser.execute(() => document.title)"
Svc->>WD: "POST /session/{id}/execute/sync {window_label:"child-left"}"
WD->>App: get_executor_for_window("child-left", ...)
App-->>WD: "MacOSExecutor { webview: child-left }"
WD->>WD: webview.with_webview(...evaluate JS in child WKWebView...)
WD-->>Test: "Child WebView child-left"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Test as E2E Test
participant Svc as TauriWorkerService
participant WD as WebDriver Server (Rust)
participant App as Tauri AppState
Test->>Svc: beforeCommand("switchToWindow", ["child-left"])
Note over Svc: pendingExternalWindowSwitches["session"]["child-left"] = 1
Svc->>WD: "POST /session/{id}/window {handle:"child-left"}"
WD->>App: has_window_label("child-left")
App->>App: app.webviews().filter(is_webview_exposed)
App-->>WD: true (macOS: child exposed)
WD->>WD: session.switch_to_window("child-left") browsing_context_generation++ frame_context.clear()
WD-->>Svc: null (success)
Svc->>Svc: afterCommand("switchToWindow", ["child-left"], null)
Note over Svc: pendingCount 1→0 suppressActiveWindowFocus() setCurrentWindowLabel("child-left")
Test->>Svc: "browser.execute(() => document.title)"
Svc->>WD: "POST /session/{id}/execute/sync {window_label:"child-left"}"
WD->>App: get_executor_for_window("child-left", ...)
App-->>WD: "MacOSExecutor { webview: child-left }"
WD->>WD: webview.with_webview(...evaluate JS in child WKWebView...)
WD-->>Test: "Child WebView child-left"
Reviews (3): Last reviewed commit: "fix(tauri): preserve non-macos window cl..." | Re-trigger Greptile
| #[cfg(desktop)] | ||
| #[allow(dead_code)] | ||
| pub(crate) fn is_standalone_webview<R: Runtime>(webview: &Webview<R>) -> bool { |
There was a problem hiding this comment.
is_standalone_webview is exported as pub(crate) and is imported and called in window.rs on every desktop target, so it is not dead code. The #[allow(dead_code)] is spurious and will silence Rust's dead-code lint if the call-site is ever removed, making the stale helper harder to notice.
| #[cfg(desktop)] | |
| #[allow(dead_code)] | |
| pub(crate) fn is_standalone_webview<R: Runtime>(webview: &Webview<R>) -> bool { | |
| #[cfg(desktop)] | |
| pub(crate) fn is_standalone_webview<R: Runtime>(webview: &Webview<R>) -> bool { |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Removed the redundant allow(dead_code) in 15f862a. The helper remains used by the desktop close handler.
| fn child_webviews_are_exposed() -> bool { | ||
| child_webviews_are_exposed_for_platform(std::env::consts::OS) | ||
| } | ||
|
|
||
| fn child_webviews_are_exposed_for_platform(platform: &str) -> bool { | ||
| platform == "macos" | ||
| } |
There was a problem hiding this comment.
Platform string evaluated at call time on every request
child_webviews_are_exposed() calls std::env::consts::OS (a static string) on every get_webview / get_window_labels invocation rather than computing the result once. The call is cheap, but a static or OnceLock would make the intent ("this is a compile-time platform constant") explicit and avoid the repeated string comparison under any future hot path. Leaving as-is is fine; just worth noting.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Kept this as a direct platform predicate. std::env::consts::OS is a compile-time static string, not a runtime environment lookup, and the pure helper keeps all target policies testable without adding mutable or static state. This path is limited to handle lookup and enumeration rather than a hot loop.
|
Addressed the review follow-ups in 15f862a: removed the two accidental root-level development logs, removed the redundant dead-code suppression, and documented why maximize/fullscreen return the host outer rect while getWindowRect remains child-relative. Rust unit tests, clippy, targeted rustfmt, and the Node 24 pre-push suite passed. The external macOS multi-child acceptance was re-pinned to this exact SHA and passed three consecutive runs. |
|
The non-macOS close behavior is fixed in c59baec. close_window now applies the standalone guard only through the shared macOS child-exposure policy. Windows and Linux continue to expose only primary handles and retain their prior primary close behavior even if native child webviews exist. Cargo tests, clippy, macOS embedded E2E, and the Node 24 pre-push suite passed. The external multi-child acceptance was re-pinned to this SHA and passed three consecutive runs. |
|
Follow-up on the two spots noted by Greptile: no source change is needed. The element-frame expect is an internal invariant: FrameId::Element and Some(js_var_for_element) are created together in the same request-parse match before the await, and the later code only moves that local value; client input cannot create the mismatched state. For child setWindowRect, the operation intentionally changes only the embedded Webview bounds. Exiting host fullscreen or maximized state there would incorrectly turn a child-local rectangle command into a host-window state mutation. Primary handles retain the existing host-state reset behavior. |
Summary
Webviewlabels as standard WebDriver handlescloseWindowfor child and shared-host primary webviews while retaining standalone close behaviorPlatform scope
Runtime child-webview support is validated on macOS only. Other targets continue
to expose primary webviews while their existing executor paths remain supported.
Verification
cargo clippy --manifest-path packages/tauri-plugin-webdriver/Cargo.toml --all-targets -- -D warningscargo test --manifest-path packages/tauri-plugin-webdriver/Cargo.tomlpnpm --filter @wdio/tauri-service testpnpm --filter @wdio/tauri-service typecheckpnpm lintpnpm format:checka40d85f589b3a982f852305f8411eb824f57368fcargo fmt --checkstill reports existing formatting drift in unchanged upstreamfiles
src/server/handlers/actions.rsandsrc/server/handlers/direct_eval.rs.