Skip to content

test(dioxus): browser-mode test parity (integration + E2E + fixture)#489

Merged
goosewobbler merged 2 commits into
mainfrom
feat/dioxus-browser-test-parity
Jun 29, 2026
Merged

test(dioxus): browser-mode test parity (integration + E2E + fixture)#489
goosewobbler merged 2 commits into
mainfrom
feat/dioxus-browser-test-parity

Conversation

@goosewobbler

Copy link
Copy Markdown
Contributor

Closes #472.

Browser mode shipped for Tauri, Electron, and Dioxus, but Dioxus test coverage was asymmetric vs Electron. This brings Dioxus up to the Electron/Tauri template across all three tiers. Dioxus has no native event bus, so the events tier is intentionally omitted (matching how the Tauri/Electron docs handle events by explicit omission).

Integration tests

packages/dioxus-service/test/integration/ (new — only a no-op setup.ts existed before):

  • launcher.browser.integration.spec.ts — launcher→worker handshake: capability→chrome transform + dioxus:options removal, browser mode detected on a non-first capability, devServerUrl required/valid-URL validation, devServerUrl propagation into the worker's initial navigation, and no embedded driver spawned (startEmbeddedDriver never called).
  • browser-mode.integration.spec.ts — worker browser-mode machinery: initial navigation + IPC injection, the browser.dioxus API surface install, the url() override re-injecting the IPC script after navigation (and not on arg-less url()), navigate-then-inject ordering, swallowed re-injection failures, single shared mock store (global dioxus.<command> keying), and the mock register → update()mockRestore() lifecycle incl. post-navigation recovery.
  • helpers.tscreateFakeBrowser / createFakeMock / defer / flushMicrotasks, trimmed to Dioxus's machinery (Dioxus has no MockUpdateScheduler, per-browser store keying, or BiDi preload, so those Electron/Tauri suites don't apply).

21 integration tests, all green.

E2E

  • e2e/wdio.dioxus-browser.conf.ts — real Chrome via chromedriver, self-hosted static dev server (:8088), autoXvfb, and a ps-based assertion that no Dioxus driver stack (wdio-dioxus* app binary, wdio-dioxus-driver, msedgedriver/WebKitWebDriver) is spawned.
  • e2e/test/dioxus-browser/mock.spec.ts — value-returning invoke + invoke-with-arguments via browser.dioxus.mock + mock.update().
  • fixtures/e2e-apps/dioxus-browser/ — static fixture app driving window.__WDIO_DIOXUS__.invoke('get_app_version' | 'greet').

Package fixture

fixtures/package-tests/dioxus-app/test-browser/browser.spec.ts now actually exercises browser.dioxus.mock('get_info') + mock.update() (added a fetch-info button + invoke to browser/index.html), matching the Tauri/Electron fixtures (was page-load + service-presence only).

Docs

packages/dioxus-service/docs/browser-mode.md — one-line Events note that events are unsupported (no native event bus; see packages/native-types/src/dioxus.ts).

CI

  • Wired pnpm e2e:dioxus-browser into _ci-browser-mode.reusable.yml (alongside the electron/tauri E2E steps) + the e2e/+root package.json scripts.
  • scripts/detect-changes.ts: route e2e/test/<svc>-*/ test dirs to their service via serviceForDir (so dioxus-browser specs trigger only dioxus, mirroring the existing wdio.<svc>-*.conf.ts rule) — with regression guards added in detect-changes.spec.ts.

Verification

Command Result
pnpm --filter @wdio/dioxus-service test:integration ✅ 21 passed
pnpm --filter @wdio/dioxus-service test:unit ✅ 131 passed (no regression)
pnpm --filter @wdio/dioxus-service typecheck ✅ clean
pnpm test:scripts (detect-changes) ✅ 98 passed
pnpm format:check (repo-wide) ✅ clean
biome check (linter) on all changed TS ✅ 0 issues

Not run locally: the live browser-mode E2E (pnpm e2e:dioxus-browser) — it needs real Chrome + the built service against a live dev server, which isn't available in this environment. The config + spec typecheck and are structurally identical to the shipping electron/tauri browser-mode E2E (same static-server + no-driver-assertion + cast patterns); they will exercise in CI's Browser Mode job.

🤖 Generated with Claude Code

