Skip to content

feature(ios-web): Adding support for ios simulator, physical and web beta#54

Merged
BlueShork merged 93 commits into
mainfrom
feature/ios-support
Jun 7, 2026
Merged

feature(ios-web): Adding support for ios simulator, physical and web beta#54
BlueShork merged 93 commits into
mainfrom
feature/ios-support

Conversation

@BlueShork

Copy link
Copy Markdown
Owner

Pull request

What

Why

Closes #

How

Checklist

  • Title follows Conventional Commits (e.g., feat(inspector): ...)
  • pnpm typecheck && pnpm lint passes
  • cargo fmt --check && cargo clippy -- -D warnings passes
  • cargo test --lib passes
  • New behavior is covered by a test (or a clear note why not)
  • Docs / comments updated where the change is non-obvious
  • No new unwrap() / expect() in production code paths

BlueShork added 30 commits June 7, 2026 21:40
BlueShork added 26 commits June 7, 2026 21:40
…pace

The screenshot covers only the CSS viewport (e.g. 1200x766) but maestro's
element list includes below-the-fold nodes and a full-document wrapper
whose bounds run far past it (e.g. 1860 tall). The frontend inferred the
hierarchy coordinate space from the max bounds in the tree, so it picked
up that document height and compressed the inspector overlay vertically.

