Skip to content

Commit cd48822

Browse files
committed
fix: require tracked app for ios snapshots
1 parent e3deb89 commit cd48822

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/daemon/handlers/__tests__/snapshot-handler.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,68 @@ test('snapshot rejects @ref scope without existing session snapshot', async () =
336336
}
337337
});
338338

339+
test('snapshot on iOS rejects sessions without a tracked app', async () => {
340+
const sessionStore = makeSessionStore();
341+
const sessionName = 'ios-sim-no-app';
342+
sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice));
343+
344+
const response = await handleSnapshotCommands({
345+
req: {
346+
token: 't',
347+
session: sessionName,
348+
command: 'snapshot',
349+
positionals: [],
350+
flags: {},
351+
},
352+
sessionName,
353+
logPath: '/tmp/daemon.log',
354+
sessionStore,
355+
});
356+
357+
expect(response?.ok).toBe(false);
358+
if (response?.ok === false) {
359+
expect(response.error.code).toBe('SESSION_NOT_FOUND');
360+
expect(response.error.message).toMatch(/iOS snapshot requires an active app session/i);
361+
}
362+
expect(mockDispatch).not.toHaveBeenCalled();
363+
});
364+
365+
test('snapshot on iOS runs when the session tracks an app', async () => {
366+
const sessionStore = makeSessionStore();
367+
const sessionName = 'ios-sim-app';
368+
sessionStore.set(sessionName, {
369+
...makeSession(sessionName, iosSimulatorDevice),
370+
appBundleId: 'org.reactnavigation.playground',
371+
});
372+
mockDispatch.mockResolvedValue({
373+
nodes: [{ index: 0, depth: 0, type: 'Button', label: 'Home' }],
374+
truncated: false,
375+
backend: 'ios',
376+
});
377+
378+
const response = await handleSnapshotCommands({
379+
req: {
380+
token: 't',
381+
session: sessionName,
382+
command: 'snapshot',
383+
positionals: [],
384+
flags: {},
385+
},
386+
sessionName,
387+
logPath: '/tmp/daemon.log',
388+
sessionStore,
389+
});
390+
391+
expect(response?.ok).toBe(true);
392+
expect(mockDispatch).toHaveBeenCalledWith(
393+
iosSimulatorDevice,
394+
'snapshot',
395+
[],
396+
undefined,
397+
expect.objectContaining({ appBundleId: 'org.reactnavigation.playground' }),
398+
);
399+
});
400+
339401
test('snapshot surfaces filtered-to-zero Android guidance for interactive snapshots', async () => {
340402
const sessionStore = makeSessionStore();
341403
const sessionName = 'android-empty-interactive';

src/daemon/snapshot-runtime.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ async function dispatchSnapshotRuntimeCommand(
115115
}
116116
const resolvedScope = resolveSnapshotScope(req.flags?.snapshotScope, session);
117117
if (!resolvedScope.ok) return resolvedScope;
118+
const iosAppSessionGuard = requireIosAppSessionForSnapshot(params.command, session, device);
119+
if (iosAppSessionGuard) return iosAppSessionGuard;
118120

119121
return await withSessionlessRunnerCleanup(session, device, async () => {
120122
const runtime = createSnapshotRuntime({
@@ -158,6 +160,20 @@ async function dispatchSnapshotRuntimeCommand(
158160
});
159161
}
160162

163+
function requireIosAppSessionForSnapshot(
164+
command: 'snapshot' | 'diff',
165+
session: SessionState | undefined,
166+
device: SessionState['device'],
167+
): DaemonResponse | null {
168+
if (device.platform !== 'ios' || session?.appBundleId) {
169+
return null;
170+
}
171+
return errorResponse(
172+
'SESSION_NOT_FOUND',
173+
`iOS ${command} requires an active app session on the target device. Run open first (for example: open --session ${session?.name ?? 'sim'} --platform ios --device "<name>" <app>).`,
174+
);
175+
}
176+
161177
function createSnapshotRuntime(params: {
162178
req: DaemonRequest;
163179
sessionName: string;

0 commit comments

Comments
 (0)