Brings Dioxus browser-mode test coverage up to the Electron/Tauri template
(closes #472). Dioxus has no native event bus, so the events tier is omitted
by design (matching the Tauri/Electron docs' explicit-omission convention).

- Integration: add browser-mode.integration.spec.ts + launcher.browser.integration.spec.ts
  (+ helpers.ts) under packages/dioxus-service/test/integration/. Covers the
  launcher→worker handshake (capability→chrome transform, dioxus:options
  removal, devServerUrl validation/propagation, no embedded driver spawned)
  and the worker browser-mode machinery (initial navigation + IPC injection,
  url() override re-injecting after navigation, single shared mock store,
  mock register/update/restore lifecycle). 21 tests, all green.
- E2E: add e2e/wdio.dioxus-browser.conf.ts (real Chrome, self-hosted static
  dev server, ps-based no-Dioxus-driver assertion) + e2e/test/dioxus-browser/mock.spec.ts
  (value-returning invoke + invoke-with-arguments via browser.dioxus.mock).
  New fixtures/e2e-apps/dioxus-browser/ app drives window.__WDIO_DIOXUS__.invoke.
- Package fixture: extend fixtures/package-tests/dioxus-app to actually exercise
  browser.dioxus.mock('get_info') + mock.update() (button + invoke in browser/index.html).
- Docs: note in packages/dioxus-service/docs/browser-mode.md that events are
  unsupported (no native event bus; see packages/native-types/src/dioxus.ts).
- CI: wire pnpm e2e:dioxus-browser into _ci-browser-mode.reusable.yml + the
  e2e/root package.json scripts; route e2e/test/<svc>-*/ test dirs to their
  service in detect-changes (so dioxus-browser specs trigger only dioxus) with
  regression guards in detect-changes.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Standing release PR: #456 · 10 packages queued · open 93h 42m · ✅ ready to merge

Release Preview — 11 packages

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#456) when merged:

Changelog

@wdio/dioxus-bridge N/A → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/dioxus-service N/A → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/electron-service wdio-electron-service@v10.1.0 → 10.1.1

Changed

  • Update version to 10.1.1
@wdio/native-mobile-core wdio-native-mobile-core@v1.0.0 → 1.0.1

Changed

  • Update version to 1.0.1
@wdio/native-spy wdio-native-spy@v1.1.0 → 1.1.1

Changed

  • Update version to 1.1.1
@wdio/native-utils wdio-native-utils@v2.4.0 → 2.4.1

Changed

  • Update version to 2.4.1
@wdio/react-native-service N/A → 1.0.1

Changed

  • Update version to 1.0.1
dioxus-package-test-app N/A → 0.1.1

Changed

  • Update version to 0.1.1
wdio-dioxus-driver N/A → 1.0.1

Changed

  • Update version to 1.0.1
wdio-dioxus-embedded-driver N/A → 1.0.1

Changed

  • Update version to 1.0.1

Also bumped (sync versioning)

  • wdio-dioxus-bridge → 1.0.1

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
@wdio/dioxus-bridge 1.1.0 1.0.1 1.1.0
@wdio/dioxus-service 1.1.0 1.0.1 1.1.0
@wdio/electron-service 10.2.0 10.1.1 10.2.0
@wdio/native-mobile-core 1.1.0 1.0.1 1.1.0
@wdio/native-spy 1.2.0 1.1.1 1.2.0
@wdio/native-utils 2.5.0 2.4.1 2.5.0
@wdio/react-native-service 1.1.0 1.0.1 1.1.0
dioxus-package-test-app 0.2.0 0.1.1 0.2.0
wdio-dioxus-driver 1.1.0 1.0.1 1.1.0
wdio-dioxus-embedded-driver 1.1.0 1.0.1 1.1.0

Updated automatically by ReleaseKit

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR brings Dioxus browser-mode test coverage to parity with the Electron and Tauri templates across integration tests, E2E tests, and fixture exercises. Events are intentionally omitted — Dioxus has no native event bus.

  • Integration tests (21 new): launcher.browser.integration.spec.ts covers capability→chrome transform, devServerUrl validation, non-first-capability detection, and no-driver-spawn guarantee; browser-mode.integration.spec.ts covers worker init, navigation lifecycle (re-inject, no-op on arg-less url(), failure swallowing), and the full mock lifecycle including post-navigation recovery.
  • E2E: new wdio.dioxus-browser.conf.ts (port 8088, same safe static-server + ps-based driver-absence check as the Tauri conf) + mock.spec.ts exercising value-returning and argument-passing invokes.
  • scripts/detect-changes.ts: fixes e2e/test/ dir routing to use serviceForDir (prefix matching) so dioxus-browser correctly routes to dioxus, consistent with the existing wdio.<svc>-*.conf.ts rule; guarded with new spec cases.

Confidence Score: 5/5

Safe to merge — purely additive test and CI changes with no production code modifications.

All changes are tests, fixtures, CI wiring, and a one-line docs note. The new integration and E2E files faithfully mirror the existing Tauri/Electron patterns. The two previously-flagged issues (missing try/catch in the package fixture HTML and unused helper exports) are both resolved in this iteration. The detect-changes routing fix is guarded by new spec cases. No logic regressions are possible in the service itself.

No files require special attention.

Important Files Changed

Filename Overview
packages/dioxus-service/test/integration/browser-mode.integration.spec.ts New integration suite (21 tests) covering browser-mode init, navigation lifecycle, and full mock lifecycle including post-navigation recovery; well-structured with clear per-suite beforeEach/afterEach teardown.
packages/dioxus-service/test/integration/launcher.browser.integration.spec.ts New launcher integration suite covering capability transform, devServerUrl validation, non-first-capability browser-mode detection, and no-driver-spawn guarantee; correct Vitest mock hoisting pattern used.
e2e/wdio.dioxus-browser.conf.ts New WDIO config for Dioxus browser-mode E2E — structurally identical to wdio.tauri-browser.conf.ts with a different port (8088) and service name; includes the same safe static-file-enumeration server and ps-based driver-absence check.
scripts/detect-changes.ts Fixes e2e/test/ dir routing to use serviceForDir (prefix matching) instead of exact match, so suffixed dirs like dioxus-browser correctly route to dioxus; consistent with the existing wdio.*.conf.ts rule.
packages/dioxus-service/test/integration/helpers.ts New test helpers exposing only createFakeBrowser and FakeBrowser — correctly trimmed to what Dioxus's machinery actually needs (no MockUpdateScheduler, per-browser keying, or BiDi preload helpers).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CI as CI (browser-mode job)
    participant Launcher as DioxusLaunchService.onPrepare
    participant Worker as DioxusWorkerService.before
    participant Static as Static HTTP Server (:8088)
    participant Chrome as Chrome (via chromedriver)
    participant Spec as E2E / Integration Spec

    CI->>Launcher: "onPrepare(caps [{browserName:'dioxus', mode:'browser', devServerUrl}])"
    Launcher-->>Launcher: validate devServerUrl, rewrite browserName to chrome, strip dioxus:options
    Launcher-->>CI: caps transformed (no embedded driver spawned)

    CI->>Static: startStaticServer(fixtureRoot) [onPrepare hook]
    Static-->>CI: listening on http://localhost:8088

    CI->>Chrome: launch Chrome session
    Chrome-->>Worker: session ready — before() called

    Worker->>Chrome: browser.url(devServerUrl) [initial navigation]
    Worker->>Chrome: browser.execute(IPC injection script) [installs __WDIO_DIOXUS__ + __wdio_spy__]
    Worker->>Chrome: overwriteCommand('url', patchedUrl) [re-injects after future navigations]
    Worker-->>CI: browser.dioxus API surface installed

    Spec->>Worker: browser.dioxus.mock('get_app_version')
    Worker->>Chrome: browser.execute(register mock script)
    Worker-->>Spec: DioxusMock handle

    Spec->>Chrome: click fetch-version button
    Chrome->>Chrome: invoke('get_app_version') — mock returns '99.0.0-browser'
    Chrome-->>Spec: version-output text updated

    Spec->>Worker: mock.update()
    Worker->>Chrome: browser.execute(read __wdio_spy__ call data)
    Worker-->>Spec: mock.mock.calls populated

    CI->>Static: close() [onComplete hook]
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 CI as CI (browser-mode job)
    participant Launcher as DioxusLaunchService.onPrepare
    participant Worker as DioxusWorkerService.before
    participant Static as Static HTTP Server (:8088)
    participant Chrome as Chrome (via chromedriver)
    participant Spec as E2E / Integration Spec

    CI->>Launcher: "onPrepare(caps [{browserName:'dioxus', mode:'browser', devServerUrl}])"
    Launcher-->>Launcher: validate devServerUrl, rewrite browserName to chrome, strip dioxus:options
    Launcher-->>CI: caps transformed (no embedded driver spawned)

    CI->>Static: startStaticServer(fixtureRoot) [onPrepare hook]
    Static-->>CI: listening on http://localhost:8088

    CI->>Chrome: launch Chrome session
    Chrome-->>Worker: session ready — before() called

    Worker->>Chrome: browser.url(devServerUrl) [initial navigation]
    Worker->>Chrome: browser.execute(IPC injection script) [installs __WDIO_DIOXUS__ + __wdio_spy__]
    Worker->>Chrome: overwriteCommand('url', patchedUrl) [re-injects after future navigations]
    Worker-->>CI: browser.dioxus API surface installed

    Spec->>Worker: browser.dioxus.mock('get_app_version')
    Worker->>Chrome: browser.execute(register mock script)
    Worker-->>Spec: DioxusMock handle

    Spec->>Chrome: click fetch-version button
    Chrome->>Chrome: invoke('get_app_version') — mock returns '99.0.0-browser'
    Chrome-->>Spec: version-output text updated

    Spec->>Worker: mock.update()
    Worker->>Chrome: browser.execute(read __wdio_spy__ call data)
    Worker-->>Spec: mock.mock.calls populated

    CI->>Static: close() [onComplete hook]
Loading

Reviews (3): Last reviewed commit: "test(dioxus): address review — fixture t..." | Re-trigger Greptile

- package-test browser fixture: wrap the fetch-info invoke in try/catch (matching
  the e2e fixture) so a pre-injection click surfaces as visible output instead of
  an unhandled rejection.
- integration helpers.ts: remove the unused defer/createFakeMock/flushMicrotasks
  exports (and their types + the DioxusMock import) — Dioxus's specs only use
  createFakeBrowser, so the rest was dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@goosewobbler goosewobbler merged commit 2ef83ee into main Jun 29, 2026
479 of 491 checks passed
@goosewobbler goosewobbler deleted the feat/dioxus-browser-test-parity branch June 29, 2026 23:10
goosewobbler added a commit that referenced this pull request Jun 30, 2026
…on tests (#493)

* chore: release @wdio/flutter-service@1.0.0-next.1 [skip ci]

* ci(release): use releasekit refresh-after-release for the post-release reconcile (#486)

releasekit 0.34.0 shipped `refresh-after-release` (goosewobbler/releasekit#438) — the
purpose-built post-release command that reconciles the standing PR (closes it if empty,
else rebuilds the release branch against the new HEAD) and refreshes feeder-PR previews.
Our two custom reconcile-after-* jobs duplicated the reconcile half via
standing-pr-update --reconcile.

- Add a `Refresh standing PR after release` step to the release job; it runs after a
  scoped, non-dry release and pushes over the checkout's SSH deploy key so the standing
  PR's CI re-runs (a GITHUB_TOKEN push wouldn't).
- Drop the `reconcile-after-auto` / `reconcile-after-manual` jobs and the `reconcile_after`
  dispatch input from release.yml.
- Drop the now-unused `reconcile` input from _standing-pr-update.reusable.yml (only the
  routine push-triggered update uses it now, without reconcile).

Behaviour change: manual releases now always reconcile (the `reconcile_after` opt-in is
gone) — the native command is the post-release default. The standing-pr-publish path is
unchanged (the step is gated to the scoped-release path). Feeder-PR preview refresh stays
a no-op until ci.prPreview.refreshAfterRelease is enabled (follow-up).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): bump releasekit to v0.35.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): declare service packages as releasekit primaryPackages

Render the standing-PR checklist as release units (one checkbox per
top-level service, coupled members and prerequisites nested beneath),
using the releasekit 0.35.0 primaryPackages feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): add dioxus linked group; skip native-mobile-core github release

Group the dioxus service + bridge npm package and the standalone
embedded-driver/driver crates in lockstep. The colocated wdio-dioxus-bridge
crate is driven by its sibling npm package, so it is not listed separately.
Exclude @wdio/native-mobile-core from GitHub releases like the other shared
internal packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(flutter): promote wdio_flutter to top-level packages/flutter-bridge

Move the cooperative Dart contract out of the flutter-service package up to a
top-level dir, matching how every other independently-published unit (the
dioxus crates, the tauri plugins) sits as a packages/* sibling. The published
pub.dev name stays wdio_flutter — only the directory changes.

Repointed: releasekit pub.paths, the e2e app's path: dep, the detect-changes
spec, the pubspec homepage, flutter-service README links, the ROADMAP link,
the release-workflow comment, and the AGENTS.md structure tree. flutter-bridge
rides the existing `flutter-` rule in detect-changes (no code change). Added a
package .gitignore for .dart_tool/ + pubspec.lock (libraries don't commit the
lockfile), closing the gap that was leaking them as untracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): bump releasekit to v0.36.0 and drop version.skip

0.36.0 excludes private/unpublishable packages from the release flow by
default (npm `private: true`, Cargo `publish = false`, pub `publish_to: none`),
so the hand-maintained version.skip list is redundant — removed it.

To make privacy actually drive the skip, fixed the manifests it was masking:
- e2e/package.json: `"private": "true"` (string) → `true` (boolean); releasekit
  has a strict `private !== true` check that the string slipped past.
- mark the publishable fixture Cargo crates `publish = false`: tauri-e2e-app,
  tauri-app-example (hybrid: private npm root + a publishable src-tauri crate
  the name-based skip was hiding), and dioxus-package-test-app (a pre-existing
  leak — never in version.skip, so it was being versioned/tagged on every
  dioxus release).

Verified via `version --dry-run`: none of the 19 formerly-skipped packages
nor any fixture crate enters the release set; real packages still bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(dioxus): browser-mode test parity (integration + E2E + fixture) (#489)

* test(dioxus): browser-mode test parity (integration + E2E + fixture)

Brings Dioxus browser-mode test coverage up to the Electron/Tauri template
(closes #472). Dioxus has no native event bus, so the events tier is omitted
by design (matching the Tauri/Electron docs' explicit-omission convention).

- Integration: add browser-mode.integration.spec.ts + launcher.browser.integration.spec.ts
  (+ helpers.ts) under packages/dioxus-service/test/integration/. Covers the
  launcher→worker handshake (capability→chrome transform, dioxus:options
  removal, devServerUrl validation/propagation, no embedded driver spawned)
  and the worker browser-mode machinery (initial navigation + IPC injection,
  url() override re-injecting after navigation, single shared mock store,
  mock register/update/restore lifecycle). 21 tests, all green.
- E2E: add e2e/wdio.dioxus-browser.conf.ts (real Chrome, self-hosted static
  dev server, ps-based no-Dioxus-driver assertion) + e2e/test/dioxus-browser/mock.spec.ts
  (value-returning invoke + invoke-with-arguments via browser.dioxus.mock).
  New fixtures/e2e-apps/dioxus-browser/ app drives window.__WDIO_DIOXUS__.invoke.
- Package fixture: extend fixtures/package-tests/dioxus-app to actually exercise
  browser.dioxus.mock('get_info') + mock.update() (button + invoke in browser/index.html).
- Docs: note in packages/dioxus-service/docs/browser-mode.md that events are
  unsupported (no native event bus; see packages/native-types/src/dioxus.ts).
- CI: wire pnpm e2e:dioxus-browser into _ci-browser-mode.reusable.yml + the
  e2e/root package.json scripts; route e2e/test/<svc>-*/ test dirs to their
  service in detect-changes (so dioxus-browser specs trigger only dioxus) with
  regression guards in detect-changes.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(dioxus): address review — fixture try/catch, drop unused helpers

- package-test browser fixture: wrap the fetch-info invoke in try/catch (matching
  the e2e fixture) so a pre-injection click surfaces as visible output instead of
  an unhandled rejection.
- integration helpers.ts: remove the unused defer/createFakeMock/flushMicrotasks
  exports (and their types + the DioxusMock import) — Dioxus's specs only use
  createFakeBrowser, so the rest was dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): clear errors for non-chrome browserName and unreachable dev server (#490)

* fix(browser-mode): clear errors for non-chrome browserName and unreachable dev server

Two browser-mode (#260) misconfigurations failed in ways that didn't match
the acceptance criterion (a misconfiguration should surface a clear,
actionable error). Both are fixed across the Tauri, Electron, and Dioxus
launchers via a shared helper in @wdio/native-core.

Fix 1 — non-chrome browserName was silently overwritten to 'chrome'. The
launcher now throws SevereServiceError when browserName is set to a value the
service can't drive in browser mode. Each service allows its own framework
display/detection name plus chrome (Electron: electron, Tauri: tauri/wry,
Dioxus: dioxus) so legitimate native-mode capabilities aren't rejected; only
genuinely foreign browsers (e.g. firefox) error.

Fix 2 — an unreachable dev server failed late inside browser.url(). The
launcher now preflights devServerUrl in onPrepare with a HEAD probe (fetch +
AbortSignal.timeout) and throws SevereServiceError("Dev server not reachable
at <url> — is it running?") before any session is created.

warn-vs-throw: throw, matching the existing fatal-misconfig pattern already
used for the missing/invalid devServerUrl cases (SevereServiceError stops the
runner with a clear message rather than warning and proceeding).

DRY: the three launchers hand-rolled the browser-mode block with no shared
helper, so the new logic lives once in @wdio/native-core (browserMode.ts:
nonChromeBrowserNameError + probeDevServerReachable, returning Result) and
each launcher throws its own SevereServiceError, consistent with the existing
inline devServerUrl handling.

Unit coverage added for both branches per service plus the shared helper.

Closes #416

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): address review — chrome-guided error, electron two-pass probe

- nonChromeBrowserNameError: the message now consistently guides to 'chrome'
  (the allow-list also carries tolerated native names like 'electron'/'wry' that
  shouldn't be advertised as a value to set).
- electron launcher: split the browser-mode block into validate-all-browserNames
  → probe-each-distinct-url-once → mutate, matching the Tauri/Dioxus two-pass.
  A non-chrome cap now fails fast before any dev-server probe, and a shared
  devServerUrl is probed once instead of per-cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): narrow the caught probe error instead of casting

Use `error instanceof Error ? error.message : String(error)` rather than an
unchecked `(error as Error)` cast on the caught unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): stub dev-server probe in browser-mode launcher integration tests

#490 added a fetch HEAD dev-server probe to the dioxus launcher's onPrepare; #489's
launcher.browser.integration.spec.ts calls the real onPrepare on the happy path and
predates the probe, so it now hits an absent server and fails with
"Dev server not reachable" (main went red when both landed). Stub fetch reachable in
beforeEach (matching the sibling tauri/electron specs #490 already stubbed); error-path
tests throw before the probe and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
goosewobbler added a commit that referenced this pull request Jun 30, 2026
* feat(dioxus): implement W3C Actions API in the embedded driver

Wire POST/DELETE /session/{id}/actions to a real handler instead of the
not_implemented stub. Unlocks element.click(options), doubleClick, moveTo,
drag-and-drop, and browser.action(...)/actions(...) chains (pointer, key,
wheel, pause).

The Dioxus driver has no native input path — everything runs as JavaScript
inside the webview through the bridge eval channel — so pointer/key/wheel
actions are synthesized as DOM events (MouseEvent / KeyboardEvent /
WheelEvent) dispatched on the element under the resolved coordinates.

Pointer origin is resolved up front (viewport / pointer / element-center via
getBoundingClientRect, mirroring the element rect endpoint) so a
.click(options) on an element hits that element's center rather than viewport
(0,0). This is the bug #423 hit on the Tauri side; it is landed correctly here
from the start.

Closes #427

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): address review on the Actions handler

- buttons bitmask (P1): MouseEvent.buttons now reflects the cumulative held
  buttons at fire time, not just the triggering button. A left mouseup reports
  buttons:0, click/contextmenu report buttons:0, and mousemove reports the
  currently-held mask. perform() and release() track held_mask through
  press/release; pointer_event_js takes an explicit `buttons` arg.
- stale element (P2): eval_point returns stale_element_reference (404) when the
  element-center eval yields JS null (element gone), instead of unknown_error.
- tick ordering (P2): documented that sources are dispatched serially (matching
  the Tauri handler); cross-source tick interleaving tracked in #491.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): synthesize dblclick for element.doubleClick()

A second primary click on the same spot within one performActions now emits a
`dblclick` MouseEvent after the `click`, matching what a real browser produces
for element.doubleClick()'s two press/release pairs (tracked via last_click_pos,
local to the request so separate clicks don't merge into a double-click).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dioxus): accept pointerCancel in the embedded driver Actions API

A pointerCancel is a valid W3C pointer action, but without the enum variant
serde rejected it and 400'd the entire performActions request. Accept it and
release the source's held buttons (a cancel is not a click, so no MouseEvent
is synthesized). Addresses a gap flagged in the Greptile review of #488.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): translate special-key codepoints and reset dblclick tracking on move

Two defects in the embedded driver's Actions handler flagged by Greptile:

- Special keys arrive as W3C Private-Use-Area codepoints (Key.Enter = \u{E007},
  arrows, F-keys, modifiers). They were forwarded verbatim as KeyboardEvent.key,
  so Key.* constants produced a garbage `key`. Add normalize_key (mirroring the
  tauri-plugin-wdio-webdriver table) and translate before synthesizing the event.
- last_click_pos was never cleared on pointerMove, so click → move away → click
  back at the same coords fired a spurious dblclick. Reset it on every move;
  doubleClick() never moves between its press/release pairs, so real dblclicks
  are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): carry keyboard modifier flags on synthesized action events

Greptile flagged that pointer_event_js/key_event_js never read the session's
pressed_keys, so a held Control/Shift/Alt/Meta produced MouseEvents/KeyboardEvents
with ctrlKey/shiftKey/etc. all false — Ctrl+click handlers, multi-select, and
shortcut handlers wouldn't observe the modifier.

Add a Modifiers value derived from the held modifier keys (left + right PUA
codepoints), kept in sync as key actions press/release and seeded from modifiers
still held from a prior performActions call, and stamp ctrlKey/shiftKey/altKey/
metaKey onto every synthesized pointer and key event.

Interleaving a modifier with a pointer *within a single* performActions (e.g.
Ctrl+click in one call) additionally needs tick-by-tick processing — still
tracked separately in #491.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): right-variant modifier keys + decreasing modifiers on release

- normalize_key now maps the right-hand modifier codepoints (\u{E050}-\u{E053})
  to Shift/Control/Alt/Meta, so a right modifier dispatches the correct DOM
  KeyboardEvent.key instead of the raw PUA codepoint. (from_keys already set the
  flags for these; only the key name was missing.)
- release() emits each keyup with the modifiers still held after that key is
  released (computed from the remaining held keys), instead of a flat
  Modifiers::default() — so releasing Shift while Control is down reports
  shiftKey:false, ctrlKey:true. The mouseups run after all keys are up, so their
  empty modifiers stay correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): guard button_to_mask against out-of-range button indices

`1 << n` overflows for button index >= 32 — a panic in debug/test builds, a
silent wrap in release (button 32 aliasing button 0). A malformed request could
crash the driver. Out-of-range indices now contribute no bit (W3C defines 0–4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dioxus): process Actions tick-by-tick across sources (#491)

Dispatch the action at index N from every source before index N+1 from any
source, per the W3C Actions tick model, instead of draining each source serially.
Cross-source chains now interleave correctly — e.g. a key source + a pointer
source in one performActions (Ctrl+click) holds the modifier down when the click
lands. Per-action behaviour is unchanged; only the iteration order changes.

Implements the Dioxus half of #491 (Tauri half tracked separately).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): set KeyboardEvent.code and deterministic keyup order

Addresses two Greptile observations on the W3C Actions handler:

- Synthesized KeyboardEvents now carry `code` (the physical key), so apps
  reading `event.code` for keyboard shortcuts receive it. Derived for the
  special keys, ASCII letters (`KeyA`) and digits (`Digit1`) shortcuts use;
  left/right modifier variants map to distinct `*Left`/`*Right` codes.
  Layout-dependent symbols fall back to `""` (the spec default).
- The release handler drains held keys from a HashSet (unordered); sort
  before dispatching keyups so the order is deterministic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): stub dev-server probe in browser-mode launcher integration tests (#493)

* chore: release @wdio/flutter-service@1.0.0-next.1 [skip ci]

* ci(release): use releasekit refresh-after-release for the post-release reconcile (#486)

releasekit 0.34.0 shipped `refresh-after-release` (goosewobbler/releasekit#438) — the
purpose-built post-release command that reconciles the standing PR (closes it if empty,
else rebuilds the release branch against the new HEAD) and refreshes feeder-PR previews.
Our two custom reconcile-after-* jobs duplicated the reconcile half via
standing-pr-update --reconcile.

- Add a `Refresh standing PR after release` step to the release job; it runs after a
  scoped, non-dry release and pushes over the checkout's SSH deploy key so the standing
  PR's CI re-runs (a GITHUB_TOKEN push wouldn't).
- Drop the `reconcile-after-auto` / `reconcile-after-manual` jobs and the `reconcile_after`
  dispatch input from release.yml.
- Drop the now-unused `reconcile` input from _standing-pr-update.reusable.yml (only the
  routine push-triggered update uses it now, without reconcile).

Behaviour change: manual releases now always reconcile (the `reconcile_after` opt-in is
gone) — the native command is the post-release default. The standing-pr-publish path is
unchanged (the step is gated to the scoped-release path). Feeder-PR preview refresh stays
a no-op until ci.prPreview.refreshAfterRelease is enabled (follow-up).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): bump releasekit to v0.35.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): declare service packages as releasekit primaryPackages