Two parts: the screenshot poller already reports the CSS viewport in the
web_frame payload, but the paint hook overwrote it with the PNG's natural
(devicePixelRatio) size; now we paint at native resolution but report the
CSS size as the device coordinate space. And for web, hierarchyDims is
the reported viewport (streamW/H) rather than the tree's max bounds.
Together with the viewport-sized root (35b47ab) the overlay and hit-test
line up; below-fold elements fall outside the visible canvas as expected.
iOS is unaffected (its reported dims equal the bitmap's natural size).
maestro drives web via a Selenium-managed chromedriver that launches a
headed Chrome. Our teardown SIGKILLs the `maestro studio` JVM (stop /
kill_on_drop), which reaps neither the driver nor the browser, so headed
Chrome windows piled up across sessions. Sweep the orphaned
chromedriver + webdriver-flagged Chrome on web keeper start (cull a prior
session's leftovers) and stop (close the window we opened). Matched by
selenium/chromedriver and --test-type=webdriver markers a user's normal
Chrome never carries, so personal browsing is untouched.
A fast Cmd+Q (macOS) or window close used to terminate the app without
running the async teardown, leaving orphaned maestro studio / chromedriver
/ Chrome / iproxy processes behind (kill_on_drop doesn't fire on a hard
exit). Now both the window CloseRequested and app-level ExitRequested are
held: the backend emits `quit-requested` and the frontend either asks for
confirmation (default) or, if the user opted out, quits straight away.

Either path calls `confirm_quit`, which flips `quit_confirmed`, tears down
every session via the extracted `teardown_all_sessions` (shared with
disconnect_device), then exits — so nothing is orphaned regardless of the
prompt. The dialog has a 'Don't ask me again' checkbox; the preference is
persisted and re-enableable from Settings → General.
The default macOS app menu's Quit calls AppKit `terminate:`, which
hard-exits without a preventable RunEvent::ExitRequested — so Cmd+Q
bypassed the confirm-before-quit dialog (the window close button worked
because it goes through WindowEvent::CloseRequested). Replace the menu on
macOS with one whose Quit is a plain item bound to Cmd+Q that emits
`quit-requested` instead of terminating; the rest mirrors the macOS
defaults (predefined Edit/Window items keep native copy/paste/undo). The
ExitRequested guard stays as a safety net for other exit paths; Windows/
Linux are unaffected (no app menu; they quit via the window close path).
Adds physical iPhone support alongside the existing iOS Simulator,
Android, and Web paths — purely additive. devicelab's maestro-ios-device
builds/runs the XCTest HTTP driver on the device (:22087) and forwards a
local port (6001) over usbmux, removing the go-ios tunnel blocker that
made stock maestro unable to drive a physical device.

- device: `physical: bool` on Device; ios discovery merges simctl sims +
  `devicectl list devices` physical iPhones (connected-only: real hardware
  with a transport, tunnelState != unavailable — a plugged-in iOS 17+
  device idles at 'disconnected' and must still list).
- ios_session: IosDriverKeeper gains an IosTarget {Simulator, Physical}.
  Physical spawns `maestro-ios-device --team-id --device`, HTTP client on
  the forwarded port, and pulls preview frames from the driver's HTTP
  /screenshot (sim path — simctl boot + maestro studio + simctl shots —
  unchanged). Reuses wait_until_ready / hierarchy / input / deviceInfo.
- commands: ensure_ios_keeper reads device.physical from state; run_flow
  physical branch keeps the bridge alive and runs via
  spawn_ios_device_runner (`--driver-host-port`), unlike the sim path.
- runner: spawn_ios_device_runner with a `--driver-host-port` preflight
  that surfaces a clear 'install devicelab patched maestro' error.
- tool_paths: maestro_ios_device binary path (+ getter, view, set).
- frontend: physical iPhones render in the main device list (booted=false
  but physical=true) labelled '… · device'; tool-paths Settings field.

Not yet validated on a real device (user tests later): needs maestro-ios-
device installed, Xcode, an Apple Team ID, and a Developer-Mode iPhone.
161 Rust tests + 146 frontend tests, build/clippy/typecheck/lint green.
Removes the manual `curl … setup.sh` step for physical iOS — only users
who actually want it trigger the download. New `install_ios_device_bridge`
command fetches the devicelab release binary for the Mac's arch
(arm64/amd64), marks it executable, runs its `setup` (which pulls the
XCTest runner + patched maestro jars into ~/.maestro), and persists the
path as the maestro_ios_device override. `ios_device_bridge_installed`
backs an 'Install automatically' button in Settings → Tool paths (shown
only when the bridge is missing). devicelab is Apache-2.0, so fetching its
release is fine; we host nothing. Keeper's not-found error now points at
that button. Build/clippy/typecheck/lint + 146 FE tests green.
Patched maestro 2.5.1 registers --driver-host-port with hidden=true, so it
never appears in `maestro --help`. The physical-device flow-run preflight
now probes `maestro --driver-host-port 6001 test --help` and treats the flag
being accepted (no picocli "Unknown option"/"Unmatched argument") as support,
instead of grepping help text — which gave a false negative and blocked
physical-device runs.
Avoid flashing false ✗ rows (maestro/Xcode/bridge "not installed") before
the async status probe and the parent's bridge/team-id load resolve — render
a "Checking your setup…" placeholder until both are known.
The screenshot poller slept a fixed 350ms after each frame, so physical
devices ran at ~1.8 fps (200ms capture + 350ms sleep). The driver's
/screenshot is itself ~200ms (the device-side ceiling, ~5 fps, confirmed:
parallel requests don't beat sequential — capture is serialized), so poll
back-to-back for physical devices and let capture latency pace us. Errors
back off 500ms to avoid spinning when the device is unplugged. Simulator
cadence (pre-framebuffer fallback) is unchanged.
…hork)

The in-app "Install automatically" now fetches the bridge + patched maestro
2.5.1 jars + XCTest runner from BlueShork/maestro-ios-device (public) instead
of upstream devicelab, which only patches maestro 2.0.9-2.1.0. Updates the
Tools hints accordingly. This makes physical-iPhone setup one-click for users
on maestro 2.5.1.
The first physical-device connect builds the XCTest driver (~10 min), so the
generic "Waiting for frames…" looked frozen. Show a dedicated state for
physical iOS only: animated spinner, a phase message (connecting → building),
the ~10 min heads-up, and a running elapsed timer so it's clearly progressing.
The inspect action menu is a React child of the device-view container that
drives the inspector (hover on pointer-move, tap on pointer-down, swipe on
wheel). React events bubble up the React tree regardless of the menu's fixed
positioning, so moving/clicking/scrolling inside the menu re-triggered the
inspector underneath (extra hovers and phantom taps on the device). Stop
pointer/click/wheel/contextmenu propagation at the menu root; menu buttons sit
deeper so their handlers still fire, and outside clicks still close it.
Maestro's command output mirrors ElementSelector.description(): a text selector
is quoted ("Welcome") but an id selector is unquoted (id: welcomeMessage), and
inputText prints its value unquoted. parseLine's regexes required quotes, so
id-based assertVisible/tapOn (and unquoted inputText) produced no StepEvent and
their editor lines never turned green. Capture the target up to the run
indicator / 'is visible', then normalize quoted text, 'id: <v>', or the raw
value to the bare arg so it matches the YAML AST. Verified against Maestro's
ElementSelectorTest ("Assert that id: hello_element is visible").
The Run→Stop toggle only fired after ipc.runFlow returned. On physical
iOS that round-trip awaits maestro_supports_driver_host_port, which boots
the maestro JVM (maestro test --help) on every run — several seconds where
the button looked stuck on Run.

- frontend: optimistic 'starting' state posted on click (disabled
  'Starting…' button) before the backend round-trip; rolls back on error;
  re-entry guard so Cmd/Ctrl+R can't double-launch. Logs now clear in
  setStarting so early runner stdout isn't dropped.
- backend: cache confirmed-good maestro bins so the JVM capability probe
  runs once per session instead of every run.
The Performance panel's <section> lacked h-full/min-h-0 (so its background
stopped short of the pane height) and used a fixed w-[280px] shrink-0 (so a
white gap showed on the right when the pane was wider). Match RunConsole:
h-full min-h-0 w-full.
Adds an editor-style context menu to the Workspace tree:
- file → Rename, Delete
- folder → New file, New folder, Rename, Delete
- empty area/root → New file, New folder

New ContextMenu UI primitive wrapping @radix-ui/react-context-menu. Rename
is inline (the label becomes a prefilled input with the base name selected).
renameEntry/deleteEntry in workspace-ops repoint the open editor, lastOpenFile
and expanded keys when a file or an ancestor folder is renamed/deleted, and
remove folders recursively. Hover action buttons kept. Grants fs:allow-rename
in the Tauri capabilities (rename was previously denied).
Bump version across package.json, tauri.conf.json, Cargo.toml; refresh BUSL Change Date; add v0.4.0 changelog and a macOS release-staging helper.
- install_ios_device_bridge returns early on non-macOS, but the Unix-only PermissionsExt::set_mode still has to compile on Windows. Move it into a #[cfg(unix)] block so the Windows CI build links.
- Remove docs/physical-ios-setup.md and scripts/stage-release.sh.
The sim-capture-poll thread used objc2 accessors that panic on a nil/null return: every bare Retained<_> msg_send in attach() (nil context / device set / availableDevices / io / ioPorts), and IOSurface::baseAddress() (which returns NonNull and panics when a locked surface has no CPU-mappable base).

With panic = abort (release profile) any such panic on this background thread aborts the whole process — which is why selecting a just-booted simulator crashed only the production build (in debug, panic=unwind just kills the poll thread silently and the app survives).

Make every fragile call recoverable: nil objc returns become a descriptive AppError (or skip-this-device), and the base address is read via the raw, nullable IOSurfaceGetBaseAddress so the existing null guard actually fires. Adds ignored manual-repro tests against a booted sim.
…markdown components

- flowUrl: the value class [^"'\n]+ overlapped the trailing whitespace quantifier, giving super-linear backtracking (S5852, a security hotspot). Capture the rest of the line with an unambiguous pattern and strip quotes/space in code; also switches to RegExp.exec (S6594).
- UpdateDialog: hoist the react-markdown component map to module scope so the element factories aren't redefined on every render (clears the 'nested component definition' smells).
…eliability)

The menu root already swallows pointer/click/wheel events so they don't leak to the inspector underneath; add an onKeyDown stopPropagation too. Coherent with that intent and clears Sonar's 'interactive element needs a keyboard listener' bug (Reliability New Code B -> A).
@sonarqubecloud

sonarqubecloud Bot commented Jun 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
16.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@BlueShork BlueShork merged commit ae859fd into main Jun 7, 2026
8 of 9 checks passed
@BlueShork BlueShork deleted the feature/ios-support branch June 7, 2026 22:19
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant