Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Fixed (android): a chunked `record stop` (recordings over 170 s) no longer warns that screenrecord
stopped before record stop at the 180 s limit. Rotation always ends every earlier chunk before
stop, so the warning now fires only when the last chunk's recorder had already exited.
- Fixed: an iOS snapshot whose XCTest query-sweep tier cannot read the screen no longer ends the
runner process. On a live React Native feed (Bluesky Home, images re-rendering) the AX server
rejects each of the sweep's 19 element-type queries with `kAXErrorIllegalArgument`, and XCTest
Expand Down
46 changes: 46 additions & 0 deletions packages/platform-android/src/recording/chunks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ test('returns secondary client paths, split/180s warnings, and skips chunked tou
}
});

async function finishedWarning(params: {
rotate: boolean;
exitedBeforeStop: readonly string[];
}): Promise<string | undefined> {
vi.useFakeTimers();
try {
let nextPid = 41;
const exited = new Set(params.exitedBeforeStop);
const runtime = await start({
start: async () => recordingProcess(String(++nextPid)),
inspect: async ({ pid }: { pid: string }) => (exited.has(pid) ? 'missing' : 'owned-alive'),
stop: async ({ pid }: { pid: string }) => {
if (exited.has(pid)) return 'already-missing' as const;
exited.add(pid);
return 'stopped' as const;
},
});
const started = await runtime.screenRecordingStart(recordingInput());
const handle = started.pendingHandle.transfer();
if (params.rotate) await vi.advanceTimersByTimeAsync(170_000);
const finishing = handle.finish();
await vi.advanceTimersByTimeAsync(1_000);
const outcome = await finishing;
expect(outcome.status).toBe('completed');
return outcome.status === 'completed' ? outcome.result.warning : undefined;
} finally {
vi.useRealTimers();
}
}

test('omits the 180s limit warning when only a rotated-out chunk exited before record stop', async () => {
// Rotation stops chunk 42, so it is already missing at record stop; chunk 43 is still recording.
const warning = await finishedWarning({ rotate: true, exitedBeforeStop: [] });
expect(warning).toContain('split into multiple MP4 chunks');
expect(warning).not.toContain('likely after reaching the 180s platform limit');
});

test('warns about the 180s limit when the last chunk exited before record stop', async () => {
await expect(finishedWarning({ rotate: false, exitedBeforeStop: ['42'] })).resolves.toContain(
'likely after reaching the 180s platform limit',
);
await expect(finishedWarning({ rotate: true, exitedBeforeStop: ['43'] })).resolves.toContain(
'likely after reaching the 180s platform limit',
);
});

test('continues through every owned chunk after a stop or removal failure', async () => {
const chunks = [
{
Expand Down
12 changes: 9 additions & 3 deletions packages/platform-android/src/recording/chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,27 @@ function validProcessIdentity(
);
}

/**
* Resolves whether the active (last) chunk's recorder had already exited, i.e. the video ends before
* record stop. Earlier chunks always end before stop because rotation replaced them.
*/
export async function stopOwnedChunks(
transport: Transport,
chunks: readonly NativeChunk[],
): Promise<boolean> {
let reachedLimit = false;
const active = chunks.at(-1);
let activeAlreadyExited = false;
let failure: unknown;
for (const chunk of [...chunks].reverse()) {
try {
reachedLimit = (await stopChunk(transport, chunk)) || reachedLimit;
const alreadyExited = await stopChunk(transport, chunk);
if (chunk === active) activeAlreadyExited = alreadyExited;
} catch (error) {
failure ??= error;
}
}
if (failure) throw failure;
return reachedLimit;
return activeAlreadyExited;
}

export async function waitForStableArtifacts(
Expand Down
Loading