Skip to content

Commit ef20dea

Browse files
authored
feat: add AndroidTV and tvOS target support (#125)
* feat: add AndroidTV and tvOS target support * feat: enable tvOS runner interactions * refactor: unify apple target handling and tvOS capability gates * refactor: remove hardcoded tvOS app-switcher sleep * fix: align tvOS capabilities and docs for simulator-only helpers * feat: improve AppleTV and AndroidTV physical device detection * refactor: simplify tv target detection helpers
1 parent 996d1fc commit ef20dea

26 files changed

Lines changed: 752 additions & 119 deletions

File tree

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ CLI to control iOS and Android devices for AI agents influenced by Vercel’s [a
1313
The project is in early development and considered experimental. Pull requests are welcome!
1414

1515
## Features
16-
- Platforms: iOS (simulator + physical device core automation) and Android (emulator + device).
16+
- Platforms: iOS/tvOS (simulator + physical device core automation) and Android/AndroidTV (emulator + device).
1717
- Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`, `reinstall`, `push`.
1818
- Inspection commands: `snapshot` (accessibility tree), `diff snapshot` (structural baseline diff), `appstate`, `apps`, `devices`.
1919
- Clipboard commands: `clipboard read`, `clipboard write <text>`.
@@ -200,7 +200,8 @@ Efficient snapshot usage:
200200

201201
Flags:
202202
- `--version, -V` print version and exit
203-
- `--platform ios|android`
203+
- `--platform ios|android|apple` (`apple` aliases the iOS/tvOS backend)
204+
- `--target mobile|tv` select device class within platform (requires `--platform`; for example AndroidTV/tvOS)
204205
- `--device <name>`
205206
- `--udid <udid>` (iOS)
206207
- `--serial <serial>` (Android)
@@ -220,8 +221,22 @@ Flags:
220221
- `--on-error stop` batch: stop when a step fails
221222
- `--max-steps <n>` batch: max allowed steps per request
222223

224+
TV targets:
225+
- Use `--target tv` together with `--platform ios|android|apple`.
226+
- TV target selection supports both simulator/emulator and connected physical devices (AppleTV + AndroidTV).
227+
- AndroidTV app launch/app listing use TV launcher discovery (`LEANBACK_LAUNCHER`) and fallback component resolution when needed.
228+
- tvOS uses the same runner-driven interaction/snapshot flow as iOS (`snapshot`, `wait`, `press`, `fill`, `get`, `scroll`, `back`, `home`, `app-switcher`, `record`, and related selector flows).
229+
- tvOS back/home/app-switcher use Siri Remote semantics in the runner (`menu`, `home`, double-home).
230+
- tvOS follows iOS simulator-only command semantics for helpers like `pinch`, `settings`, and `push`.
231+
232+
Examples:
233+
- `agent-device open YouTube --platform android --target tv`
234+
- `agent-device apps --platform android --target tv`
235+
- `agent-device open Settings --platform ios --target tv`
236+
- `agent-device screenshot ./apple-tv.png --platform ios --target tv`
237+
223238
Pinch:
224-
- `pinch` is supported on iOS simulators.
239+
- `pinch` is supported on iOS simulators (including tvOS simulator targets).
225240
- On Android, `pinch` currently returns `UNSUPPORTED_OPERATION` in the adb backend.
226241

227242
Swipe timing:
@@ -250,7 +265,7 @@ Sessions:
250265
- On iOS, `appstate` is session-scoped and requires an active session on the target device.
251266

252267
Navigation helpers:
253-
- `boot --platform ios|android` ensures the target is ready without launching an app.
268+
- `boot --platform ios|android|apple` ensures the target is ready without launching an app.
254269
- Use `boot` mainly when starting a new session and `open` fails because no booted simulator/emulator is available.
255270
- `open [app|url] [url]` already boots/activates the selected target when needed.
256271
- `reinstall <app> <path>` uninstalls and installs the app binary in one command (Android + iOS simulator/device).
@@ -383,7 +398,7 @@ Boot diagnostics:
383398
- Boot failures include normalized reason codes in `error.details.reason` (JSON mode) and verbose logs.
384399
- Reason codes: `IOS_BOOT_TIMEOUT`, `IOS_RUNNER_CONNECT_TIMEOUT`, `ANDROID_BOOT_TIMEOUT`, `ADB_TRANSPORT_UNAVAILABLE`, `CI_RESOURCE_STARVATION_SUSPECTED`, `BOOT_COMMAND_FAILED`, `UNKNOWN`.
385400
- Android boot waits fail fast for permission/tooling issues and do not always collapse into timeout errors.
386-
- Use `agent-device boot --platform ios|android` when starting a new session only if `open` cannot find/connect to an available target.
401+
- Use `agent-device boot --platform ios|android|apple` when starting a new session only if `open` cannot find/connect to an available target.
387402
- `--debug` captures retry telemetry in diagnostics logs.
388403
- Set `AGENT_DEVICE_RETRY_LOGS=1` to also print retry telemetry directly to stderr (ad-hoc troubleshooting).
389404

@@ -400,6 +415,7 @@ Diagnostics files:
400415
## iOS notes
401416
- Core runner commands: `snapshot`, `wait`, `click`, `fill`, `get`, `is`, `find`, `press`, `longpress`, `focus`, `type`, `scroll`, `scrollintoview`, `back`, `home`, `app-switcher`.
402417
- Simulator-only commands: `alert`, `pinch`, `settings`.
418+
- tvOS targets are selectable (`--platform ios --target tv` or `--platform apple --target tv`) and support runner-driven interaction/snapshot commands.
403419
- `record` supports iOS simulators and physical iOS devices.
404420
- iOS simulator recording uses native `simctl io ... recordVideo`.
405421
- Physical iOS device recording is runner-based and built from repeated `XCUIScreen.main.screenshot()` frames (no native video stream/audio capture).
@@ -438,7 +454,7 @@ Environment selectors:
438454
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY=<identity>` optional signing identity override.
439455
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE=<profile>` optional provisioning profile specifier for iOS device runner signing.
440456
- `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH=<path>` optional override for iOS runner derived data root. By default, simulator uses `~/.agent-device/ios-runner/derived` and physical device uses `~/.agent-device/ios-runner/derived/device`. If you set this override, use separate paths per kind to avoid simulator/device artifact collisions.
441-
- `AGENT_DEVICE_IOS_CLEAN_DERIVED=1` rebuild iOS runner artifacts from scratch for runtime daemon-triggered builds (`pnpm ad ...`) on the selected path. `pnpm build:xcuitest`/`pnpm build:all` already clear `~/.agent-device/ios-runner/derived/device` and do not require this variable. When `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` is set, cleanup is blocked by default; set `AGENT_DEVICE_IOS_ALLOW_OVERRIDE_DERIVED_CLEAN=1` only for trusted custom paths.
457+
- `AGENT_DEVICE_IOS_CLEAN_DERIVED=1` rebuild iOS runner artifacts from scratch for runtime daemon-triggered builds (`pnpm ad ...`) on the selected path. `pnpm build:xcuitest` (alias of `pnpm build:xcuitest:ios`), `pnpm build:xcuitest:tvos`, and `pnpm build:all` already clear their default derived paths and do not require this variable. When `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` is set, cleanup is blocked by default; set `AGENT_DEVICE_IOS_ALLOW_OVERRIDE_DERIVED_CLEAN=1` only for trusted custom paths.
442458

443459
Test screenshots are written to:
444460
- `test/screenshots/android-settings.png`

ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@
258258
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
259259
MTL_FAST_MATH = YES;
260260
ONLY_ACTIVE_ARCH = YES;
261-
SDKROOT = iphoneos;
261+
SDKROOT = auto;
262262
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
263263
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
264264
};
@@ -315,7 +315,7 @@
315315
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
316316
MTL_ENABLE_DEBUG_INFO = NO;
317317
MTL_FAST_MATH = YES;
318-
SDKROOT = iphoneos;
318+
SDKROOT = auto;
319319
SWIFT_COMPILATION_MODE = wholemodule;
320320
VALIDATE_PRODUCT = YES;
321321
};
@@ -350,7 +350,9 @@
350350
SWIFT_EMIT_LOC_STRINGS = YES;
351351
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
352352
SWIFT_VERSION = 5.0;
353-
TARGETED_DEVICE_FAMILY = "1,2";
353+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator";
354+
TARGETED_DEVICE_FAMILY = "1,2,3";
355+
TVOS_DEPLOYMENT_TARGET = 15.6;
354356
};
355357
name = Debug;
356358
};
@@ -383,7 +385,9 @@
383385
SWIFT_EMIT_LOC_STRINGS = YES;
384386
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
385387
SWIFT_VERSION = 5.0;
386-
TARGETED_DEVICE_FAMILY = "1,2";
388+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator";
389+
TARGETED_DEVICE_FAMILY = "1,2,3";
390+
TVOS_DEPLOYMENT_TARGET = 15.6;
387391
};
388392
name = Release;
389393
};
@@ -404,7 +408,9 @@
404408
SWIFT_OBJC_BRIDGING_HEADER = "AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h";
405409
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
406410
SWIFT_VERSION = 5.0;
407-
TARGETED_DEVICE_FAMILY = "1,2";
411+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator";
412+
TARGETED_DEVICE_FAMILY = "1,2,3";
413+
TVOS_DEPLOYMENT_TARGET = 15.6;
408414
TEST_TARGET_NAME = AgentDeviceRunner;
409415
};
410416
name = Debug;
@@ -426,7 +432,9 @@
426432
SWIFT_OBJC_BRIDGING_HEADER = "AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h";
427433
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
428434
SWIFT_VERSION = 5.0;
429-
TARGETED_DEVICE_FAMILY = "1,2";
435+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator";
436+
TARGETED_DEVICE_FAMILY = "1,2,3";
437+
TVOS_DEPLOYMENT_TARGET = 15.6;
430438
TEST_TARGET_NAME = AgentDeviceRunner;
431439
};
432440
name = Release;

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ final class RunnerTests: XCTestCase {
4040
private let postSnapshotInteractionDelay: TimeInterval = 0.2
4141
private let firstInteractionAfterActivateDelay: TimeInterval = 0.25
4242
private let scrollInteractionIdleTimeoutDefault: TimeInterval = 1.0
43+
private let tvRemoteDoublePressDelayDefault: TimeInterval = 0.0
4344
private let minRecordingFps = 1
4445
private let maxRecordingFps = 120
4546
private var needsPostSnapshotInteractionDelay = false
@@ -790,7 +791,7 @@ final class RunnerTests: XCTestCase {
790791
performBackGesture(app: activeApp)
791792
return Response(ok: true, data: DataPayload(message: "back"))
792793
case .home:
793-
XCUIDevice.shared.press(.home)
794+
pressHomeButton()
794795
return Response(ok: true, data: DataPayload(message: "home"))
795796
case .appSwitcher:
796797
performAppSwitcherGesture(app: activeApp)
@@ -990,23 +991,78 @@ final class RunnerTests: XCTestCase {
990991
back.tap()
991992
return true
992993
}
993-
return false
994+
return pressTvRemoteMenuIfAvailable()
994995
}
995996

996997
private func performBackGesture(app: XCUIApplication) {
998+
if pressTvRemoteMenuIfAvailable() {
999+
return
1000+
}
9971001
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
9981002
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5))
9991003
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
10001004
start.press(forDuration: 0.05, thenDragTo: end)
10011005
}
10021006

10031007
private func performAppSwitcherGesture(app: XCUIApplication) {
1008+
if performTvRemoteAppSwitcherIfAvailable() {
1009+
return
1010+
}
10041011
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
10051012
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
10061013
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
10071014
start.press(forDuration: 0.6, thenDragTo: end)
10081015
}
10091016

1017+
private func pressHomeButton() {
1018+
if pressTvRemoteHomeIfAvailable() {
1019+
return
1020+
}
1021+
XCUIDevice.shared.press(.home)
1022+
}
1023+
1024+
private func pressTvRemoteMenuIfAvailable() -> Bool {
1025+
#if os(tvOS)
1026+
XCUIRemote.shared.press(.menu)
1027+
return true
1028+
#else
1029+
return false
1030+
#endif
1031+
}
1032+
1033+
private func pressTvRemoteHomeIfAvailable() -> Bool {
1034+
#if os(tvOS)
1035+
XCUIRemote.shared.press(.home)
1036+
return true
1037+
#else
1038+
return false
1039+
#endif
1040+
}
1041+
1042+
private func performTvRemoteAppSwitcherIfAvailable() -> Bool {
1043+
#if os(tvOS)
1044+
XCUIRemote.shared.press(.home)
1045+
sleepFor(resolveTvRemoteDoublePressDelay())
1046+
XCUIRemote.shared.press(.home)
1047+
return true
1048+
#else
1049+
return false
1050+
#endif
1051+
}
1052+
1053+
private func resolveTvRemoteDoublePressDelay() -> TimeInterval {
1054+
guard
1055+
let raw = ProcessInfo.processInfo.environment["AGENT_DEVICE_TV_REMOTE_DOUBLE_PRESS_DELAY_MS"],
1056+
!raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
1057+
else {
1058+
return tvRemoteDoublePressDelayDefault
1059+
}
1060+
guard let parsedMs = Double(raw), parsedMs >= 0 else {
1061+
return tvRemoteDoublePressDelayDefault
1062+
}
1063+
return min(parsedMs, 1000) / 1000.0
1064+
}
1065+
10101066
private func findElement(app: XCUIApplication, text: String) -> XCUIElement? {
10111067
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text)
10121068
let element = app.descendants(matching: .any).matching(predicate).firstMatch
@@ -1109,6 +1165,9 @@ final class RunnerTests: XCTestCase {
11091165
}
11101166

11111167
private func swipe(app: XCUIApplication, direction: SwipeDirection) {
1168+
if performTvRemoteSwipeIfAvailable(direction: direction) {
1169+
return
1170+
}
11121171
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
11131172
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
11141173
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
@@ -1127,6 +1186,24 @@ final class RunnerTests: XCTestCase {
11271186
}
11281187
}
11291188

1189+
private func performTvRemoteSwipeIfAvailable(direction: SwipeDirection) -> Bool {
1190+
#if os(tvOS)
1191+
switch direction {
1192+
case .up:
1193+
XCUIRemote.shared.press(.up)
1194+
case .down:
1195+
XCUIRemote.shared.press(.down)
1196+
case .left:
1197+
XCUIRemote.shared.press(.left)
1198+
case .right:
1199+
XCUIRemote.shared.press(.right)
1200+
}
1201+
return true
1202+
#else
1203+
return false
1204+
#endif
1205+
}
1206+
11301207
private func pinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) {
11311208
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
11321209

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"build": "rslib build",
1717
"clean:daemon": "rm -f ~/.agent-device/daemon.json && rm -f ~/.agent-device/daemon.lock",
1818
"build:node": "pnpm build && pnpm clean:daemon",
19-
"build:xcuitest": "rm -rf ~/.agent-device/ios-runner/derived/device && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
19+
"build:xcuitest": "pnpm build:xcuitest:ios",
20+
"build:xcuitest:ios": "rm -rf ~/.agent-device/ios-runner/derived/device && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
21+
"build:xcuitest:tvos": "rm -rf ~/.agent-device/ios-runner/derived/tvos && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=tvOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived/tvos",
2022
"build:all": "pnpm build:node && pnpm build:xcuitest",
2123
"ad": "node bin/agent-device.mjs",
2224
"format": "prettier --write .",

skills/agent-device/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ agent-device session list
6868
```
6969

7070
Use `boot` only as fallback when `open` cannot find/connect to a ready target.
71+
Use `--target mobile|tv` with `--platform` (required) to pick phone/tablet vs TV targets (AndroidTV/tvOS).
72+
73+
TV quick reference:
74+
- AndroidTV: `open`/`apps` use TV launcher discovery automatically.
75+
- TV target selection works on emulators/simulators and connected physical devices (AndroidTV + AppleTV).
76+
- tvOS: runner-driven interactions and snapshots are supported (`snapshot`, `wait`, `press`, `fill`, `get`, `scroll`, `back`, `home`, `app-switcher`, `record` and related selector flows).
77+
- tvOS `back`/`home`/`app-switcher` map to Siri Remote actions (`menu`, `home`, double-home) in the runner.
78+
- tvOS follows iOS simulator-only command semantics for helpers like `pinch`, `settings`, and `push`.
7179

7280
### Snapshot and targeting
7381

@@ -118,6 +126,7 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
118126
- iOS `appstate` is session-scoped; Android `appstate` is live foreground state.
119127
- Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on Android and iOS simulators; iOS physical devices are not supported yet.
120128
- iOS settings helpers are simulator-only; use `appearance light|dark|toggle` and faceid `match|nonmatch|enroll|unenroll`.
129+
- For AndroidTV/tvOS selection, always pair `--target` with `--platform` (`ios`, `android`, or `apple` alias); target-only selection is invalid.
121130
- `push` simulates notification delivery:
122131
- iOS simulator uses APNs-style payload JSON.
123132
- Android uses broadcast action + typed extras (string/boolean/number).

src/cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,9 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
323323
const name = d?.name ?? d?.id ?? 'unknown';
324324
const platform = d?.platform ?? 'unknown';
325325
const kind = d?.kind ? ` ${d.kind}` : '';
326+
const target = d?.target ? ` target=${d.target}` : '';
326327
const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
327-
return `${name} (${platform}${kind})${booted}`;
328+
return `${name} (${platform}${kind}${target})${booted}`;
328329
});
329330
process.stdout.write(`${lines.join('\n')}\n`);
330331
if (logTailStopper) logTailStopper();

