feat(dioxus): implement W3C Actions API in the embedded driver#488
Conversation
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>
|
Standing release PR: #456 · 9 packages queued · open 158h 5m · ✅ ready to merge Release Preview — 12 packages
These changes will be added to the release PR (#456) when merged: Changelog@wdio/dioxus-bridge 1.0.0-next.3 → 1.1.0Changed
@wdio/dioxus-service 1.0.0-next.3 → 1.1.0Fixed
wdio-dioxus-driver 1.0.0-next.3 → 1.1.0Changed
wdio-dioxus-embedded-driver 1.0.0-next.3 → 1.1.0Added
Fixed
@wdio/tauri-service 1.2.0 → 1.3.0Changed
@wdio/flutter-service 1.0.0-next.1 → 1.1.0Changed
wdio_flutter N/A → 1.1.0Changed
@wdio/electron-service 10.1.0 → 10.2.0Changed
@wdio/native-core 1.0.0 → 1.1.0Changed
@wdio/native-mobile-core 1.0.0 → 1.1.0Changed
@wdio/native-spy 1.1.0 → 1.2.0Changed
@wdio/native-utils 2.4.0 → 2.5.0Changed
@wdio/react-native-service 1.0.0-next.0 → 1.1.0Changed
After merge — predicted release
Updated automatically by ReleaseKit |
- 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>
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>
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>
…king 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>
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>
…lease
- 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>
`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>
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>
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>
…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>
…actions # Conflicts: # packages/dioxus-service/test/integration/launcher.browser.integration.spec.ts
…-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>
|
Greptile's two latest DOM-completeness observations — |
Summary
Implements the W3C Actions endpoint (
POST/DELETE /session/{id}/actions) in@wdio/dioxus-embedded-driver, which was previously wired to thenot_implementedstub. Any WDIO command routed through the Actions API failed withunsupported operation("not implemented for Dioxus embedded driver").Closes #427.
Affected commands now unlocked
element.click(options)(.click({ button: 'right' }),.click({ x, y })),element.doubleClick(),element.moveTo(), drag-and-drop, andbrowser.action(...)/browser.actions(...)chains (pointer, key, wheel, pause). (Bare.click()already worked via theelementClickendpoint.)Handler design
The Dioxus embedded driver has no native input path — unlike the Tauri driver (which dispatches real OS-level events through a native platform executor), every Dioxus command runs as JavaScript inside the webview through the bridge eval channel. So pointer/key/wheel actions are synthesized as DOM events dispatched on the element under the resolved coordinates, matching what a real browser produces for input:
pointerDown/pointerUp/pointerMove→MouseEvent(mousedown/mouseup/mousemove) dispatched ondocument.elementFromPoint(x, y). A primary press + release at the same spot synthesizes aclick; a secondary (right) release synthesizescontextmenu— the events the browser would emit for real input, so element handlers fire.keyDown/keyUp→KeyboardEventondocument.activeElement.scroll→WheelEventcarryingdeltaX/deltaY.tokio::time::sleep.Pointer position and pressed keys/buttons persist on the session's new
ActionStatesoorigin: "pointer"resolves relative to the last position and theDELETErelease emits the matchingkeyUp/pointerUpevents.originresolution (does NOT reproduce #423)#427 is the Dioxus sibling of #423 (Tauri), where
PointerMovewas missing theoriginfield, so.click(options)silently clicked viewport(0, 0)and missed the element.originis parsed and resolved correctly here from the start, mirroring the fixed Tauri handler:"viewport"(or no origin) → x/y are absolute viewport coordinates"pointer"→ x/y are relative to the current pointer position{ "element-6066-…": "<id>" }→ x/y are offsets from the element's in-view center, computed viagetBoundingClientRect()(reusing the same rect lookup behind the elementrectendpoint)So
element.click(options)— which WDIO sends as apointerMovewith an element origin and x/y defaulting to 0 — lands on the element's center, not viewport(0, 0). An unrecognised named origin is rejected withinvalid argumentrather than silently treated as viewport.Verification
Run from
packages/dioxus-embedded-driver:cargo buildcargo testcargo clippy --all-targetsoriginresolution is covered by unit tests on the pure JS-builder + deserialization logic: the click-options shape (element origin, x/y = 0) deserializes toOrigin::Elementwith the right element ref; the element-center JS adds half-width/half-height to the rect origin (the #423 regression guard); named"viewport"/"pointer"origins parse; and key/wheel/pause sequences round-trip.Cargo.lockis unchanged (no new dependencies — reusesserde,serde_json,tokio,uuid,axum, and the bridge).Limitations
durationis honored as a pause before the final move (no intermediate-tick interpolation, matching the Tauri handler).🤖 Generated with Claude Code