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

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 on lines +40 to +42
#[cfg(desktop)]
#[allow(dead_code)]
pub(crate) fn is_standalone_webview<R: Runtime>(webview: &Webview<R>) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
#[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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant allow(dead_code) in 15f862a. The helper remains used by the desktop close handler.

Comment on lines +77 to +83
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"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@abu0306

abu0306 commented Jul 16, 2026

Copy link
Copy Markdown
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
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
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.

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.

1 participant