Skip to content

Commit 9252097

Browse files
committed
test(recorder): add unit tests for recording pipeline, uploader, spool, and backup
1 parent 553d3f2 commit 9252097

9 files changed

+1873
-0
lines changed

apps/web/__tests__/unit/instant-recording-uploader.test.ts

Lines changed: 731 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
appendLocalRecordingChunk,
4+
finalizeLocalRecording,
5+
initialLocalRecordingState,
6+
} from "@/app/(org)/dashboard/caps/components/web-recorder-dialog/local-recording-backup";
7+
8+
const makeBlob = (size: number, type = "video/webm;codecs=vp9,opus") =>
9+
new Blob([new Uint8Array(size)], { type });
10+
11+
describe("local recording backup", () => {
12+
it("retains a full local copy when configured for capped streaming backup", () => {
13+
const firstChunk = makeBlob(3);
14+
const secondChunk = makeBlob(4);
15+
16+
const afterFirstChunk = appendLocalRecordingChunk(
17+
initialLocalRecordingState(),
18+
firstChunk,
19+
{ mode: "capped", maxBytes: 10 },
20+
);
21+
const afterSecondChunk = appendLocalRecordingChunk(
22+
afterFirstChunk,
23+
secondChunk,
24+
{ mode: "capped", maxBytes: 10 },
25+
);
26+
27+
const blob = finalizeLocalRecording(afterSecondChunk);
28+
29+
expect(afterSecondChunk.overflowed).toBe(false);
30+
expect(afterSecondChunk.retainedBytes).toBe(7);
31+
expect(blob?.size).toBe(7);
32+
expect(blob?.type).toBe("video/webm;codecs=vp9,opus");
33+
});
34+
35+
it("drops the backup copy after the capped limit is exceeded", () => {
36+
const firstChunk = makeBlob(6);
37+
const secondChunk = makeBlob(5);
38+
const thirdChunk = makeBlob(3);
39+
40+
const afterFirstChunk = appendLocalRecordingChunk(
41+
initialLocalRecordingState(),
42+
firstChunk,
43+
{ mode: "capped", maxBytes: 10 },
44+
);
45+
const afterSecondChunk = appendLocalRecordingChunk(
46+
afterFirstChunk,
47+
secondChunk,
48+
{ mode: "capped", maxBytes: 10 },
49+
);
50+
const afterThirdChunk = appendLocalRecordingChunk(
51+
afterSecondChunk,
52+
thirdChunk,
53+
{ mode: "capped", maxBytes: 10 },
54+
);
55+
56+
const blob = finalizeLocalRecording(afterThirdChunk);
57+
58+
expect(afterSecondChunk.overflowed).toBe(true);
59+
expect(afterSecondChunk.retainedBytes).toBe(0);
60+
expect(afterSecondChunk.chunks).toHaveLength(0);
61+
expect(afterThirdChunk.overflowed).toBe(true);
62+
expect(afterThirdChunk.retainedBytes).toBe(0);
63+
expect(afterThirdChunk.chunks).toHaveLength(0);
64+
expect(blob).toBeNull();
65+
});
66+
67+
it("keeps the uncapped buffered fallback intact", () => {
68+
const state = appendLocalRecordingChunk(
69+
initialLocalRecordingState(),
70+
makeBlob(12, "video/mp4"),
71+
{ mode: "full" },
72+
);
73+
74+
const blob = finalizeLocalRecording(state);
75+
76+
expect(state.overflowed).toBe(false);
77+
expect(blob?.size).toBe(12);
78+
expect(blob?.type).toBe("video/mp4");
79+
});
80+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
getMultipartFileKey,
4+
getSubpath,
5+
isRawRecorderUpload,
6+
} from "@/app/api/upload/[...route]/multipart-utils";
7+
8+
describe("multipart upload utils", () => {
9+
it("builds a multipart file key from video id and subpath", () => {
10+
expect(
11+
getMultipartFileKey("user-123", {
12+
videoId: "video-456",
13+
subpath: "raw-upload.webm",
14+
}),
15+
).toBe("user-123/video-456/raw-upload.webm");
16+
});
17+
18+
it("defaults the multipart subpath to result.mp4", () => {
19+
const input: { subpath?: string } = {};
20+
21+
expect(
22+
getMultipartFileKey("user-123", {
23+
videoId: "video-456",
24+
}),
25+
).toBe("user-123/video-456/result.mp4");
26+
expect(getSubpath(input)).toBe("result.mp4");
27+
});
28+
29+
it("parses deprecated fileKey input into the current user-scoped key", () => {
30+
expect(
31+
getMultipartFileKey("user-123", {
32+
fileKey: "legacy-owner/video-456/raw-upload.webm",
33+
}),
34+
).toBe("user-123/video-456/raw-upload.webm");
35+
expect(
36+
getSubpath({
37+
fileKey: "legacy-owner/video-456/raw-upload.webm",
38+
}),
39+
).toBeUndefined();
40+
});
41+
42+
it("detects raw recorder uploads", () => {
43+
expect(isRawRecorderUpload("raw-upload.webm")).toBe(true);
44+
expect(isRawRecorderUpload("raw-upload.mp4")).toBe(true);
45+
expect(isRawRecorderUpload("result.mp4")).toBe(false);
46+
});
47+
48+
it("rejects missing video ids", () => {
49+
expect(() =>
50+
getMultipartFileKey("user-123", {
51+
subpath: "raw-upload.webm",
52+
}),
53+
).toThrow("Video id not found");
54+
});
55+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { moveRecordingSpoolToInMemoryBackup } from "@/app/(org)/dashboard/caps/components/web-recorder-dialog/recording-spool-fallback";
3+
4+
const blobToText = async (blob: Blob) =>
5+
new TextDecoder().decode(await blob.arrayBuffer());
6+
7+
describe("moveRecordingSpoolToInMemoryBackup", () => {
8+
it("merges recovered chunks with later in-memory chunks without duplicating them", async () => {
9+
let retainedChunks = [new Blob(["older"], { type: "video/webm" })];
10+
let releaseRecovery: (() => void) | null = null;
11+
12+
const replaceLocalRecording = vi.fn((chunks: Blob[]) => {
13+
retainedChunks = chunks;
14+
});
15+
16+
const transitionPromise = moveRecordingSpoolToInMemoryBackup({
17+
spool: {
18+
recoverBlob: () =>
19+
new Promise<Blob>((resolve) => {
20+
releaseRecovery = () =>
21+
resolve(new Blob(["persisted"], { type: "video/webm" }));
22+
}),
23+
},
24+
setLocalRecordingStrategy: () => {
25+
retainedChunks = [];
26+
},
27+
getRetainedChunks: () => [...retainedChunks],
28+
replaceLocalRecording,
29+
});
30+
31+
retainedChunks = [
32+
...retainedChunks,
33+
new Blob(["later"], { type: "video/webm" }),
34+
];
35+
36+
releaseRecovery?.();
37+
await transitionPromise;
38+
39+
expect(replaceLocalRecording).toHaveBeenCalledTimes(1);
40+
expect(retainedChunks).toHaveLength(2);
41+
expect(await blobToText(new Blob(retainedChunks))).toBe("persistedlater");
42+
});
43+
});

0 commit comments

Comments
 (0)