Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"!android-snapshot-helper/dist/*.idsig",
"android-multitouch-helper/dist",
"!android-multitouch-helper/dist/*.idsig",
"third_party/serve-sim-camera",
"src/platforms/linux/atspi-dump.py",
"skills",
"server.json",
Expand Down
31 changes: 31 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@ test('apps.open resolves session device identifiers from open response', async (
const result = await client.apps.open({
app: 'Settings',
platform: 'ios',
cameraVideo: './fixtures/back.mp4',
relaunch: true,
});

assert.equal(setup.calls.length, 1);
assert.equal(setup.calls[0]?.command, 'open');
assert.deepEqual(setup.calls[0]?.positionals, ['Settings']);
assert.equal(setup.calls[0]?.flags?.cameraVideo, './fixtures/back.mp4');
assert.equal(result.identifiers.session, 'qa');
assert.equal(result.identifiers.deviceId, 'SIM-001');
assert.equal(result.identifiers.udid, 'SIM-001');
Expand All @@ -80,6 +82,35 @@ test('apps.open resolves session device identifiers from open response', async (
assert.equal(result.device?.ios?.simulatorSetPath, '/tmp/sim-set');
});

test('devices.boot forwards Android emulator camera options', async () => {
const setup = createTransport(async () => ({
ok: true,
data: {
platform: 'android',
target: 'mobile',
device: 'Pixel_9_Pro_XL',
id: 'emulator-5554',
kind: 'emulator',
booted: true,
},
}));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });

await client.devices.boot({
platform: 'android',
device: 'Pixel_9_Pro_XL',
cameraFront: '/tmp/front.mp4',
cameraBack: 'virtualscene',
});

assert.equal(setup.calls.length, 1);
assert.equal(setup.calls[0]?.command, 'boot');
assert.equal(setup.calls[0]?.flags?.platform, 'android');
assert.equal(setup.calls[0]?.flags?.device, 'Pixel_9_Pro_XL');
assert.equal(setup.calls[0]?.flags?.cameraFront, '/tmp/front.mp4');
assert.equal(setup.calls[0]?.flags?.cameraBack, 'virtualscene');
});

test('apps.open forwards explicit runtime hints through the daemon request', async () => {
const setup = createTransport(async () => ({
ok: true,
Expand Down
1 change: 1 addition & 0 deletions src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export type BackendOpenTarget = {
};

export type BackendOpenOptions = {
cameraVideo?: string;
launchArgs?: string[];
relaunch?: boolean;
};
Expand Down
3 changes: 3 additions & 0 deletions src/client-normalizers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
androidDeviceAllowlist: options.androidDeviceAllowlist,
surface: options.surface,
activity: options.activity,
cameraVideo: options.cameraVideo,
launchConsole: options.launchConsole,
launchArgs: options.launchArgs,
relaunch: options.relaunch,
Expand Down Expand Up @@ -311,6 +312,8 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
pauseMs: options.pauseMs,
pattern: options.pattern,
headless: options.headless,
cameraFront: options.cameraFront,
cameraBack: options.cameraBack,
restart: options.restart,
replayUpdate: options.replayUpdate,
replayBackend: options.replayBackend,
Expand Down
7 changes: 7 additions & 0 deletions src/client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export type AppOpenOptions = AgentDeviceRequestOverrides &
url?: string;
surface?: SessionSurface;
activity?: string;
cameraVideo?: string;
launchConsole?: string;
launchArgs?: string[];
relaunch?: boolean;
Expand Down Expand Up @@ -555,6 +556,9 @@ type RepeatedPressOptions = {

export type DeviceBootOptions = DeviceCommandBaseOptions & {
headless?: boolean;
cameraVideo?: string;
cameraFront?: string;
cameraBack?: string;
};

export type DeviceShutdownOptions = DeviceCommandBaseOptions;
Expand Down Expand Up @@ -850,6 +854,8 @@ type CommandExecutionOptions = Partial<ScreenshotRequestFlags> & {
pauseMs?: number;
pattern?: SwipePattern;
headless?: boolean;
cameraFront?: string;
cameraBack?: string;
restart?: boolean;
replayUpdate?: boolean;
replayBackend?: string;
Expand Down Expand Up @@ -878,6 +884,7 @@ export type InternalRequestOptions = AgentDeviceClientConfig &
overlayRefs?: boolean;
surface?: SessionSurface;
activity?: string;
cameraVideo?: string;
launchConsole?: string;
launchArgs?: string[];
relaunch?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const MAX_APP_PUSH_PAYLOAD_BYTES = 8 * 1024;

export type OpenAppCommandOptions = CommandContext &
BackendOpenTarget & {
cameraVideo?: string;
launchArgs?: string[];
relaunch?: boolean;
};
Expand Down Expand Up @@ -105,6 +106,7 @@ export const openAppCommand: RuntimeCommand<OpenAppCommandOptions, OpenAppComman
toAppBackendContext(runtime, options),
target,
{
...(options.cameraVideo !== undefined ? { cameraVideo: options.cameraVideo } : {}),
launchArgs: options.launchArgs,
relaunch: options.relaunch,
},
Expand Down
3 changes: 3 additions & 0 deletions src/commands/cli-grammar/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const appCliReaders = {
boot: (_positionals, flags) => ({
...commonInputFromFlags(flags),
headless: flags.headless,
cameraFront: flags.cameraFront,
cameraBack: flags.cameraBack,
}),
shutdown: (_positionals, flags) => commonInputFromFlags(flags),
prepare: (positionals, flags) => ({
Expand All @@ -41,6 +43,7 @@ export const appCliReaders = {
url: positionals[1],
surface: flags.surface,
activity: flags.activity,
cameraVideo: flags.cameraVideo,
launchConsole: flags.launchConsole,
launchArgs: flags.launchArgs,
relaunch: flags.relaunch,
Expand Down
3 changes: 3 additions & 0 deletions src/commands/client-command-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const clientCommandMetadata = [
defineClientCommandMetadata('devices', {}),
defineClientCommandMetadata('boot', {
headless: booleanField('Boot without showing simulator UI when supported.'),
cameraFront: stringField('Android emulator front camera mode or video file path used at boot.'),
cameraBack: stringField('Android emulator back camera mode or video file path used at boot.'),
}),
defineClientCommandMetadata('shutdown', {}),
defineClientCommandMetadata('prepare', {
Expand All @@ -54,6 +56,7 @@ export const clientCommandMetadata = [
url: stringField('Optional URL passed with an app shell.'),
surface: enumField(SESSION_SURFACES),
activity: stringField('Android activity name.'),
cameraVideo: stringField('iOS simulator video file path injected as the app camera stream.'),
launchConsole: stringField('Launch console mode.'),
launchArgs: stringArrayField(
'Launch arguments forwarded verbatim to the platform launch command.',
Expand Down
46 changes: 46 additions & 0 deletions src/core/__tests__/dispatch-open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,52 @@ test('dispatch open rejects launch arguments without an app target', async () =>
);
});

test('dispatch open rejects camera video without an app target', async () => {
await assert.rejects(
() => dispatchCommand(IOS_SIMULATOR, 'open', [], undefined, { cameraVideo: './back.mp4' }),
(error: unknown) => {
assert.equal(error instanceof AppError, true);
assert.equal((error as AppError).code, 'INVALID_ARGS');
assert.match((error as AppError).message, /requires an app target/i);
return true;
},
);
});

test('dispatch open forwards iOS simulator camera video to openIosApp', async () => {
await dispatchCommand(IOS_SIMULATOR, 'open', ['com.example.app'], undefined, {
cameraVideo: '/tmp/back.mp4',
});

assert.equal(mockOpenIosApp.mock.calls.length, 1);
assert.equal(mockOpenIosApp.mock.calls[0]?.[0], IOS_SIMULATOR);
assert.equal(mockOpenIosApp.mock.calls[0]?.[1], 'com.example.app');
assert.equal(mockOpenIosApp.mock.calls[0]?.[2]?.cameraVideo, '/tmp/back.mp4');
});

test('dispatch open rejects camera video outside iOS simulator', async () => {
const device: DeviceInfo = {
platform: 'android',
id: 'emulator-5554',
name: 'Pixel',
kind: 'emulator',
booted: true,
};

await assert.rejects(
() =>
dispatchCommand(device, 'open', ['com.example.app'], undefined, {
cameraVideo: './back.mp4',
}),
(error: unknown) => {
assert.equal(error instanceof AppError, true);
assert.equal((error as AppError).code, 'UNSUPPORTED_OPERATION');
assert.match((error as AppError).message, /iOS simulators/i);
return true;
},
);
});

test('dispatch open forwards Android launch arguments to openAndroidApp', async () => {
const device: DeviceInfo = {
platform: 'android',
Expand Down
1 change: 1 addition & 0 deletions src/core/dispatch-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type DispatchContext = ScreenshotDispatchFlags & {
requestId?: string;
appBundleId?: string;
activity?: string;
cameraVideo?: string;
launchConsole?: string;
launchArgs?: string[];
clearAppState?: boolean;
Expand Down
15 changes: 15 additions & 0 deletions src/core/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ async function handleOpenCommand(
): Promise<Record<string, unknown>> {
const app = positionals[0];
const url = positionals[1];
const cameraVideo = context?.cameraVideo;
const launchConsole = context?.launchConsole;
const launchArgs = context?.launchArgs;
if (positionals.length > 2) {
Expand All @@ -187,9 +188,18 @@ async function handleOpenCommand(
if (launchArgs && launchArgs.length > 0) {
throw new AppError('INVALID_ARGS', '--launch-args requires an app target');
}
if (cameraVideo) {
throw new AppError('INVALID_ARGS', '--camera-video requires an app target');
}
await interactor.openDevice();
return { app: null, ...successText('Opened device') };
}
if (cameraVideo && (device.platform !== 'ios' || device.kind !== 'simulator')) {
throw new AppError(
'UNSUPPORTED_OPERATION',
'--camera-video is supported only for iOS simulators.',
);
}
if (launchConsole && (device.platform !== 'ios' || device.kind !== 'simulator')) {
throw new AppError('UNSUPPORTED_OPERATION', LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE);
}
Expand All @@ -212,6 +222,7 @@ async function handleOpenCommand(
await interactor.open(app, {
activity: context?.activity,
appBundleId: context?.appBundleId,
cameraVideo,
launchArgs,
url,
});
Expand All @@ -220,6 +231,9 @@ async function handleOpenCommand(
if (launchConsole && isDeepLinkTarget(app)) {
throw new AppError('INVALID_ARGS', LAUNCH_CONSOLE_DIRECT_APP_ONLY_MESSAGE);
}
if (cameraVideo && isDeepLinkTarget(app)) {
throw new AppError('INVALID_ARGS', '--camera-video requires an app target');
}
if (context?.clearAppState) {
if (isDeepLinkTarget(app)) {
throw new AppError(
Expand All @@ -232,6 +246,7 @@ async function handleOpenCommand(
await interactor.open(app, {
activity: context?.activity,
appBundleId: context?.appBundleId,
cameraVideo,
launchConsole,
launchArgs,
});
Expand Down
1 change: 1 addition & 0 deletions src/core/interactor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type Interactor = {
options?: {
activity?: string;
appBundleId?: string;
cameraVideo?: string;
launchConsole?: string;
launchArgs?: string[];
url?: string;
Expand Down
1 change: 1 addition & 0 deletions src/core/interactors/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function createAppleInteractor(
open: (app, options) =>
openIosApp(device, app, {
appBundleId: options?.appBundleId,
cameraVideo: options?.cameraVideo,
launchConsole: options?.launchConsole,
launchArgs: options?.launchArgs,
url: options?.url,
Expand Down
3 changes: 3 additions & 0 deletions src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type OpenAppOptions = {
udid?: NonNullable<DaemonRequest['flags']>['udid'];
serial?: NonNullable<DaemonRequest['flags']>['serial'];
activity?: NonNullable<DaemonRequest['flags']>['activity'];
cameraVideo?: NonNullable<DaemonRequest['flags']>['cameraVideo'];
launchConsole?: NonNullable<DaemonRequest['flags']>['launchConsole'];
launchArgs?: NonNullable<DaemonRequest['flags']>['launchArgs'];
out?: NonNullable<DaemonRequest['flags']>['out'];
Expand Down Expand Up @@ -226,6 +227,7 @@ export async function openApp(options: OpenAppOptions = {}): Promise<DaemonRespo
udid,
serial,
activity,
cameraVideo,
launchConsole,
launchArgs,
out,
Expand All @@ -248,6 +250,7 @@ export async function openApp(options: OpenAppOptions = {}): Promise<DaemonRespo
...(udid !== undefined ? { udid } : {}),
...(serial !== undefined ? { serial } : {}),
...(activity !== undefined ? { activity } : {}),
...(cameraVideo !== undefined ? { cameraVideo } : {}),
...(launchConsole !== undefined ? { launchConsole } : {}),
...(launchArgs !== undefined ? { launchArgs } : {}),
...(out !== undefined ? { out } : {}),
Expand Down
6 changes: 6 additions & 0 deletions src/daemon/__tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ test('contextFromFlags forwards generic app-state clearing', () => {
assert.equal(context.clearAppState, true);
});

test('contextFromFlags forwards iOS simulator camera video path', () => {
const flags: CommandFlags = { cameraVideo: './fixtures/camera-feed.mp4' };
const context = contextFromFlags('/tmp/agent-device.log', flags);
assert.equal(context.cameraVideo, './fixtures/camera-feed.mp4');
});

test('contextFromFlags forwards screenshot flags from CLI flags', () => {
const flags: CommandFlags = {
screenshotFullscreen: true,
Expand Down
1 change: 1 addition & 0 deletions src/daemon/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function contextFromFlags(
requestId: effectiveRequestId,
appBundleId,
activity: flags?.activity,
cameraVideo: flags?.cameraVideo,
launchConsole: flags?.launchConsole,
launchArgs: flags?.launchArgs,
clearAppState: flags?.clearAppState,
Expand Down
Loading