Render the standing-PR checklist as release units (one checkbox per
top-level service, coupled members and prerequisites nested beneath),
using the releasekit 0.35.0 primaryPackages feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): add dioxus linked group; skip native-mobile-core github release

Group the dioxus service + bridge npm package and the standalone
embedded-driver/driver crates in lockstep. The colocated wdio-dioxus-bridge
crate is driven by its sibling npm package, so it is not listed separately.
Exclude @wdio/native-mobile-core from GitHub releases like the other shared
internal packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(flutter): promote wdio_flutter to top-level packages/flutter-bridge

Move the cooperative Dart contract out of the flutter-service package up to a
top-level dir, matching how every other independently-published unit (the
dioxus crates, the tauri plugins) sits as a packages/* sibling. The published
pub.dev name stays wdio_flutter — only the directory changes.

Repointed: releasekit pub.paths, the e2e app's path: dep, the detect-changes
spec, the pubspec homepage, flutter-service README links, the ROADMAP link,
the release-workflow comment, and the AGENTS.md structure tree. flutter-bridge
rides the existing `flutter-` rule in detect-changes (no code change). Added a
package .gitignore for .dart_tool/ + pubspec.lock (libraries don't commit the
lockfile), closing the gap that was leaking them as untracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(release): bump releasekit to v0.36.0 and drop version.skip

0.36.0 excludes private/unpublishable packages from the release flow by
default (npm `private: true`, Cargo `publish = false`, pub `publish_to: none`),
so the hand-maintained version.skip list is redundant — removed it.

To make privacy actually drive the skip, fixed the manifests it was masking:
- e2e/package.json: `"private": "true"` (string) → `true` (boolean); releasekit
  has a strict `private !== true` check that the string slipped past.
- mark the publishable fixture Cargo crates `publish = false`: tauri-e2e-app,
  tauri-app-example (hybrid: private npm root + a publishable src-tauri crate
  the name-based skip was hiding), and dioxus-package-test-app (a pre-existing
  leak — never in version.skip, so it was being versioned/tagged on every
  dioxus release).

Verified via `version --dry-run`: none of the 19 formerly-skipped packages
nor any fixture crate enters the release set; real packages still bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(dioxus): browser-mode test parity (integration + E2E + fixture) (#489)

* test(dioxus): browser-mode test parity (integration + E2E + fixture)

Brings Dioxus browser-mode test coverage up to the Electron/Tauri template
(closes #472). Dioxus has no native event bus, so the events tier is omitted
by design (matching the Tauri/Electron docs' explicit-omission convention).

- Integration: add browser-mode.integration.spec.ts + launcher.browser.integration.spec.ts
  (+ helpers.ts) under packages/dioxus-service/test/integration/. Covers the
  launcher→worker handshake (capability→chrome transform, dioxus:options
  removal, devServerUrl validation/propagation, no embedded driver spawned)
  and the worker browser-mode machinery (initial navigation + IPC injection,
  url() override re-injecting after navigation, single shared mock store,
  mock register/update/restore lifecycle). 21 tests, all green.
- E2E: add e2e/wdio.dioxus-browser.conf.ts (real Chrome, self-hosted static
  dev server, ps-based no-Dioxus-driver assertion) + e2e/test/dioxus-browser/mock.spec.ts
  (value-returning invoke + invoke-with-arguments via browser.dioxus.mock).
  New fixtures/e2e-apps/dioxus-browser/ app drives window.__WDIO_DIOXUS__.invoke.
- Package fixture: extend fixtures/package-tests/dioxus-app to actually exercise
  browser.dioxus.mock('get_info') + mock.update() (button + invoke in browser/index.html).
- Docs: note in packages/dioxus-service/docs/browser-mode.md that events are
  unsupported (no native event bus; see packages/native-types/src/dioxus.ts).
- CI: wire pnpm e2e:dioxus-browser into _ci-browser-mode.reusable.yml + the
  e2e/root package.json scripts; route e2e/test/<svc>-*/ test dirs to their
  service in detect-changes (so dioxus-browser specs trigger only dioxus) with
  regression guards in detect-changes.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(dioxus): address review — fixture try/catch, drop unused helpers

