watchOS XCUITest spike¶
The XCUITest runner approach supports hierarchy, tap, and screenshot operations on the watchOS simulator, as demonstrated with Xcode 26.6, watchOS 26.5, and an unpaired Apple Watch Series 11 (46 mm) simulator. The probe app is a SwiftUI app with a counter button, a Crown value, and rows in a scroll view.
Capability boundary¶
| Check | Result | Evidence and limits |
|---|---|---|
| Launch | Yes | xcodebuild test-without-building with platform=watchOS Simulator runs the service-style test. |
| Socket | Yes | A runner NWListener on port 4199 exchanges ping/pong with the host over shared loopback. TEST_RUNNER_AM_PORT=4199 arrives as AM_PORT; SIMCTL_CHILD_* was not tested. |
| Hierarchy | Yes | snapshot() returns frames, labels, identifiers, and element types. debugDescription also works. |
| Tap and press | Yes | Element tap, coordinate tap, and coordinate press(forDuration:) each reach the counter button. |
| Drag and swipes | Calls succeed; effect unverified | press(forDuration:thenDragTo:) and all four swipe directions return without error; scroll displacement was not read back. |
| Home | Call succeeds | XCUIDevice.shared.press(.home) returns without error. |
| Digital Crown rotation | Yes, with Crown focus | Typed XCUIDevice IMP calls change the focused view’s bound Crown value. This does not demonstrate scrolling a real ScrollView. |
| Screenshots | Yes | XCUIScreen.main.screenshot() and simctl io screenshot both work. |
| System apps | Settings yes; Carousel no | com.apple.NanoSettings activates and snapshots. Launching com.apple.Carousel is rejected by IOSSHLMainWorkspace. |
| CtrlProxy typecheck | Yes, compilation only | Sources/CtrlProxy typechecks with the sdk-wire module and ObjC catcher module map for arm64-apple-watchos10.0-simulator. os(iOS)-gated and UIKit-importing paths are compiled out, so they do not demonstrate watchOS behaviour. CtrlProxy itself was neither built with xcodebuild nor run. |
Installation and runner lifetime¶
For this standalone watch app, Info.plist must contain both WKApplication = true
and WKWatchOnly = true. WKApplication alone fails installation with
MIInstallerErrorDomain 97 because a non-watch-only app requires
WKCompanionAppBundleIdentifier.
Under load, test-without-building can remain running after the test passes.
Use a hard timeout wrapper.
Digital Crown contract¶
Rotation reaches the view that holds Crown focus. The demonstrated target uses
.focusable(), an @FocusState set in onAppear, and
.digitalCrownRotation($crown, from: -1000, through: 1000, by: 0.1,
sensitivity: .high, isContinuous: true, isHapticFeedbackEnabled: false).
The test taps the crown text before rotation. An unfocused target and
perform(_:with:) calls with boxed NSNumber arguments did not change the value;
typed @convention(c) IMP calls confirm rotation:
| Private selector | @convention(c) signature |
|---|---|
rotateDigitalCrownByDelta:withVelocity: |
(AnyObject, Selector, Double, Double) -> Void |
rotateDigitalCrownByDelta: |
(AnyObject, Selector, Double) -> Void |
rotateDigitalCrown:velocity: |
(AnyObject, Selector, Double, Double) -> Void (encoding v32@0:8d16d24) |
The app binding’s sensitivity, step, continuous range, and velocity affect the
result, and wrapping or clamping makes the delta-to-value mapping nonlinear. A Crown action must
report the requested delta, not a resulting position. Guard each private
selector with responds(to:) and return a typed unsupported error if absent.
Each call takes seconds, so many small steps exceed a runner timeout. Use one
call per Crown action, with a larger delta as needed and its own timeout. An
enclosing ScrollView without a Crown binding does not move.
The capture at test/fixtures/watchos-xcuitest/hierarchy.json is the original
23-node probe hierarchy, with counter and Crown values at zero. Element types
are XCUIElement.ElementType raw values.
Decision¶
GO for CtrlProxy-dependent children on hierarchy, tap, and screenshot.
Crown rotation is demonstrated, with no further device questions for this
spike; scrolling a real ScrollView by Crown remains untested and is an
implementation follow-up. The os(iOS)-gated input paths need watchOS
implementations, and device acceptance after merge remains required. Physical
watches and changes to the iOS CtrlProxy test target are out of scope.