Skip to content

Commit 37dbd1a

Browse files
committed
Add native simulator automation CLI
1 parent 5061f39 commit 37dbd1a

54 files changed

Lines changed: 4965 additions & 1910 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
2222
- `server/src/simulators/registry.rs`
2323
Tracks Rust-side simulator session state and lazy native attachment by UDID.
2424
- `cli/XCWSimctl.*`
25-
Wraps `xcrun simctl` for discovery, lifecycle management, app launching, URL opening, and screenshot capture.
25+
Wraps `xcrun simctl` for discovery, lifecycle management, app launching, URL opening, appearance toggles, and simulator log capture.
2626
- `cli/DFPrivateSimulatorDisplayBridge.*`
2727
Owns headless private display frames plus HID-based touch and keyboard injection.
28+
- `cli/XCWAccessibilityBridge.*`
29+
Owns private CoreSimulator accessibility snapshots through `AccessibilityPlatformTranslation`.
2830
- `cli/XCWPrivateSimulatorSession.*`
2931
Owns one private display bridge per booted simulator plus selectable HEVC/H.264 encode.
3032
- `cli/native/XCWNativeBridge.*`
@@ -33,8 +35,6 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
3335
Wraps one Objective-C private simulator session handle for the Rust registry.
3436
- `cli/XCWPrivateSimulatorBooter.*`
3537
Uses private `CoreSimulator` APIs for direct simulator boot when available, with `simctl` as the fallback path.
36-
- `cli/XCWPrivateSimulatorChromeBridge.*`
37-
Experimental private `SimulatorKit` chrome bridge for simulator chrome exploration.
3838
- `cli/XCWChromeRenderer.*`
3939
Renders Apple’s CoreSimulator device-type PDF chrome assets into PNGs for the browser.
4040
- `client/src/app/App.tsx`
@@ -60,10 +60,10 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
6060
Private simulator behavior is implemented locally in:
6161

6262
- Boot path: `cli/XCWPrivateSimulatorBooter.*`
63-
- Chrome asset bridge: `cli/XCWPrivateSimulatorChromeBridge.*`
6463
- Full live display bridge: `cli/DFPrivateSimulatorDisplayBridge.*`
64+
- Accessibility bridge: `cli/XCWAccessibilityBridge.*`
6565

66-
The current repo uses the private boot path and private display bridge directly. The browser streams frames from that bridge and injects touch and keyboard events through the same native session layer.
66+
The current repo uses the private boot path, private display bridge, and private accessibility translation bridge directly. The browser streams frames from that bridge, injects touch and keyboard events through the same native session layer, inspects accessibility through `AccessibilityPlatformTranslation`, and renders device chrome from `cli/XCWChromeRenderer.*`.
6767

6868
## Build and Run
6969

@@ -105,8 +105,26 @@ Useful direct commands:
105105
./build/xcode-canvas-web list
106106
./build/xcode-canvas-web boot <udid>
107107
./build/xcode-canvas-web shutdown <udid>
108+
./build/xcode-canvas-web erase <udid>
109+
./build/xcode-canvas-web install <udid> /path/to/App.app
110+
./build/xcode-canvas-web uninstall <udid> com.example.App
108111
./build/xcode-canvas-web open-url <udid> https://example.com
109112
./build/xcode-canvas-web launch <udid> com.apple.Preferences
113+
./build/xcode-canvas-web pasteboard set <udid> "hello"
114+
./build/xcode-canvas-web pasteboard get <udid>
115+
./build/xcode-canvas-web screenshot <udid> --output screen.png
116+
./build/xcode-canvas-web describe-ui <udid>
117+
./build/xcode-canvas-web tap <udid> 120 240
118+
./build/xcode-canvas-web tap <udid> --label "Continue" --wait-timeout-ms 5000
119+
./build/xcode-canvas-web swipe <udid> 200 700 200 200
120+
./build/xcode-canvas-web gesture <udid> scroll-down
121+
./build/xcode-canvas-web pinch <udid> --start-distance 160 --end-distance 80
122+
./build/xcode-canvas-web rotate-gesture <udid> --radius 100 --degrees 90
123+
./build/xcode-canvas-web key-sequence <udid> --keycodes h,e,l,l,o
124+
./build/xcode-canvas-web key-combo <udid> --modifiers cmd --key a
125+
./build/xcode-canvas-web type <udid> "hello"
126+
./build/xcode-canvas-web button <udid> lock --duration-ms 1000
127+
./build/xcode-canvas-web home <udid>
110128
```
111129

112130
## Expectations For Future Changes
@@ -120,5 +138,5 @@ Useful direct commands:
120138
## Near-Term Roadmap
121139

122140
- Compose the private frame stream and CoreSimulator chrome into a single server-side render path.
123-
- Add richer input surfaces such as gesture synthesis, text entry helpers, and chrome-button actions on top of the HID bridge.
124-
- Add simulator creation, erase, pasteboard, install, and log streaming commands.
141+
- Keep private Indigo multi-touch packet assumptions documented when Xcode runtimes change.
142+
- Add simulator creation and log streaming commands.

README.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,22 @@
2222

2323
## Install
2424

25+
Requirements:
26+
27+
- macOS
28+
- Xcode or Command Line Tools
29+
- Rust toolchain (`cargo`)
30+
- Node.js 18+
31+
2532
Install the published CLI globally:
2633

2734
```sh
2835
npm install -g xcode-canvas-web
2936
```
3037

38+
The npm package builds the native Rust/Objective-C CLI during `postinstall`; it
39+
is not a prebuilt cross-platform binary.
40+
3141
Install the current local checkout globally from source:
3242

3343
```sh
@@ -43,6 +53,8 @@ xcode-canvas-web serve --port 4310
4353
```
4454

4555
Then open [http://127.0.0.1:4310](http://127.0.0.1:4310).
56+
To focus a specific simulator, open
57+
[http://127.0.0.1:4310?device=UDID](http://127.0.0.1:4310?device=UDID).
4658

4759
The Rust server exposes HTTP on the requested port and WebTransport on `port + 1`.
4860
The browser bootstrap comes from `GET /api/health`, which returns the WebTransport URL template,
@@ -56,22 +68,80 @@ Enable the per-user background service with `launchd`:
5668
xcode-canvas-web service on --port 4310
5769
```
5870

71+
Restart it:
72+
73+
```sh
74+
xcode-canvas-web service restart
75+
```
76+
5977
Disable it:
6078

6179
```sh
6280
xcode-canvas-web service off
6381
```
6482

83+
Restart the CoreSimulator service layer when `simctl` reports a stale service
84+
version or the live display gets stuck before the first frame:
85+
86+
```sh
87+
xcode-canvas-web core-simulator restart
88+
```
89+
90+
You can also start or stop the CoreSimulator service layer explicitly:
91+
92+
```sh
93+
xcode-canvas-web core-simulator start
94+
xcode-canvas-web core-simulator shutdown
95+
```
96+
6597
## CLI
6698

6799
```sh
68100
xcode-canvas-web list
69101
xcode-canvas-web boot <udid>
70102
xcode-canvas-web shutdown <udid>
103+
xcode-canvas-web erase <udid>
104+
xcode-canvas-web install <udid> /path/to/App.app
105+
xcode-canvas-web uninstall <udid> com.example.App
71106
xcode-canvas-web open-url <udid> https://example.com
72107
xcode-canvas-web launch <udid> com.apple.Preferences
108+
xcode-canvas-web toggle-appearance <udid>
109+
xcode-canvas-web pasteboard set <udid> "hello"
110+
xcode-canvas-web pasteboard get <udid>
111+
xcode-canvas-web screenshot <udid> --output screen.png
112+
xcode-canvas-web describe-ui <udid>
113+
xcode-canvas-web describe-ui <udid> --point 120,240
114+
xcode-canvas-web tap <udid> 120 240
115+
xcode-canvas-web tap <udid> --label "Continue" --wait-timeout-ms 5000
116+
xcode-canvas-web swipe <udid> 200 700 200 200
117+
xcode-canvas-web gesture <udid> scroll-down
118+
xcode-canvas-web pinch <udid> --start-distance 160 --end-distance 80
119+
xcode-canvas-web rotate-gesture <udid> --radius 100 --degrees 90
120+
xcode-canvas-web touch <udid> 0.5 0.5 --phase began --normalized
121+
xcode-canvas-web touch <udid> 120 240 --down --up --delay-ms 800
122+
xcode-canvas-web key <udid> enter
123+
xcode-canvas-web key-sequence <udid> --keycodes h,e,l,l,o
124+
xcode-canvas-web key-combo <udid> --modifiers cmd --key a
125+
xcode-canvas-web type <udid> "hello"
126+
xcode-canvas-web type <udid> --file message.txt
127+
xcode-canvas-web button <udid> lock --duration-ms 1000
128+
xcode-canvas-web batch <udid> --step "tap --label Continue" --step "type 'hello'"
129+
xcode-canvas-web dismiss-keyboard <udid>
130+
xcode-canvas-web home <udid>
131+
xcode-canvas-web app-switcher <udid>
132+
xcode-canvas-web rotate-left <udid>
133+
xcode-canvas-web rotate-right <udid>
134+
xcode-canvas-web chrome-profile <udid>
135+
xcode-canvas-web logs <udid> --seconds 30 --limit 200
73136
```
74137

138+
`describe-ui` uses the built-in private CoreSimulator accessibility bridge and
139+
does not shell out to AXe. Coordinate commands accept screen coordinates from
140+
the accessibility tree by default; pass `--normalized` to send `0.0..1.0`
141+
coordinates directly. The CLI intentionally does not implement screenshot-based
142+
video streaming, MJPEG output, or screen recording; the live visual path remains
143+
the web UI's WebTransport stream.
144+
75145
## NativeScript Inspector
76146

77147
NativeScript apps can connect directly to the running server from JS and expose
@@ -88,8 +158,8 @@ if (__DEV__) {
88158

89159
The runtime connects to `GET /api/inspector/connect` as a WebSocket. The Rust
90160
server prefers connected NativeScript inspectors for hierarchy requests and
91-
falls back to the Swift TCP inspector or AXe when no matching app inspector is
92-
available.
161+
falls back to the Swift TCP inspector or the built-in native accessibility
162+
bridge when no matching app inspector is available.
93163

94164
## VS Code
95165

cli/DFPrivateSimulatorDisplayBridge.h

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
NS_ASSUME_NONNULL_BEGIN
55

66
@class DFPrivateSimulatorDisplayBridge;
7-
@class DFPrivateSimulatorChromeButton;
87

98
typedef NS_ENUM(NSInteger, DFPrivateSimulatorTouchPhase) {
109
DFPrivateSimulatorTouchPhaseBegan = 0,
@@ -30,33 +29,40 @@ NS_SWIFT_NAME(PrivateSimulatorDisplayBridge)
3029

3130
- (instancetype)init NS_UNAVAILABLE;
3231
- (instancetype)new NS_UNAVAILABLE;
33-
- (nullable instancetype)initWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(udid:));
32+
- (nullable instancetype)initWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(init(udid:));
33+
- (nullable instancetype)initWithUDID:(NSString *)udid
34+
attachDisplay:(BOOL)attachDisplay
35+
error:(NSError * _Nullable * _Nullable)error NS_DESIGNATED_INITIALIZER;
3436

3537
@property (nonatomic, weak, nullable) id<DFPrivateSimulatorDisplayBridgeDelegate> delegate;
36-
@property (nonatomic, readonly) NSView *displayView;
3738
@property (nonatomic, readonly, getter=isDisplayReady) BOOL displayReady;
3839
@property (nonatomic, readonly) NSString *displayStatus;
3940

40-
- (void)activateDisplayIfNeeded;
4141
- (nullable CVPixelBufferRef)copyPixelBuffer CF_RETURNS_RETAINED;
4242

4343
- (BOOL)sendTouchAtNormalizedX:(double)normalizedX
4444
normalizedY:(double)normalizedY
4545
phase:(DFPrivateSimulatorTouchPhase)phase
4646
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendTouch(normalizedX:normalizedY:phase:));
4747

48+
- (BOOL)sendMultiTouchAtNormalizedX1:(double)normalizedX1
49+
normalizedY1:(double)normalizedY1
50+
normalizedX2:(double)normalizedX2
51+
normalizedY2:(double)normalizedY2
52+
phase:(DFPrivateSimulatorTouchPhase)phase
53+
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendMultiTouch(normalizedX1:normalizedY1:normalizedX2:normalizedY2:phase:));
54+
4855
- (BOOL)sendKeyCode:(uint16_t)keyCode
4956
modifiers:(NSUInteger)modifiers
5057
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendKey(keyCode:modifiers:));
51-
52-
- (BOOL)sendKeyEvent:(NSEvent *)event
53-
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendKey(event:));
54-
55-
- (NSArray<DFPrivateSimulatorChromeButton *> *)availableChromeButtons NS_SWIFT_NAME(availableChromeButtons());
56-
- (BOOL)pressChromeButtonWithIdentifier:(NSString *)identifier
57-
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(pressChromeButton(identifier:));
58+
- (BOOL)sendKeyCode:(uint16_t)keyCode
59+
down:(BOOL)down
60+
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(sendKey(keyCode:down:));
5861

5962
- (BOOL)pressHomeButton:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(pressHomeButton());
63+
- (BOOL)pressHardwareButtonNamed:(NSString *)buttonName
64+
durationMs:(NSUInteger)durationMs
65+
error:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(pressHardwareButton(named:durationMs:));
6066

6167
- (BOOL)rotateRight:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(rotateRight());
6268
- (BOOL)rotateLeft:(NSError * _Nullable * _Nullable)error NS_SWIFT_NAME(rotateLeft());
@@ -65,18 +71,4 @@ NS_SWIFT_NAME(PrivateSimulatorDisplayBridge)
6571

6672
@end
6773

68-
NS_SWIFT_NAME(PrivateSimulatorChromeButton)
69-
@interface DFPrivateSimulatorChromeButton : NSObject
70-
71-
- (instancetype)init NS_UNAVAILABLE;
72-
- (instancetype)new NS_UNAVAILABLE;
73-
74-
@property (nonatomic, copy, readonly) NSString *identifier;
75-
@property (nonatomic, copy, readonly) NSString *title;
76-
@property (nonatomic, copy, readonly) NSString *toolTip;
77-
@property (nonatomic, copy, readonly) NSString *accessibilityLabel;
78-
@property (nonatomic, copy, readonly) NSString *summary;
79-
80-
@end
81-
8274
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)