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
|
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. |
|
Thanks for the PR, but I don't want to pull So before the detail: is there a concrete use case here? There's no issue behind it, and Tauri's multi-webview is itself behind their unstable flag. If there is one, we can consider this again. Separately, I've split the parts that stand on their own into #559 and #560, credited to you. |
Split out of #532, which fixes this as part of child-webview support. The bug is independent of that feature and present on main today. Only `browser.tauri.switchWindow()` marked the session as user-switched, so a standard W3C `browser.switchToWindow(handle)` left automatic focus recovery armed — the next getTitle/$/elementClick could quietly switch the user back to whichever window the app considered active, undoing an explicit selection. Suppress on the same terms as the Tauri-specific command, in afterCommand so a failed switch does not count. Recovery issues `browser.switchToWindow` itself (switchToWindowByTitle), so those calls are marked internal via AsyncLocalStorage — otherwise the first recovery would look like user intent and permanently disable recovery for the session. Scoped deliberately: #532's pending-switch map and the setCurrentWindowLabel commit stay with the feature, since treating a handle as a Tauri window label is child-webview specific. Claude-Session: https://claude.ai/code/session_01CsgeivZCqduTQmWK55zqiT Co-authored-by: abu0306 <abu0306@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split out of #532, which fixes this as a side effect of child-webview support. The bug is independent of that feature and present on main today. `switch_to_window` set `current_window` and left `frame_context` untouched. W3C Switch To Window selects the target window's *top-level* browsing context, but because the stack survived, `wrap_script_for_frame_context` kept wrapping every subsequent command to descend `doc.querySelectorAll('iframe, frame')[i]` — so after switching out of a frame, scripts and element lookups in the new window walked into whatever sat at that index there. Nothing in the tauri e2e suite exercises switchToFrame, which is why it went unnoticed. `Session::switch_to_window` now advances a browsing-context generation and clears the frame stack in one step, and frame commits are validated against that generation: a switch landing during the executor's frame validation no longer pushes a frame onto the wrong window. Also refreshes the stale fixture Cargo.lock (it still pinned the plugin crates at 1.0.0 while their manifests say 1.2.0, so any fixture build dirtied it). Claude-Session: https://claude.ai/code/session_01CsgeivZCqduTQmWK55zqiT Co-authored-by: abu0306 <abu0306@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.