Skip to content

Support physical iOS and tvOS devices - #15

Open
DouweBos wants to merge 4 commits into
mainfrom
douwe/physical-apple-devices
Open

Support physical iOS and tvOS devices#15
DouweBos wants to merge 4 commits into
mainfrom
douwe/physical-apple-devices

Conversation

@DouweBos

Copy link
Copy Markdown
Owner

Adds support for driving real iPhones, iPads, and Apple TVs, alongside the existing simulator support.

Physical devices are discovered through devicectl, show up in list-devices with status connected, and are addressed by their CoreDevice identifier. Everything that runs through the XCTest driver — inspect, capture-ui, tap-on, press-key, swipe, launch-app, flows — behaves as it does on a simulator.

How it works

Three things differ from a simulator, and each forced a design decision:

Signing. Real hardware only runs code signed for the user's team, and we can't ship a signed driver. The XCTest driver is therefore built and signed locally on first use (xcodebuild build-for-testing -allowProvisioningUpdates), cached per team under ~/.conductor/<platform>-driver-device/<team>/. Driver sources ship in the drivers tarball to make this possible. CONDUCTOR_TEAM_ID selects the team; when a Mac has several, conductor refuses to guess rather than sign with the wrong one.

Reachability. The driver bound 127.0.0.1, which on a device is the device's loopback — unreachable from the host. A BIND_ALL=1 env var (injected into the xctestrun) switches it to 0.0.0.0, and the host connects over the LAN. Note that devicectl's *.coredevice.local hostnames only resolve inside Apple's tunnel, so the address is derived from the device's Bonjour name instead.

App lifecycle. simctl has no device equivalent, so install/uninstall/launch/terminate route through devicectl.

Not supported on device

These fail with an explicit message rather than misbehaving: set-location, open-link, clipboard, clear-keychain, add-media, screen recording, and the live video stream (capture attaches to the Simulator framebuffer via SimulatorKit, which has no hardware counterpart). OS log collection is also simulator-only — devicectl has no console subcommand — but Metro logs still stream, which is the useful source for React Native. clear-state uninstalls without reinstalling.

Driver fixes

Three bugs that only real hardware surfaces:

  • XCUIApplication("com.apple.HeadBoard") hangs indefinitely on a physical Apple TV. It backed screen size, status bars, and window origin, which broke deviceInfo and viewHierarchy — the core inspect path. None of it applies on a platform with no windowing and no status bar, so all three are now #if os(tvOS)-guarded. deviceInfo went from a hang to 0.25s.
  • A phone lying flat reports .faceUp, which simulators never do. orientationAwarePoint had no case for it and hit fatalError, so every tap killed the driver. Flat orientations now map to portrait, and an unmapped orientation logs and passes the point through instead of taking the driver down.
  • Bonjour names drop apostrophes rather than dashing them, so a device named "Douwe's iPhone" was unreachable.

Testing

Full suite passes (350 tests), including 8 new unit tests covering devicectl parsing, reachability classification, and hostname derivation.

Validated live on a physical Apple TV 4K and a physical iPhone 14 Pro: driver build/sign, inspect, capture-ui, screenshot, list-apps, launch-app, and tap / press-key / swipe navigation, plus the streaming-input WebSocket. That validation was done against CLI 0.27.2; the branch has since been rebased onto 0.30.0 (which added Roku), so a repeat run on real hardware is worth doing before relying on it.

Studio no longer records case results: Qase is the system of record, so the
local execution log, the manual run wizard, the pass/fail buttons, the
record_case_result MCP tool and the matrix's verdict cells are gone.

Around the cases it does read:

- Resolve select-type custom fields to their option titles instead of showing
  Qase's option ids, and decode the HTML entities Qase stores prose with.
- A step can name several page objects, each with its own env — steps regularly
  bundle actions — and a scaffold emits a runFlow per entry, in order.
- Linking and scaffolding write the case's priority beside testCaseId.
- Scaffolded comments stay commented on every line, so multi-line case prose no
  longer breaks the YAML.
- Unlinking a flow whose file is already gone is a quiet no-op.
- The open case lives in the URL, so leaving the screen and coming back reopens
  it; the nav rail returns each view to where it was.

Editor and design system: Tab indents (and accepts a completion) instead of
tabbing out, `properties` is offered as a flow header key, and a status pill
grows with its text rather than spilling out of its background.
Qase nests suites and Studio only read their titles, so 250 cases arrived as one
flat list under a row of dropdowns. Fetch the hierarchy, carry each case's suite
path, and show it as the folder tree beside the matrix: same nesting, counts
including everything below a folder, and picking one scopes the matrix to that
subtree.

The filter row starts as a search and nothing else — every other filter is added
from an "Add filter" picker and removed with an ✕ — so a project with six custom
fields no longer greets you with six dropdowns. The "group by" banding goes with
it: the tree is the table of contents it stood in for.
Discover real iPhones, iPads, and Apple TVs through devicectl and drive them
with the same XCTest driver used for simulators.

Real hardware only runs code signed for the user's team, so the driver is built
and signed locally on first use and cached per team. It binds all interfaces on
device because the host reaches it over the LAN rather than a shared loopback,
and app lifecycle routes through devicectl instead of simctl.

Simulator-only features (set-location, open-link, clipboard, clear-keychain,
add-media, recording, video stream, OS logs) fail with an explicit message.

Also fixes tvOS inspection hanging on physical Apple TVs: querying HeadBoard for
screen size, the status bar, and window origin never returns there, and none of
it is needed on a platform without windowing or a status bar.
Three defects that only a physical device surfaces:

Bonjour hostnames dashed apostrophes instead of dropping them, so "Douwe's
iPhone" resolved to a name that doesn't exist. Candidates now also reuse
devicectl's own sanitized hostnames, re-pointed at .local.

Every tap killed the driver. A phone resting on a desk reports .faceUp, which
simulators never do and orientationAwarePoint had no case for, so it hit
fatalError. Flat orientations map to portrait, and an unmapped orientation now
degrades to an untranslated point rather than taking the driver down.

The first driver build for a team creates provisioning profiles as a side
effect and Xcode regularly references one before it lands on disk, so retry
once. Build failures now report xcodebuild's errors, not just an exit code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant