Skip to content

Commit bebc4ef

Browse files
authored
feat(simulators): add native simulator creation flow for iOS and Android (#41)
* fix: boot new simulators and sort booted ones first * feat: boot new simulators and sort booted first * style(client): format simulator creation UI
1 parent 5d48881 commit bebc4ef

19 files changed

Lines changed: 2382 additions & 55 deletions

File tree

cli/XCWSimctl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ NS_ASSUME_NONNULL_BEGIN
55
@interface XCWSimctl : NSObject
66

77
- (nullable NSArray<NSDictionary *> *)listSimulatorsWithError:(NSError * _Nullable * _Nullable)error;
8+
- (nullable NSDictionary *)simulatorCreationOptionsWithError:(NSError * _Nullable * _Nullable)error;
9+
- (nullable NSDictionary *)createSimulatorWithName:(NSString *)name
10+
deviceTypeIdentifier:(NSString *)deviceTypeIdentifier
11+
runtimeIdentifier:(nullable NSString *)runtimeIdentifier
12+
pairedWatchName:(nullable NSString *)pairedWatchName
13+
pairedWatchDeviceTypeIdentifier:(nullable NSString *)pairedWatchDeviceTypeIdentifier
14+
pairedWatchRuntimeIdentifier:(nullable NSString *)pairedWatchRuntimeIdentifier
15+
error:(NSError * _Nullable * _Nullable)error;
816
- (BOOL)bootSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
917
- (BOOL)shutdownSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
1018
- (BOOL)toggleAppearanceForSimulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;

cli/XCWSimctl.m

Lines changed: 312 additions & 18 deletions
Large diffs are not rendered by default.

cli/native/XCWNativeBridge.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ void xcw_native_initialize_app(void);
3434
void xcw_native_run_main_loop_slice(double duration_seconds);
3535

3636
char * _Nullable xcw_native_list_simulators(char * _Nullable * _Nullable error_message);
37+
char * _Nullable xcw_native_simulator_creation_options(char * _Nullable * _Nullable error_message);
38+
char * _Nullable xcw_native_create_simulator(const char * _Nonnull name,
39+
const char * _Nonnull device_type_identifier,
40+
const char * _Nullable runtime_identifier,
41+
const char * _Nullable paired_watch_name,
42+
const char * _Nullable paired_watch_device_type_identifier,
43+
const char * _Nullable paired_watch_runtime_identifier,
44+
char * _Nullable * _Nullable error_message);
3745
bool xcw_native_boot_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
3846
bool xcw_native_shutdown_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
3947
bool xcw_native_toggle_appearance(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);

cli/native/XCWNativeBridge.m

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,44 @@ void xcw_native_run_main_loop_slice(double duration_seconds) {
367367
}
368368
}
369369

370+
char *xcw_native_simulator_creation_options(char **error_message) {
371+
@autoreleasepool {
372+
XCWSimctl *simctl = [[XCWSimctl alloc] init];
373+
NSError *error = nil;
374+
NSDictionary *options = [simctl simulatorCreationOptionsWithError:&error];
375+
if (options == nil) {
376+
XCWSetErrorMessage(error_message, error);
377+
return NULL;
378+
}
379+
return XCWJSONStringFromObject(options, error_message);
380+
}
381+
}
382+
383+
char *xcw_native_create_simulator(const char *name,
384+
const char *device_type_identifier,
385+
const char *runtime_identifier,
386+
const char *paired_watch_name,
387+
const char *paired_watch_device_type_identifier,
388+
const char *paired_watch_runtime_identifier,
389+
char **error_message) {
390+
@autoreleasepool {
391+
XCWSimctl *simctl = [[XCWSimctl alloc] init];
392+
NSError *error = nil;
393+
NSDictionary *result = [simctl createSimulatorWithName:XCWStringFromCString(name)
394+
deviceTypeIdentifier:XCWStringFromCString(device_type_identifier)
395+
runtimeIdentifier:runtime_identifier == NULL ? nil : XCWStringFromCString(runtime_identifier)
396+
pairedWatchName:paired_watch_name == NULL ? nil : XCWStringFromCString(paired_watch_name)
397+
pairedWatchDeviceTypeIdentifier:paired_watch_device_type_identifier == NULL ? nil : XCWStringFromCString(paired_watch_device_type_identifier)
398+
pairedWatchRuntimeIdentifier:paired_watch_runtime_identifier == NULL ? nil : XCWStringFromCString(paired_watch_runtime_identifier)
399+
error:&error];
400+
if (result == nil) {
401+
XCWSetErrorMessage(error_message, error);
402+
return NULL;
403+
}
404+
return XCWJSONStringFromObject(result, error_message);
405+
}
406+
}
407+
370408
bool xcw_native_boot_simulator(const char *udid, char **error_message) {
371409
@autoreleasepool {
372410
return XCWPerformSimctlAction(error_message, ^BOOL(XCWSimctl *simctl, NSError **error) {

client/src/api/simulators.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import type {
44
AccessibilityTreeResponse,
55
ChromeDevToolsTargetDiscovery,
66
ChromeProfile,
7+
CreateSimulatorRequest,
8+
CreateSimulatorResponse,
79
InspectorRequestResponse,
810
SimulatorPerformanceResponse,
911
SimulatorLogsResponse,
12+
SimulatorCreateOptionsResponse,
1013
SimulatorMetadata,
1114
SimulatorProcessListResponse,
1215
SimulatorStateResponse,
@@ -22,6 +25,24 @@ export async function listSimulators(
2225
return data.simulators ?? [];
2326
}
2427

28+
export async function fetchSimulatorCreateOptions(
29+
options: RequestInit = {},
30+
): Promise<SimulatorCreateOptionsResponse> {
31+
return apiRequest<SimulatorCreateOptionsResponse>(
32+
"/api/simulators/create-options",
33+
options,
34+
);
35+
}
36+
37+
export async function createSimulator(
38+
payload: CreateSimulatorRequest,
39+
): Promise<CreateSimulatorResponse> {
40+
return apiRequest<CreateSimulatorResponse>("/api/simulators", {
41+
body: JSON.stringify(payload),
42+
method: "POST",
43+
});
44+
}
45+
2546
export async function fetchChromeProfile(udid: string): Promise<ChromeProfile> {
2647
return apiRequest<ChromeProfile>(`/api/simulators/${udid}/chrome-profile`);
2748
}

client/src/api/types.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface SimulatorMetadata {
4040
runtimeIdentifier?: string;
4141
deviceTypeName?: string;
4242
deviceTypeIdentifier?: string;
43+
pairedWatchUDID?: string;
44+
pairedWatchName?: string;
45+
pairedPhoneUDID?: string;
46+
pairedPhoneName?: string;
47+
devicePairIdentifier?: string;
48+
devicePairState?: string;
4349
isBooted: boolean;
4450
android?: {
4551
avdName?: string;
@@ -53,6 +59,80 @@ export interface SimulatorsResponse {
5359
simulators: SimulatorMetadata[];
5460
}
5561

62+
export interface SimulatorDeviceTypeOption {
63+
identifier: string;
64+
name: string;
65+
productFamily?: string;
66+
modelIdentifier?: string;
67+
minRuntimeVersion?: number;
68+
minRuntimeVersionString?: string;
69+
maxRuntimeVersion?: number;
70+
maxRuntimeVersionString?: string;
71+
supportedRuntimeIdentifiers?: string[];
72+
}
73+
74+
export interface SimulatorRuntimeOption {
75+
identifier: string;
76+
name: string;
77+
platform?: string;
78+
version?: string;
79+
buildVersion?: string;
80+
isAvailable?: boolean;
81+
supportedDeviceTypeIdentifiers?: string[];
82+
}
83+
84+
export interface AndroidEmulatorDeviceTypeOption {
85+
identifier: string;
86+
name: string;
87+
oem?: string | null;
88+
tag?: string | null;
89+
}
90+
91+
export interface AndroidEmulatorSystemImageOption {
92+
identifier: string;
93+
name: string;
94+
description?: string;
95+
apiLevel?: number | null;
96+
tag?: string;
97+
abi?: string;
98+
}
99+
100+
export interface AndroidEmulatorCreateOptions {
101+
deviceTypes: AndroidEmulatorDeviceTypeOption[];
102+
systemImages: AndroidEmulatorSystemImageOption[];
103+
unavailableReason?: string;
104+
}
105+
106+
export interface SimulatorCreateOptionsResponse {
107+
deviceTypes: SimulatorDeviceTypeOption[];
108+
runtimes: SimulatorRuntimeOption[];
109+
android?: AndroidEmulatorCreateOptions;
110+
}
111+
112+
export interface CreatePairedWatchRequest {
113+
name: string;
114+
deviceTypeIdentifier: string;
115+
runtimeIdentifier?: string;
116+
}
117+
118+
export interface CreateSimulatorRequest {
119+
platform?: "ios" | "android" | string;
120+
name: string;
121+
deviceTypeIdentifier: string;
122+
runtimeIdentifier?: string;
123+
pairedWatch?: CreatePairedWatchRequest;
124+
}
125+
126+
export interface CreateSimulatorResponse {
127+
ok: boolean;
128+
created: {
129+
udid: string;
130+
pairedWatchUDID?: string;
131+
};
132+
simulator: SimulatorMetadata;
133+
pairedWatchSimulator?: SimulatorMetadata | null;
134+
}
135+
56136
export interface WebKitTarget {
57137
id: string;
58138
appId: string;

client/src/app/AppShell.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import type {
7474
ViewMode,
7575
} from "../features/viewport/types";
7676
import { useViewportLayout } from "../features/viewport/useViewportLayout";
77+
import { NewSimulatorModal } from "../features/simulators/NewSimulatorModal";
7778
import {
7879
buildShellRotationTransform,
7980
clampPan,
@@ -388,6 +389,7 @@ export function AppShell({
388389
initialUiState.bundleIDValue ?? "com.apple.Preferences",
389390
);
390391
const [menuOpen, setMenuOpen] = useState(false);
392+
const [newSimulatorOpen, setNewSimulatorOpen] = useState(false);
391393
const [localError, setLocalError] = useState("");
392394
const [failedStreamUDIDs, setFailedStreamUDIDs] = useState<Set<string>>(
393395
() => new Set(),
@@ -2194,6 +2196,20 @@ export function AppShell({
21942196
}
21952197
}
21962198

2199+
function handleSimulatorCreated(response: {
2200+
simulator: SimulatorMetadata;
2201+
pairedWatchSimulator?: SimulatorMetadata | null;
2202+
}) {
2203+
updateSimulator(response.simulator);
2204+
if (response.pairedWatchSimulator) {
2205+
updateSimulator(response.pairedWatchSimulator);
2206+
}
2207+
setSelectedUDID(response.simulator.udid);
2208+
setNewSimulatorOpen(false);
2209+
setLocalError("");
2210+
void refresh();
2211+
}
2212+
21972213
async function submitPairing(event: FormEvent<HTMLFormElement>) {
21982214
event.preventDefault();
21992215
const code = pairingCode.trim();
@@ -2303,6 +2319,10 @@ export function AppShell({
23032319
}
23042320
}}
23052321
onOpenBundlePrompt={promptForBundleID}
2322+
onOpenNewSimulator={() => {
2323+
setMenuOpen(false);
2324+
setNewSimulatorOpen(true);
2325+
}}
23062326
onOpenUrlPrompt={promptForURL}
23072327
onRotateRight={() => {
23082328
if (!selectedSimulator) {
@@ -2387,6 +2407,12 @@ export function AppShell({
23872407
touchOverlayVisible={touchOverlayVisible}
23882408
devToolsVisible={devToolsVisible}
23892409
/>
2410+
<NewSimulatorModal
2411+
onClose={() => setNewSimulatorOpen(false)}
2412+
onCreated={handleSimulatorCreated}
2413+
open={newSimulatorOpen && !hideSimulatorSelection}
2414+
selectedSimulator={selectedSimulator}
2415+
/>
23902416
<SimulatorViewport
23912417
accessibilityHoveredId={accessibilityHoveredId}
23922418
accessibilityPanel={

0 commit comments

Comments
 (0)