Skip to content

feat(tauri): support child webview handles - #532

Open
abu0306 wants to merge 17 commits into
webdriverio:mainfrom
AtomInnoLab:feat/tauri-child-webview-handles
Open

feat(tauri): support child webview handles#532
abu0306 wants to merge 17 commits into
webdriverio:mainfrom
AtomInnoLab:feat/tauri-child-webview-handles

Conversation

@abu0306

@abu0306 abu0306 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expose managed Tauri child Webview labels as standard WebDriver handles
  • execute page commands against the selected webview while preserving shared host-window behavior
  • preserve primary outer-window and child-relative rectangle semantics using public label topology
  • reject closeWindow for child and shared-host primary webviews while retaining standalone close behavior
  • preserve explicit user handle selection across automatic focus recovery
  • add generic macOS embedded-provider fixture and E2E coverage for child lifecycle, frames, and session switching

Platform 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 warnings
  • cargo test --manifest-path packages/tauri-plugin-webdriver/Cargo.toml
  • pnpm --filter @wdio/tauri-service test
  • pnpm --filter @wdio/tauri-service typecheck
  • pnpm lint
  • pnpm format:check
  • local macOS embedded Tauri scenarios, including child handles, frames, lifecycle removal, rectangles, close safety, and explicit switching
  • three consecutive external multi-child application probes against immutable revision a40d85f589b3a982f852305f8411eb824f57368f

cargo fmt --check still reports existing formatting drift in unchanged upstream
files src/server/handlers/actions.rs and src/server/handlers/direct_eval.rs.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Jul 15, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds child Webview handle support to the embedded Tauri WebDriver provider, exposing Tauri child webviews as standard WebDriver window handles on macOS. The implementation refactors the PlatformExecutor trait to operate on Webview (instead of WebviewWindow), routes page and element commands to the selected child webview, preserves host-window semantics for maximize/minimize/fullscreen, and blocks closeWindow for child and shared-host primary webviews. The TypeScript service layer gains before/afterCommand hooks to track explicit user switchToWindow calls and suppress automatic focus recovery without overriding the user's explicit selection.

  • Rust side: AppState::get_window_labels and get_webview are switched from app.webview_windows() to app.webviews(), filtered by is_webview_exposed to expose child webviews only on macOS; child-relative bounds are implemented in get_window_rect/set_window_rect, and a browsing_context_generation counter in Session prevents stale frame context commits across concurrent window switches.
  • TypeScript side: pendingExternalWindowSwitches tracks in-flight switchToWindow calls by session and handle so that setCurrentWindowLabel and suppressActiveWindowFocus are only committed when the command succeeds; AsyncLocalStorage is used to mark internal focus-recovery switches so they are not treated as explicit user selections.

Confidence Score: 5/5

Safe to merge; the child webview feature is well-scoped to macOS and the embedded provider, with non-macOS paths unchanged in behavior.

The Webview-level refactor is mechanical and consistent across all five platform executors. The new child webview exposure, close protection, frame context generation, and switchToWindow tracking all include unit tests that cover edge cases (overflow, concurrent handles, session isolation). The one structural asymmetry — set_window_rect skipping the fullscreen/maximized guard for child webviews — is observable only when a child handle is selected while the host is in a managed window state, and doesn't affect correctness for the primary use-case.

executor.rs (child set_window_rect path) and frame.rs (expect on element frame invariant) are the two spots worth a second look before merging.

Important Files Changed

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"
Loading
%%{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"
Loading

Reviews (3): Last reviewed commit: "fix(tauri): preserve non-macos window cl..." | Re-trigger Greptile

Comment thread packages/tauri-plugin-webdriver/src/platform/executor.rs
Comment thread packages/tauri-plugin-webdriver/src/server/mod.rs
@abu0306

abu0306 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

@abu0306

abu0306 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

@abu0306

abu0306 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

@goosewobbler

goosewobbler commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR, but I don't want to pull tauri/unstable into the plugin. Cargo unifies features, so it reaches the consuming app and flips the primary webview from WindowContent to WindowChild in tauri-runtime-wry — every window then hosts its webview differently. The harness shouldn't change the app it measures, least of all with #540 open in that area.

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.

goosewobbler added a commit that referenced this pull request Aug 1, 2026
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>
goosewobbler added a commit that referenced this pull request Aug 1, 2026
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>
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.

2 participants