feat(desktop): expose Config::with_background_throttling to opt out of WKWebView WebContent suspension#5587
Open
goosewobbler wants to merge 1 commit into
Conversation
WKWebView aggressively suspends the WebContent process when the host window is not visible/focused. For long-running background work — most notably automation drivers like @wdio/dioxus-service whose JS polling loop must keep firing across spec boundaries on a headless CI runner — the default Suspend policy freezes the process and never recovers. wry already exposes WebViewBuilder::with_background_throttling with a BackgroundThrottlingPolicy enum (Disabled / Suspend / Throttle), but dioxus-desktop hasn't surfaced it through Config. Add a builder method that stores the policy and apply it during WebView construction in webview.rs alongside the existing with_background_color call. When unset, behaviour is unchanged (wry falls back to its platform default, which on macOS is effectively Suspend).
goosewobbler
added a commit
to webdriverio/desktop-mobile
that referenced
this pull request
May 26, 2026
Greptile flagged the [patch.crates-io] blocks using a moving branch reference: a force-push or accidental commit on the fork branch would silently change what CI compiles against. We don't commit Cargo.lock for these fixtures so there's no second line of defence. Switch from \`branch = "feat/..."\` to \`rev = "<sha>"\` in both fixture Cargo.toml files, pinned to the same commit currently on goosewobbler/dioxus@feat/desktop-background-throttling-policy-v0.7.9 (c03c394d3). Also add the upstream PR link (DioxusLabs/dioxus#5587) to the fixture comment so future readers can see exactly what's pending. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Author
|
Note for reviewers: the failing Check | Minimum dependency versions job is unrelated to this PR's changes — it's the upstream |
goosewobbler
added a commit
to webdriverio/desktop-mobile
that referenced
this pull request
May 27, 2026
The previous rule overcorrected by banning all ticket/PR references. The real distinction is whether the reference is load-bearing — an issue link whose resolution removes the commented code is the opposite of drift, it's an active signal to update. Refine the Comments section: - Keep the "no version numbers / transient stack traces / 'used to do X'" rule — these rot silently - Add an explicit positive case for tracking refs that gate code removal, with a concrete example matching the upstream-issue-workaround pattern we use elsewhere in this repo (e.g. the dioxus-bridge throttling fix linked to DioxusLabs/dioxus#5587). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
goosewobbler
added a commit
to webdriverio/desktop-mobile
that referenced
this pull request
May 27, 2026
The previous rule overcorrected by banning all ticket/PR references. The real distinction is whether the reference is load-bearing — an issue link whose resolution removes the commented code is the opposite of drift, it's an active signal to update. Refine the Comments section: - Keep the "no version numbers / transient stack traces / 'used to do X'" rule — these rot silently - Add an explicit positive case for tracking refs that gate code removal, with a concrete example matching the upstream-issue-workaround pattern we use elsewhere in this repo (e.g. the dioxus-bridge throttling fix linked to DioxusLabs/dioxus#5587). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit caad347)
goosewobbler
added a commit
to webdriverio/desktop-mobile
that referenced
this pull request
May 28, 2026
…throttling (#294) * chore: update package dependencies and configurations - Bump versions of several packages in package.json, including `zod` to ^4.4.3 and `@types/node` to ^25.9.1 across various fixtures and packages. - Update `electron-nightly` version in pnpm-workspace.yaml to 44.0.0-nightly.20260521. - Adjust pnpm-lock.yaml to reflect the updated package versions and ensure consistency across the workspace. - Add `vitest` as a dependency in multiple packages to enhance testing capabilities. These changes ensure that the project is using the latest compatible versions of dependencies, improving stability and performance. * chore: update package dependencies and versions - Bump `@releasekit/release` to version `^0.23.0` in `package.json` and `pnpm-lock.yaml`. - Upgrade `lint-staged` to version `^17.0.5` in `package.json` and `pnpm-lock.yaml`. - Update `rollup-plugin-node-externals` to version `^9.0.1` in `packages/bundler/package.json`. - Upgrade `happy-dom` to version `^20.9.0` in `packages/dioxus-bridge/package.json`. - Update `@electron/packager` to version `^20.0.0` in both `packages/electron-service/package.json` and `packages/native-types/package.json`. - Bump `puppeteer-core` to version `^25.0.4` in `packages/electron-service/package.json`. - Upgrade `@puppeteer/browsers` to version `^3.0.3` in `packages/electron-service/package.json`. These updates ensure compatibility with the latest features and improvements across the project. * chore(native-utils): align tsx peer range with devDependencies Bump peerDependencies.tsx from ^4.21.0 to ^4.22.3 to match the devDependency floor. Although ^4.21.0 already satisfies 4.22.x, the older minimum allows consumers to install patches we no longer actively test against. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(bundler): pin rollup-plugin-node-externals to ^8.x v9.0.1 requires Node >=24, which breaks the Tauri package-test Docker images (Ubuntu/Debian/Fedora/Arch — all intentionally on Node 20 LTS, see arch.dockerfile for the Node 26 undici incompatibility note). The workspace's stated engine (>=24.11.0) is for our dev toolchain, not the distros where end users install built artefacts, so the package-build chain still needs to run cleanly on Node 20. v8.1.2 supports Node 18+ and is API-compatible for the way bundler uses the plugin (single getConfig call in rollup.config.ts). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(electron-service): tighten engines for puppeteer-core v25 puppeteer-core v25 raised its minimum to Node 20.19 / 22+. The engines field still allowed Node 18.12+ and 20.9.x, which are below the transitive minimum. All puppeteer-core imports are type-only so consumers on older Node versions don't crash at runtime, but the engines constraint should match what the package actually supports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(deps): pin @wdio/* and webdriverio at 9.27.0 in default catalog 9.27.1 regressed Dioxus E2E on macOS-ARM: executeScript calls time out after 30s × 3 retries on a trivial polyfill wrapper, with the whole spec suite cascading into timeouts after the first test. Linux and Windows pass cleanly with 9.27.1 on the same commit, and main was green at 9.27.0 before this PR. Pinning exact (rather than ^) so future patches don't quietly re-introduce the regression. Catalog comment notes the reason to revisit. Fixture package.jsons keep their direct ^9.27.1 specs — they simulate end-user installs and should track latest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(fixtures/dioxus-app): pin @wdio/* + webdriverio to 9.27.0 The fixture's direct ^9.27.1 specs bypassed the default-catalog pin in pnpm-workspace.yaml, so Package - Dioxus [macOS-ARM] would still resolve webdriverio@9.27.1 transitively and hit the same executeScript-timeout regression the catalog pin was added to avoid. webdriverio is added as a direct devDep so the peer dependency resolution can't pick up 9.27.1 from sibling fixtures (electron/tauri package tests still pin ^9.27.1 — they don't exercise the Dioxus embedded driver and pass cleanly on macOS-ARM). Lift the pin alongside the catalog one when an upstream fix lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revert "fix(deps): pin @wdio/* and webdriverio at 9.27.0" The macOS-ARM Dioxus E2E (standard + multiremote) still fail with the same first-spec-passes / spec-1+ -all-timeout pattern at 9.27.0. wdio is not the cause. Unpin so the dep update PR's intent is preserved and the real culprit can be hunted properly. Also reverts fixtures/package-tests/dioxus-app pin (added in the same attempt). Engines bump on packages/electron-service stays — that one is unrelated to the Dioxus regression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(diagnostic): reorder Dioxus standard specs to test position hypothesis Move api.spec.ts (currently first alphabetically and the only spec that passes on macOS-ARM CI) to last. If the failure is positional — every spec after the first hangs — api.spec.ts will now fail in last position and application.spec.ts (now first) will pass. Revert once diagnosed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: revert diagnostic spec reorder Diagnostic served its purpose: confirmed the macOS-ARM failure is positional and triggered by tests that throw a JS error inside browser.dioxus.execute() (invoke-rejection or thrown-error). The bridge recovers cleanly from those locally; the hang is specific to GHA macOS-ARM runners. Investigation continues in a separate change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(dioxus): harden script execute against WKWebView throw edge case + add invoke tracing Two changes targeting the macOS-ARM CI hang where scripts that throw inside browser.dioxus.execute poison the polling loop: - packages/dioxus-service/src/commands/execute.ts: wrap the user function in `new Promise((resolve, reject) => { try { ... .then(resolve, reject); } catch (e) { reject(e); } })`. Any synchronous throw becomes an explicit promise rejection regardless of how the surrounding AsyncFunction body handles it. Speculative — covers a WKWebView-specific path where a sync throw inside a sync IIFE inside an AsyncFunction body might not propagate cleanly to the awaiting caller. - packages/dioxus-bridge/src/invoke.rs: emit a tracing::debug line for every incoming invoke command. On CI, captured backend logs will show the last command processed before the hang, narrowing the failure window to the JS-side step that follows. wdio_dioxus_bridge=debug is already enabled in the e2e fixture's tracing filter so this surfaces in artifacts without additional plumbing. Local M-series Mac runs the full standard E2E suite green in 17s with both changes applied (was 19s without — no regression). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(deps,docs): align shipped Node range with parent webdriverio (>=18.20.0) Two strands: 1. Bring the shipped Node range and the actual runtime deps back in line with what end users get from webdriverio. The repo had drifted into inconsistency (engines said one thing, deps required newer). - All engines fields set to `>=18.20.0` (matches webdriverio v9). - puppeteer-core ^25.0.4 → ^24.41.0 in @wdio/electron-service runtime deps (v25 requires Node 22.12+; v24 supports Node 18+). - @electron/packager ^20.0.0 → ^18.4.4 in @wdio/electron-service runtime deps (v19+ require Node 22.12+; v18 supports Node 16.13+). Only `allOfficialArchsForPlatformAndVersion` + `OfficialArch` types are used — both exist in v18, no API drift, full test suite passes. - pnpm.overrides for puppeteer-core: ^24.41.0 — without it pnpm hoists v25 to satisfy webdriverio's puppeteer-core peer range `>=22.x || <=24.x`, which is effectively unconstrained because of the OR. Drop the override when WDIO v10 lands and we bump our floor. - devDeps untouched: contributor toolchain (vitest, eslint, lint-staged, jsdom, ora, @inquirer/prompts, @releasekit/release, @electron/packager in native-types devDeps, @puppeteer/browsers in electron-service devDeps) can require Node 20+ — they don't ship to end users. 2. Documented the policy in CONTRIBUTING.md + cross-refs from README.md, docs/setup.md, AGENTS.md. The strategy section is version-agnostic: points at the engines field per package.json as the source of truth, no specific Node/dep versions baked into prose. Contributors keep the concrete "Node 24 LTS" recommendation at the install step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-bridge): instrument polling loop catches to surface silent throw CI traces on macOS-ARM show the polling loop's cadence flipping from ~13ms (normal empty-queue path) to ~100ms (outer-catch back-off) immediately after a script that rejects via invoke() completes. The outer catch is silent (`} catch {`), so we have no visibility into what it's catching. ~500ms later the loop dies entirely. Add a console.warn in the outer catch + in the result-delivery catch capturing the actual error string. console.warn is already wrapped to forward to invoke('log_frontend') fire-and-forget, so the lines land in captured backend logs without further plumbing. Drop these once root-caused. Local smoke test: full standard E2E still passes in 19s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-bridge): add independent heartbeat to prove JS aliveness CI diagnostic from the previous push showed zero log_frontend invokes reaching Rust after the polling loop appeared to die — telling us that console.warn from inside the loop's outer catch either never fired OR its underlying fetch hung too. Either way the in-loop diagnostic can't differentiate "loop died but JS alive" from "whole webview wedged". Add a setInterval(2s) heartbeat outside the polling loop's lifecycle. It fires `void invoke('__diag', 'heartbeat #N')` fire-and-forget. Combined with the existing __embedded_poll trace, the next failing run tells us: - heartbeats keep arriving after polls stop → loop died, JS+fetch alive (problem is in the loop's await chain) - heartbeats also stop → JS frozen or fetch globally broken (problem is in webview / wdio:// scheme handler) Wire __diag handler in dioxus-bridge that just emits tracing::warn to the wdio_dioxus_bridge::diag target. Drop both once root-caused. Local smoke test: full standard E2E still passes in 19s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-e2e): fail-fast settings to speed up macOS-ARM diagnostic loop Standard run was taking ~64 min in CI because every failing spec waited its full retry window (~120s) then moved on to the next, cascading through all 7 remaining specs. With root-causing in progress that's a ~hour feedback cycle per push. Three settings tightened, all clearly marked as diagnostic-only: - bail:1 → stop after first spec failure (we already know they all cascade, so subsequent failures add nothing new) - connectionRetryCount:0 → WDIO no longer retries failed commands 3×, so a hung executeScript fails in ~5s instead of ~90s - mochaOpts.timeout:30000 → match the new script-window so the test itself fails as soon as the script gives up - timeouts.script:5000 on each capability → embedded driver's per-session Axum-side script_timeout drops from default 30s to 5s Net: failing CI run should complete in <5 min instead of ~64 min. Local 8-spec passes in 17s with the new settings (was 19s — diagnostic overhead is noise). Revert all four together when the root cause is found. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-e2e): suppress macOS App Nap on Dioxus E2E to test process-freeze hypothesis CI diagnostic showed both the polling loop AND an independent setInterval heartbeat stop firing at the exact same instant during spec-boundary worker restarts on macOS-ARM runners. That points to the whole Dioxus app process being suspended by macOS (App Nap / process throttling), not a bug in the loop or the bridge fetch chain. Why only Dioxus + macOS: * Dioxus uses an in-process embedded WebDriver (Axum + JS polling loop in the host app). When the app naps, both freeze with it. * Tauri uses external tauri-driver + WebKitWebDriver, which signal automation context to macOS and keep their target webview awake. * macOS App Nap criteria — no visible/focused windows, no user input, system idle — are always met on a headless CI runner. Two suppressions on the macOS branch of the Dioxus E2E command: * NSAppSleepDisabled=YES propagates to the spawned app process and disables App Nap entirely for the test duration. * caffeinate -dims wraps the pnpm command to prevent display/idle/disk/ system sleep over its lifetime. If heartbeat #3+ now fires and tests pass, the App Nap hypothesis is confirmed and we move to a proper fix (wry config or driver-side keep-alive signal). If they still fail, we're back to the process-state debugger. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-bridge): silent AudioContext to defeat WKWebView background throttling App-process-level workaround (NSAppSleepDisabled + caffeinate) didn't fix the macOS-ARM Dioxus E2E hang in the previous run — both polls AND the independent setInterval heartbeat still stopped at the same instant. That rules out OS-managed App Nap and points to **WKWebView's WebContent process throttling**, which is a WebKit-internal mechanism unaffected by OS-level sleep assertions. WebKit suspends the WebContent process running JS when the host window isn't visible/focused — always true on a headless CI runner. Tauri escapes this because tauri-driver/WebKitWebDriver signals automation context to WebKit. Dioxus's embedded driver has no such external signal. Workaround: start a silent AudioContext oscillator at bridge load when the embedded driver is active. WebKit classifies pages with active audio graphs as "playing media" and exempts them from background throttling. Zero audible output (gain 0). Gated on __WDIO_EMBEDDED_PORT so end-user apps without the embedded driver never run this. Diagnostic invokes added: __diag tracks `audio-keepalive started (state=...)` so the next CI log shows whether the AudioContext actually started or was deferred by autoplay policy. Either way, the oscillator ties up the hardware-output graph which is enough to defeat throttling even when the context is suspended. Long-term fix is to expose `Config::with_background_throttling` upstream in Dioxus (which wraps wry's BackgroundThrottlingPolicy::Disabled). PR to dioxus pending. The JS workaround stays until that lands and we can bump dioxus across our test fixtures. Local 8-spec passes in 18s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci(dioxus-bridge): switch keepalive from AudioContext to muted HTMLAudioElement Last CI run confirmed the throttling diagnosis: `audio-keepalive started (state=suspended)` — the AudioContext was created but blocked by WebKit's autoplay policy, so the oscillator never actually played and no media exemption was granted. Same 2-heartbeat death pattern persisted. Muted HTMLMediaElement autoplay is more permissive in WKWebView than audio-graph playback. Switch to a looping `<audio>` element pointing at a small base64-encoded silent WAV data URL, with `muted=true volume=0`. Reports `audio-keepalive started (playing=true, muted=true)` if it actually starts, or `play() rejected: <name>: <msg>` if blocked. If this run still shows heartbeats stopping at #2, autoplay is also blocking muted HTMLMediaElement and we need to either (a) fork dioxus to expose `Config::with_background_throttling`, or (b) configure `mediaTypesRequiringUserActionForPlayback` via wry — neither of which is exposed in Dioxus's current Config API. Local 8-spec passes in 15s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(dioxus-bridge): disable WKWebView background throttling under automation Root cause confirmed across four CI iterations: WKWebView's WebContent process (the process running page JS) gets fully suspended when the host window is not visible/focused, which is always the case on a headless macOS-ARM runner. Both the embedded driver's polling loop AND an independent setInterval heartbeat freeze at the same instant, every time, at a spec boundary. Tauri E2E is unaffected because tauri-driver/ WebKitWebDriver signals automation context to WebKit externally and keeps the view awake. Proper fix uses wry's BackgroundThrottlingPolicy::Disabled to tell WKWebView never to suspend the view. dioxus-desktop didn't expose this yet — patched the fork (goosewobbler/dioxus, branch feat/desktop-background-throttling-policy-v0.7.9) to add Config::with_background_throttling. Upstream PR to follow. Bridge calls this automatically when automation::is_requested() returns true (set by @wdio/dioxus-service via DIOXUS_WEBVIEW_AUTOMATION=true). End-user apps that don't set this env var keep the default behaviour. Workspace-side changes: - packages/dioxus-bridge/src/lib.rs: call config.with_background_throttling (Disabled) when DIOXUS_WEBVIEW_AUTOMATION is set - packages/dioxus-bridge/guest-js/index.ts: drop the silent-audio workaround; the proper fix replaces it. Heartbeat kept for one more CI cycle so we can verify #3+ now fires. - fixtures/e2e-apps/dioxus/Cargo.toml: [patch.crates-io] block pointing the dioxus family (dioxus, dioxus-core, dioxus-desktop) at the fork branch. Whole family patched (not just dioxus-desktop) because Cargo treats crates.io dioxus-core != git-fork dioxus-core even at the same version, causing type-identity mismatches across the dep tree. - fixtures/package-tests/dioxus-app/src-dioxus/Cargo.toml: same patch. Local 8-spec passes in 15s. Drop the [patch.crates-io] block when an upstream dioxus-desktop release includes the API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(dioxus-bridge): gate with_background_throttling call behind Cargo feature Build Dioxus Crates CI job was failing because standalone `cargo check` on wdio-dioxus-bridge doesn't inherit the binary apps' [patch.crates-io] block — it sees the published dioxus-desktop@0.7 which doesn't have Config::with_background_throttling. Error E0599: no method named `with_background_throttling` found for struct `dioxus_desktop::Config`. Gate the call behind a new optional `with-background-throttling` Cargo feature on wdio-dioxus-bridge: - Off by default → standalone bridge check sees no call, builds clean against published dioxus-desktop. - Enabled by the binary apps that include the [patch.crates-io] block that points dioxus-desktop at the fork with the API. Wired through wdio-dioxus-embedded-driver as a forwarding feature so the binary apps only need to enable one feature on their direct driver dep: wdio-dioxus-embedded-driver = { path = "...", features = ["with-background-throttling"] } Enabled on the two binary apps that have the patch: - fixtures/e2e-apps/dioxus - fixtures/package-tests/dioxus-app/src-dioxus Drop the feature gate when upstream Dioxus releases the API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(dioxus-service): set DIOXUS_WEBVIEW_AUTOMATION on embedded driver spawn Last CI confirmed the bridge's automation gate was returning false: > wdio_dioxus_bridge: DIOXUS_WEBVIEW_AUTOMATION not set — automation disabled …so the background-throttling fix never executed and the WebContent process froze at spec 0-1 as before. The env var was only being set by the external driver path (packages/dioxus-driver/src/webdriver.rs sets it on the WebKitWebDriver / msedgedriver subprocess that then propagates to the launched app). The embedded driver path has no external driver subprocess — the launcher spawns the app binary directly — so the env var was never set there. Add it to the env block in providers/embedded.ts so the bridge's automation::is_requested() returns true and config.with_background_throttling(Disabled) actually fires. Comment in dioxus-driver/src/webdriver.rs already names the bridge as the consumer; this commit just closes the gap on the other path. Local 8-spec passes in 14s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(dioxus): drop diagnostic scaffolding now throttling fix is verified CI is green end-to-end with the wry background-throttling fix in place on macOS-ARM. Reverts the temporary diagnostic layer: - packages/dioxus-bridge/guest-js/index.ts: drop the heartbeat setInterval and the verbose console.warn calls in the poll-loop catches. The polling loop is back to silent catches now that the root cause (WKWebView WebContent throttling) is fixed. - packages/dioxus-bridge/src/invoke.rs: drop the per-invoke tracing::debug! that was added to trace where the loop was stalling. - packages/dioxus-bridge/src/lib.rs: drop the __diag fire-and-forget Rust command (only consumer was the heartbeat). - e2e/wdio.dioxus-embedded.conf.ts: restore default mocha timeout (60s, matching sibling wdio.dioxus.conf.ts), drop bail:1, restore connectionRetryCount:3, drop per-capability timeouts.script:5000. - .github/workflows/_ci-e2e-dioxus-all-providers.reusable.yml: drop the macOS-only caffeinate -dims + NSAppSleepDisabled wrapper. App Nap turned out not to be the root cause — wry background throttling was the real one, and that's fixed at the wry level now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(dioxus-bridge): move throttling fix from bridge to app level The published bridge crate previously exposed a `with-background-throttling` Cargo feature that, when enabled, made `install()` call `Config::with_background_throttling(Disabled)`. That method only exists in our forked dioxus-desktop, so the feature ties the bridge's published API surface to an upstream-pending PR — and means end users who hit the freeze on macOS-ARM CI have to (a) patch dioxus and (b) enable a feature flag on the bridge. Move the call to the app's `main.rs` instead: - packages/dioxus-bridge: drop the `with-background-throttling` feature and its call site in `install_with_registry_and_config`. Bridge is now agnostic to the throttling API. - packages/dioxus-embedded-driver: drop the forwarded feature; re-export `wdio_dioxus_bridge::automation` so apps depending only on this crate can still gate behaviour on `DIOXUS_WEBVIEW_AUTOMATION`. - fixtures/e2e-apps/dioxus + fixtures/package-tests/dioxus-app: drop the feature opt-in; add the throttling call directly in `main.rs`, gated on `wdio_dioxus_embedded_driver::automation::is_requested()`. The `[patch.crates-io]` block stays in the fixtures (only there) until the upstream Dioxus PR lands. End users hitting the issue now copy a 3-line snippet into main.rs and patch dioxus themselves. Once upstream lands, the patch goes away and the snippet still works against published dioxus-desktop — no bridge churn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(deps): address Greptile review on PR #294 - packages/native-types: align @electron/packager to ^18.4.4. v19+ requires Node >=22.12.0 (ESM-only); the rest of the PR deliberately downgrades electron-service to the same line for Node 18 support. - fixtures/package-tests/dioxus-app: switch @wdio/* devDeps from hard-pinned ^9.27.1 to catalog:default so the fixture inherits the workspace catalog version rather than diverging. - pnpm-lock.yaml: regen for the above. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(dioxus-fixtures): pin [patch.crates-io] to a commit SHA Greptile flagged the [patch.crates-io] blocks using a moving branch reference: a force-push or accidental commit on the fork branch would silently change what CI compiles against. We don't commit Cargo.lock for these fixtures so there's no second line of defence. Switch from \`branch = "feat/..."\` to \`rev = "<sha>"\` in both fixture Cargo.toml files, pinned to the same commit currently on goosewobbler/dioxus@feat/desktop-background-throttling-policy-v0.7.9 (c03c394d3). Also add the upstream PR link (DioxusLabs/dioxus#5587) to the fixture comment so future readers can see exactly what's pending. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revert "chore(deps): switch dioxus-app fixture to catalog:default" Greptile's suggestion to use catalog references doesn't fit this fixture's purpose: package-test apps run in an isolated install environment that doesn't resolve pnpm catalog: links. All sibling package-test apps (electron-script, electron-builder, electron-forge, tauri-app) use explicit ^9.27.1 strings for the same reason — restore the same pattern here. This reverts only the catalog change from 003847f; the @electron/packager downgrade in that commit stays. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(biome): update schema version to 2.4.15 in biome.jsonc * chore: gitignore Dioxus app build dirs Add the two Dioxus fixture target/ paths in the same section style as the existing Tauri pattern: - **/src-dioxus/target/* — matches the package-test app layout (fixtures/package-tests/dioxus-app/src-dioxus/target/) - fixtures/e2e-apps/dioxus/target/ — explicit path for the e2e app which doesn't use the src-dioxus wrapper Verified via git check-ignore that both targets now match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(forge-fixtures): pin Electron back to 41.2.0 to test hypothesis CI failures since this PR are concentrated on forge fixtures (both e2e and package-tests). Builder fixtures got the same Electron 41 → 42 bump and are NOT failing, so the regression is Electron-42-specific to Forge — likely a Forge 7.11.2 + Electron 42 packaging interaction (7.11.2 changelog includes a TTY-detection fix and a lodash → eta templating swap, either could explain the silent abort at "❯ Finalizing package" we see in build logs). Pin Electron back to 41.2.0 in the three forge fixtures to isolate the variable: - fixtures/e2e-apps/electron-forge (was catalog:default → 42.2.0) - fixtures/package-tests/electron-forge-app-cjs (was 42.2.0) - fixtures/package-tests/electron-forge-app-esm (was 42.2.0) If CI goes green on forge, Electron 42 is confirmed as the trigger. Then we have options: keep this pin, file upstream issue, or bump when Forge 7.11.3 lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(deps): pin Electron back to 41.2.0 across all fixtures + catalog Electron 42 + Forge 7.11.2 combo broke forge packaging (silent abort at "❯ Finalizing package"). Earlier partial pin (just forge fixtures to 41.2.0) caused a chromedriver version mismatch — the workspace catalog still on 42.2.0 → wdio downloaded chromedriver 148 → fixture Electron 41 ships chromedriver 138 → driver/binary mismatch on launch (see run 26501340841 job 78042413833). Revert the whole Electron bump in this PR: - pnpm-workspace.yaml catalog: 42.2.0 → 41.2.0 - fixtures/e2e-apps/electron-forge: explicit 41.2.0 → catalog:default (catalog is now 41.2.0 again, so this stays consistent with sibling e2e fixtures that all use catalog:default) - fixtures/package-tests/electron-builder-app-{cjs,esm}: 42.2.0 → 41.2.0 - fixtures/package-tests/electron-forge-app-{cjs,esm}: already at 41.2.0 from previous commit Electron 42 bump can be retried as a separate PR once Forge 7.11.3 lands with a fix, or with a workaround verified. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(deps): pin script package-tests Electron back to 41.2.0 too Missed these in the last revert pass — script package-tests fixtures also bumped from 41.2.0 to 42.2.0 in this PR. Pin them back to keep the entire Electron revert consistent across all fixtures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * pin(forge): exact 7.11.1 across all fixtures (no caret) Forge 7.11.2 introduced a TTY-detection fix (#4219) and a lodash → eta templating swap (#4208) that broke our forge fixtures' packaging in CI (silent abort at "❯ Finalizing package", no output written). Reverting Electron 41 → 42 didn't fix it on its own because pnpm still resolved to 7.11.2 via the `^7.11.2` ranges. Pin Forge to exact `7.11.1` everywhere — no caret, no patch float — across: - fixtures/e2e-apps/electron-forge - fixtures/package-tests/electron-forge-app-{cjs,esm} (5 packages each: cli + maker-deb/rpm/squirrel/zip) - e2e/package.json Per maintainer note: certain build-tool deps (Forge especially) need exact pins. Floating patches have repeatedly bitten us with silent behaviour changes that are hard to diagnose. Bump deliberately when we've validated a new version works, not as a side effect of pnpm install. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ci): disable turbo cache for forge e2e build + catalog cleanup Two changes: 1. Disable turbo cache for `electron-forge-e2e-app#build` and `electron-forge-e2e-app#build:mac-universal`. The turbo cache key for per-package overrides doesn't pick up changes from the default build task's inputs, so a one-time silent-fail Forge build keeps getting served from cache across runs (latest hash 3ea815c66ebbcb29 — every Linux forge e2e job hits it and the `out/` is empty, then "Setup Protocol Handlers" can't find the binary). Disabling cache for this one task means each CI run rebuilds fresh — ~10s cost, worth it to guarantee no stale empty cache. 2. Catalog cleanup: - Add `@electron-forge/cli: 7.11.1` and `electron-builder: 26.8.1` to the default catalog. e2e + e2e-apps fixtures now use `catalog:default` instead of duplicating the version string. Package-tests fixtures keep explicit pins (per the isolated-install rule documented in feedback memory). - Remove carets from all `@wdio/*` and `webdriverio` catalog entries. Floating patches have caused intermittent CI breakages — version bumps should be deliberate, not a side effect of `pnpm install`. Same rule we're now applying to forge. Net: 3 catalog refs replace 4 explicit pins (e2e + e2e-apps forge cli; e2e-apps builder). Package-tests fixtures unchanged. Catalog itself gains 2 entries (forge, builder) and loses 8 carets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: pin Node to 24.15.0 to avoid 24.16.0 packager regression GitHub Actions runner images (ubuntu24/20260525.x, win25-vs2026/20260525.x, macos-15/20260525.x) bumped Node.js from 24.15.0 to 24.16.0 on May 26. Since that rollout, every CI run on this branch fails cross-platform with @electron/packager silently exiting at "Finalizing package" — clean exit 0, no output, dist-electron empty, downstream "Could not find Electron app". Same exact commit 93354e2 was green pre-rollout and fails post-rollout. Workspace lockfile is frozen, so the only relevant moving part is the runner toolchain. Node 24.16.0 changes touching the subprocess hot path: - src: coerce spawnSync args to string once (#62633) - process: handle rejections only when needed (#62919) Pin until upstream confirms a fix in @electron/packager or Node. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: pin Node 24.15.0 across all workflow callers Follow-up to 92f7af6 — every workflow that calls setup-workspace or setup-node passes node-version: '24' explicitly, which overrode the default I just pinned. Bulk-update all 23 callers to '24.15.0' so the pin actually takes effect. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(switch-catalog): parse unquoted workspace entries + expand globs The catalog-switch script was silently broken: 1. The regex `/^-\s*['"]|['"]$/g` only stripped the `- ` prefix when workspace entries were quoted (`- 'foo'`). Our pnpm-workspace.yaml uses unquoted entries (`- foo`), so every path kept the literal `- ` prefix and fs.existsSync failed — script warned on every entry and updated nothing. Replace with two separate replaces: strip leading `- ` unconditionally, then strip optional surrounding quotes. 2. The script also iterated `packages/*` and `examples/*` glob entries as literal paths. Add a small `expandWorkspacePath` helper that expands trailing `/*` to the actual subdirectories (the only glob pattern this workspace uses). 3. Suppress the "no package.json" warning for Rust-only workspace packages (Cargo.toml, no package.json) — those are expected and shouldn't be flagged. Verified: `pnpm catalog:default` now correctly updates 7 packages, zero warnings, and triggers `pnpm install` to apply the change. Also picks up the JS-side ripple: `e2e/package.json` and `packages/native-utils/package.json` got re-sorted by the script when it found that some of their deps weren't on the catalog reference yet (harmless — the script writes sorted output even for "no changes" runs once it touches the file, which it now correctly does). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 1b3e5dd) * catalog: add electron-builder + forge to next + minimum catalogs Without entries in the non-default catalogs, the `pnpm catalog:next` and `pnpm catalog:minimum` switch script writes broken refs into package.json (pnpm doesn't fall back to default when a `catalog:<name>` ref is missing — it errors). Mirror the default values into both: - minimum: same as default for both (we don't currently test against older forge/builder lines, and these tools' versions don't materially affect what we exercise — we run against the built binary) - next: electron-builder uses its `next` dist-tag; forge has no `next` tag on npm, so it stays at the default exact pin Catalog switching is a local dev tool (not used in CI), so this is quality-of-life, not a blocker. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 6a3214f) * fix(switch-catalog): fall back to default for deps missing in target catalog Switching to a non-default catalog (e.g. `pnpm catalog:minimum`) used to blindly write `catalog:<target>` for every dep in the default catalog, even when that dep didn't exist in the target. pnpm doesn't fall back to default for missing refs — it errors out. So any default-only dep (e.g. @wdio/xvfb, which is v9-only with no v8 to populate the `minimum` catalog) made `pnpm catalog:minimum` fail. Fix at the script level rather than padding non-default catalogs with duplicate entries: parse all catalogs into a Map<name, Set<dep>>, and for each switch, check if the target catalog has the dep. If it does, write `catalog:<target>`; if not, write `catalog:default` as a fallback. End state: a package switched to `minimum` ends up with a mix of `catalog:minimum` refs (for v8-supported deps) and `catalog:default` refs (for new-since-v8 deps like @wdio/xvfb), which is semantically what you want for minimum-version testing. Also drop the placeholder forge/builder entries that were just added to `minimum`/`next` — no longer needed since the script handles the missing-in-target case. Future predictable hits (e.g. @wdio/xvfb → @wdio/display-server rename in WDIO v10) are now automatic. Verified: round-trip `default → minimum → default` produces zero diff, and `e2e/package.json` correctly ends up with @wdio/xvfb on `catalog:default` during the `minimum` step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 01a76af) * docs(agents,catalog): tighter comment rules + drop redundant catalog comments - pnpm-workspace.yaml: drop two comments that either restated the code (the "pinned exact" block above @wdio/*) or coupled rationale to a specific version that will drift ("Forge 7.11.2 silently broke ..." above the build-tool block). Rephrase the non-default-catalog comment to drop the @wdio/xvfb-specific example, which would need updating the moment the package is renamed. - AGENTS.md Comments section: spell out the default-no-comments stance, the "don't restate what code says" rule, and the "don't couple comments to drift-prone details like version numbers, file paths, ticket refs, or PR/commit names" rule. Show the rationale-vs-citation contrast inline so future contributors (humans and LLMs alike) get the spirit of the rule, not just the letter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 1c4b32d) * docs(agents): allow load-bearing tracking refs in comments The previous rule overcorrected by banning all ticket/PR references. The real distinction is whether the reference is load-bearing — an issue link whose resolution removes the commented code is the opposite of drift, it's an active signal to update. Refine the Comments section: - Keep the "no version numbers / transient stack traces / 'used to do X'" rule — these rot silently - Add an explicit positive case for tracking refs that gate code removal, with a concrete example matching the upstream-issue-workaround pattern we use elsewhere in this repo (e.g. the dioxus-bridge throttling fix linked to DioxusLabs/dioxus#5587). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit caad347) * Revert "chore(deps,docs): align shipped Node range with parent webdriverio (>=18.20.0)" This reverts de68a0b. CI failures concentrated on Windows started appearing after de68a0b — most likely from the puppeteer-core ^25.0.4 → ^24.41.0 downgrade interacting with Windows runner AV behaviour, but a full bisect wasn't worth the cycles given the strategic context. @wdio/electron-service@10.0.0 already shipped without Node 18 support, so the Node 18 alignment work in de68a0b was bridging to a promise we couldn't keep anyway. Cleaner to revert and wait for WDIO v10 to formally bump the floor to Node 20+, at which point we align and stay aligned. Reverts: - engines fields across 14 packages - @wdio/electron-service runtime deps: puppeteer-core, @electron/packager - pnpm.overrides for puppeteer-core - The `## Node version support` section in CONTRIBUTING.md and its cross-refs in README.md / docs/setup.md / AGENTS.md Kept (small): a one-line callout in CONTRIBUTING.md and docs/setup.md distinguishing contributor toolchain Node from end-user Node, pointing at the `engines` field as source of truth. This distinction is useful regardless of which Node line we eventually align with. Follow-up issue tracking the alignment plan: filed separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit e228e8e) * fix(fixtures): replace shx-based clean:dist with tsx helper shx rm -rf ENOENT-errors on Windows when the target doesn't exist — seen in a recent CI run where Forge's e2e-app build had partial output state (out/ present, dist/ missing) and the BuildManager's pre-build clean:dist step crashed trying to scandir the missing dist/. Replace the three e2e-apps electron fixtures' clean:dist scripts with a small tsx helper that uses Node's native fs.rmSync({ force: true, recursive: true }) — which handles missing paths cleanly across platforms and avoids the pnpm dlx shx download dance entirely. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 2c06d80) * fix(fixtures): drop spaces in Forge productName on package-tests fixtures Win-only package-test Forge silent-finalize bug: Forge stops at "❯ Finalizing package" and exits 0 without writing the .exe — fingerprint of @electron/packager's Windows pipeline (rcedit, signtool, asar) choking on paths with spaces in productName. The fixture set packagerConfig.name to "Forge App Example", which made the output path "dist-electron/Forge App Example-win32-x64/...". The e2e Forge fixture has no name/executableName override — it lets Forge default to the package.json name (no spaces) and packages cleanly on Windows. Mirror that approach here: drop both packagerConfig.name and packagerConfig.executableName. Output paths become "dist-electron/electron-forge-app-example-{cjs,esm}-win32-x64/..." and wdio-electron-service auto-detects them. Tests are unaffected: the only "Forge App Example" assertion is on the renderer HTML <title>, which lives in src/renderer/index.html and doesn't depend on Forge's productName. The package.json name assertion (`html.toContain('electron-forge-app-example')`) was already passing against the package.json name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 653d30c) * ci: exclude temp paths from Windows Defender in setup-workspace Two recurring Windows-only CI flakes share the same root cause: Defender real-time scanning intercepts .exe writes by build/extraction tools, the tool exits 0 but the destination directory ends up missing the executable. - chromedriver: "browser folder (.../chromedriver/win64-X.Y.Z) exists but the executable (chromedriver-win64/chromedriver.exe) is missing" during @puppeteer/browsers extraction - Forge: Forge logs through "❯ Finalizing package" and exits 0 without writing the app .exe, downstream "Could not find Electron app built with Electron Forge!" Adding Defender exclusions for the temp/runner-temp/workspace paths and the test processes addresses both at the source. GitHub-hosted Windows runners run as admin so Add-MpPreference works without elevation. Runner is ephemeral so the lowered security posture is bounded to the job lifetime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit c4fd55b) * docs(ci): trim Defender exclusion comment Drop the package names and verbatim error strings — both drift if upstream tools rename or rephrase. Keep the WHY (Defender intercepts .exe writes during build/extract). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 860191e) * fix(ci): override yauzl@3.3.1; drop Node 24.15.0 pin Root cause confirmed via electron/forge#4277: extract-zip > yauzl@2 is broken on Node 24.16+. The Forge maintainer recommends overriding to yauzl@3.3.1 until extract-zip is replaced by an internal lib. Switching from "pin Node" to "fix the actual broken dep": - Add pnpm override: yauzl: ^3.3.1 - Revert all 23 node-version pins back to '24' (latest) - Restore setup-workspace default to '24' This is the cleaner fix — published @wdio/electron-service consumers don't need to pin Node, and the workaround disappears when extract-zip is replaced upstream. See: - electron/forge#4277 (Forge's report + fix recommendation) - electron/electron#51619 (Electron's tracking issue) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: drop Windows Defender exclusion step Added during the yauzl@2 + Node 24.16 debug as a guess; the real fix is the yauzl@3.3.1 override (a1be02d). Never observed Defender actually interfering with a build — removing per "don't add code for hypothetical scenarios". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(electron-service): downgrade @electron/packager to ^18.4.4 @electron/packager v19+ require Node 22.12+, but electron-service's engines.node still declares ^20.19.0 || >=22.12.0 — so the Node 20 code path was broken in practice on both main (v19.0.5) and this PR (v20.0.0). Downgrade to ^18.4.4 (Node 16.13+) to: - Align with electron-service's declared Node range - Match native-types which already pins ^18.4.4 for type imports - Match what Forge bundles transitively at runtime (18.4.4 via forge 7.11.1/.2) binaryPath.ts imports allOfficialArchsForPlatformAndVersion — a stable utility unaffected by the v18→v20 packager rewrites. The Node-version-policy question (drop Node 20? when?) belongs in #297. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(electron-service): drop Node 20 from engines.node Several direct deps already require Node 22.12+: - puppeteer-core@25 (>=22.12.0) - @electron/fuses@2.1.0 (>=22.12.0) - @puppeteer/browsers@3.0.3 (>=22.12.0) The ^20.19.0 bracket in engines.node was unreachable in practice on both main and this PR — pnpm fails the engine check against any of the above before npm install completes. Tighten to >=22.12.0 to match what actually works. Leaving @electron/packager at ^18.4.4 (separate commit 94e7428) for its independent value: alignment with native-types' type-import dep and with what Forge bundles transitively at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(dioxus-service): refresh execute.ts wrapper comment The comment claimed the wrapper body MUST be synchronous and pointed at executeAsync — both stale since the Promise wrapper landed (the body still has no top-level await, but does return a Promise). Replace with a description of how each driver path awaits the returned Promise: embedded via guest-js's AsyncFunction polling loop, external via the W3C executeScript spec contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5586.
What
Adds
Config::with_background_throttling(BackgroundThrottlingPolicy)todioxus-desktop, surfacing the policy that wry already supports viaWebViewBuilder::with_background_throttling. Forwards the value to the WebView at construction.Three variants from wry:
Disabled— never throttle.Suspend— fully suspend tasks when the view isn't in a window (current implicit default).Throttle— limit rather than suspend.The setting is applied at WebView creation; non-Apple platforms ignore it in wry today, so this is effectively a macOS opt-out.
Why
On macOS, WKWebView aggressively suspends the WebContent process (the process running page JS) when the host window isn't visible/focused, freezing all JS timers, fetches, and event handlers. On a headless host — CI runner, hidden window, no focus — that suspension is permanent.
Without this knob, there's no way for a Dioxus app to opt out from Rust-side config. The workarounds I tried first (
caffeinate -dims,NSAppSleepDisabled=YES, JS-sideAudioContext/mutedHTMLAudioElementkeepalive) all failed because the throttling is WebKit-internal, not OS-level App Nap. CallingWebViewBuilder::with_background_throttling(Disabled)at construction was the only fix.Concrete cases this unblocks:
@wdio/dioxus-service— an embedded WebDriver server inside the app process polls the webview over awdio://IPC channel, and the polling loop froze within seconds on macOS-ARM GitHub Actions runners. The same suite passes interactively on macOS and on Linux/Windows CI.See #5586 for the broader motivation discussion.
Implementation
Mirrors the existing pattern for other forwarded wry attributes (
with_background_color,with_data_directory, etc.):Configgetsbackground_throttling: Option<BackgroundThrottlingPolicy>(defaultNone).Config::with_background_throttling.webview.rsforwards the value toWebViewBuilder::with_background_throttlingwhen set.Nonepreserves current behaviour exactly — the attribute is only set if the app calls the new builder method.Test plan
cargo check -p dioxus-desktopclean.@wdio/dioxus-service): full E2E suite that previously timed out within the first spec now passes consistently across runs (reference run — once we wired this API up to setDisabledunder automation, the polling-loop hang disappeared).Configbuilder methods).Notes
wry::BackgroundThrottlingPolicyis reachable via the existingpub use wry;line indioxus_desktop::lib, so consumers can usedioxus::desktop::wry::BackgroundThrottlingPolicywithout adding wry as a direct dep.with_inactive_scheduling_policyto match Apple's docs term, but kept it aligned with wry'swith_background_throttlingfor consistency with the upstream API.