src/core/__tests__/capabilities.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ const androidDevice: DeviceInfo = {
2424
kind: 'device',
2525
};
2626

27+
const androidTvDevice: DeviceInfo = {
28+
platform: 'android',
29+
id: 'and-tv-1',
30+
name: 'Android TV',
31+
kind: 'device',
32+
target: 'tv',
33+
};
34+
35+
const tvOsSimulator: DeviceInfo = {
36+
platform: 'ios',
37+
id: 'tv-sim-1',
38+
name: 'Apple TV',
39+
kind: 'simulator',
40+
target: 'tv',
41+
};
42+
2743
test('iOS simulator-only commands reject iOS devices and Android', () => {
2844
for (const cmd of ['alert', 'pinch']) {
2945
assert.equal(isCommandSupportedOnDevice(cmd, iosSimulator), true, `${cmd} on iOS sim`);
@@ -85,6 +101,24 @@ test('core commands support iOS simulator, iOS device, and Android', () => {
85101
}
86102
});
87103

104+
test('Android TV uses Android capabilities for core commands', () => {
105+
for (const cmd of ['open', 'apps', 'snapshot', 'press', 'swipe', 'back', 'home', 'scroll']) {
106+
assert.equal(isCommandSupportedOnDevice(cmd, androidTvDevice), true, `${cmd} on Android TV`);
107+
}
108+
});
109+
110+
test('tvOS follows iOS capability matrix by device kind', () => {
111+
for (const cmd of ['open', 'close', 'apps', 'screenshot', 'logs', 'reinstall', 'boot']) {
112+
assert.equal(isCommandSupportedOnDevice(cmd, tvOsSimulator), true, `${cmd} on tvOS`);
113+
}
114+
for (const cmd of ['snapshot', 'wait', 'press', 'get', 'fill', 'scroll', 'back', 'home', 'app-switcher', 'record']) {
115+
assert.equal(isCommandSupportedOnDevice(cmd, tvOsSimulator), true, `${cmd} on tvOS`);
116+
}
117+
for (const cmd of ['pinch', 'push', 'settings', 'alert']) {
118+
assert.equal(isCommandSupportedOnDevice(cmd, tvOsSimulator), true, `${cmd} on tvOS simulator`);
119+
}
120+
});
121+
88122
test('unknown commands default to supported', () => {
89123
assert.equal(isCommandSupportedOnDevice('some-future-cmd', iosSimulator), true);
90124
assert.equal(isCommandSupportedOnDevice('some-future-cmd', androidDevice), true);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { resolveTargetDevice } from '../dispatch.ts';
4+
import { AppError } from '../../utils/errors.ts';
5+
6+
test('resolveTargetDevice requires platform when target selector is provided', async () => {
7+
await assert.rejects(
8+
() => resolveTargetDevice({ target: 'tv' }),
9+
(error: unknown) =>
10+
error instanceof AppError &&
11+
error.code === 'INVALID_ARGS' &&
12+
error.message.includes('requires --platform'),
13+
);
14+
});

0 commit comments

Comments
 (0)