- package-test browser fixture: wrap the fetch-info invoke in try/catch (matching
  the e2e fixture) so a pre-injection click surfaces as visible output instead of
  an unhandled rejection.
- integration helpers.ts: remove the unused defer/createFakeMock/flushMicrotasks
  exports (and their types + the DioxusMock import) — Dioxus's specs only use
  createFakeBrowser, so the rest was dead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): clear errors for non-chrome browserName and unreachable dev server (#490)

* fix(browser-mode): clear errors for non-chrome browserName and unreachable dev server

Two browser-mode (#260) misconfigurations failed in ways that didn't match
the acceptance criterion (a misconfiguration should surface a clear,
actionable error). Both are fixed across the Tauri, Electron, and Dioxus
launchers via a shared helper in @wdio/native-core.

Fix 1 — non-chrome browserName was silently overwritten to 'chrome'. The
launcher now throws SevereServiceError when browserName is set to a value the
service can't drive in browser mode. Each service allows its own framework
display/detection name plus chrome (Electron: electron, Tauri: tauri/wry,
Dioxus: dioxus) so legitimate native-mode capabilities aren't rejected; only
genuinely foreign browsers (e.g. firefox) error.

Fix 2 — an unreachable dev server failed late inside browser.url(). The
launcher now preflights devServerUrl in onPrepare with a HEAD probe (fetch +
AbortSignal.timeout) and throws SevereServiceError("Dev server not reachable
at <url> — is it running?") before any session is created.

warn-vs-throw: throw, matching the existing fatal-misconfig pattern already
used for the missing/invalid devServerUrl cases (SevereServiceError stops the
runner with a clear message rather than warning and proceeding).

DRY: the three launchers hand-rolled the browser-mode block with no shared
helper, so the new logic lives once in @wdio/native-core (browserMode.ts:
nonChromeBrowserNameError + probeDevServerReachable, returning Result) and
each launcher throws its own SevereServiceError, consistent with the existing
inline devServerUrl handling.

Unit coverage added for both branches per service plus the shared helper.

Closes #416

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): address review — chrome-guided error, electron two-pass probe

- nonChromeBrowserNameError: the message now consistently guides to 'chrome'
  (the allow-list also carries tolerated native names like 'electron'/'wry' that
  shouldn't be advertised as a value to set).
- electron launcher: split the browser-mode block into validate-all-browserNames
  → probe-each-distinct-url-once → mutate, matching the Tauri/Dioxus two-pass.
  A non-chrome cap now fails fast before any dev-server probe, and a shared
  devServerUrl is probed once instead of per-cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(browser-mode): narrow the caught probe error instead of casting

Use `error instanceof Error ? error.message : String(error)` rather than an
unchecked `(error as Error)` cast on the caught unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): stub dev-server probe in browser-mode launcher integration tests

#490 added a fetch HEAD dev-server probe to the dioxus launcher's onPrepare; #489's
launcher.browser.integration.spec.ts calls the real onPrepare on the happy path and
predates the probe, so it now hits an absent server and fails with
"Dev server not reachable" (main went red when both landed). Stub fetch reachable in
beforeEach (matching the sibling tauri/electron specs #490 already stubbed); error-path
tests throw before the probe and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dioxus): deterministic multi-button release + pointerCancel click-chain reset

Addresses the two remaining Greptile edge-case observations on the W3C
Actions handler (both unusual sequences, off the common WDIO paths):

- releaseActions drains held buttons from a HashMap<_, HashSet> (both
  unordered), so a multi-button release dispatched its mouseups in a
  non-deterministic order. Collect and sort first — same fix class as the
  keyup ordering. Each mouseup still reports the buttons held after it.
- pointerCancel aborts the gesture but left `last_click_pos` set, so a later
  same-spot click could be misread as a dblclick. Reset it on cancel,
  mirroring the pointerMove break in the click chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.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.

Browser-mode test parity for Dioxus

1 participant