198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import {
|
||
exportFilename,
|
||
formatClockTimestamp,
|
||
formatSrtTimestamp,
|
||
transcriptToSrt,
|
||
transcriptToTimestampedTxt,
|
||
transcriptToTxt,
|
||
} from "../src/lib/export";
|
||
import type { TranscriptChunk } from "../src/worker/protocol";
|
||
|
||
describe("transcriptToTxt", () => {
|
||
test("trims and appends a single trailing newline", () => {
|
||
expect(transcriptToTxt({ text: " Hello world. " })).toBe("Hello world.\n");
|
||
});
|
||
|
||
test("empty transcript becomes a bare newline", () => {
|
||
expect(transcriptToTxt({ text: "" })).toBe("\n");
|
||
});
|
||
});
|
||
|
||
describe("formatSrtTimestamp", () => {
|
||
test("zero", () => {
|
||
expect(formatSrtTimestamp(0)).toBe("00:00:00,000");
|
||
});
|
||
|
||
test("pads milliseconds to three digits", () => {
|
||
expect(formatSrtTimestamp(1.5)).toBe("00:00:01,500");
|
||
expect(formatSrtTimestamp(1.05)).toBe("00:00:01,050");
|
||
expect(formatSrtTimestamp(1.005)).toBe("00:00:01,005");
|
||
});
|
||
|
||
test("rolls over past one hour", () => {
|
||
expect(formatSrtTimestamp(3661.25)).toBe("01:01:01,250");
|
||
});
|
||
|
||
test("rounds sub-millisecond values", () => {
|
||
expect(formatSrtTimestamp(0.9996)).toBe("00:00:01,000");
|
||
});
|
||
|
||
test("clamps negatives to zero", () => {
|
||
expect(formatSrtTimestamp(-1)).toBe("00:00:00,000");
|
||
});
|
||
});
|
||
|
||
describe("transcriptToSrt", () => {
|
||
const chunk = (
|
||
text: string,
|
||
start: number,
|
||
end: number | null,
|
||
): TranscriptChunk => ({ text, timestamp: [start, end] });
|
||
|
||
test("formats numbered cues separated by blank lines", () => {
|
||
const srt = transcriptToSrt([
|
||
chunk(" Hello.", 0, 2.5),
|
||
chunk(" World.", 2.5, 4),
|
||
]);
|
||
expect(srt).toBe(
|
||
"1\n00:00:00,000 --> 00:00:02,500\nHello.\n" +
|
||
"\n" +
|
||
"2\n00:00:02,500 --> 00:00:04,000\nWorld.\n",
|
||
);
|
||
});
|
||
|
||
test("null end borrows the next chunk's start", () => {
|
||
const srt = transcriptToSrt([
|
||
chunk("One", 0, null),
|
||
chunk("Two", 3, 5),
|
||
]);
|
||
expect(srt).toContain("1\n00:00:00,000 --> 00:00:03,000\nOne");
|
||
});
|
||
|
||
test("null end on the final chunk falls back to start + 2s", () => {
|
||
const srt = transcriptToSrt([chunk("Tail", 10, null)]);
|
||
expect(srt).toBe("1\n00:00:10,000 --> 00:00:12,000\nTail\n");
|
||
});
|
||
|
||
test("skips empty and whitespace-only chunks (numbering stays contiguous)", () => {
|
||
const srt = transcriptToSrt([
|
||
chunk(" ", 0, 1),
|
||
chunk("Kept", 1, null),
|
||
chunk("", 2, 3),
|
||
chunk("Also kept", 4, 5),
|
||
]);
|
||
// The null end skips over the empty chunk to the next *captioned* start.
|
||
expect(srt).toBe(
|
||
"1\n00:00:01,000 --> 00:00:04,000\nKept\n" +
|
||
"\n" +
|
||
"2\n00:00:04,000 --> 00:00:05,000\nAlso kept\n",
|
||
);
|
||
});
|
||
|
||
test("cue never ends before it starts", () => {
|
||
// Out-of-order borrow: next captioned chunk starts before this one.
|
||
const srt = transcriptToSrt([chunk("A", 5, null), chunk("B", 4, 6)]);
|
||
expect(srt).toContain("00:00:05,000 --> 00:00:05,000");
|
||
});
|
||
|
||
test("no captioned chunks yields an empty string", () => {
|
||
expect(transcriptToSrt([])).toBe("");
|
||
expect(transcriptToSrt([chunk(" ", 0, 1)])).toBe("");
|
||
});
|
||
});
|
||
|
||
describe("formatClockTimestamp", () => {
|
||
test("zero", () => {
|
||
expect(formatClockTimestamp(0)).toBe("00:00.0");
|
||
});
|
||
|
||
test("pads minutes and seconds", () => {
|
||
expect(formatClockTimestamp(3.21)).toBe("00:03.2");
|
||
expect(formatClockTimestamp(65.4)).toBe("01:05.4");
|
||
});
|
||
|
||
test("rounding to tenths carries into the minutes", () => {
|
||
expect(formatClockTimestamp(59.96)).toBe("01:00.0");
|
||
});
|
||
|
||
test("minutes keep growing past an hour", () => {
|
||
expect(formatClockTimestamp(3661.5)).toBe("61:01.5");
|
||
});
|
||
|
||
test("clamps negatives to zero", () => {
|
||
expect(formatClockTimestamp(-1)).toBe("00:00.0");
|
||
});
|
||
});
|
||
|
||
describe("transcriptToTimestampedTxt", () => {
|
||
const chunk = (
|
||
text: string,
|
||
start: number,
|
||
end: number | null,
|
||
): TranscriptChunk => ({ text, timestamp: [start, end] });
|
||
|
||
test("one bracketed range per line, trailing newline", () => {
|
||
const txt = transcriptToTimestampedTxt([
|
||
chunk(" Hello.", 3.21, 7.9),
|
||
chunk(" World.", 7.9, 12),
|
||
]);
|
||
expect(txt).toBe("[00:03.2 – 00:07.9] Hello.\n[00:07.9 – 00:12.0] World.\n");
|
||
});
|
||
|
||
test("null end omits the range end", () => {
|
||
expect(transcriptToTimestampedTxt([chunk("Tail", 10, null)])).toBe(
|
||
"[00:10.0] Tail\n",
|
||
);
|
||
});
|
||
|
||
test("skips empty and whitespace-only chunks", () => {
|
||
const txt = transcriptToTimestampedTxt([
|
||
chunk(" ", 0, 1),
|
||
chunk("Kept", 1, 2),
|
||
]);
|
||
expect(txt).toBe("[00:01.0 – 00:02.0] Kept\n");
|
||
});
|
||
|
||
test("no captioned chunks yields an empty string", () => {
|
||
expect(transcriptToTimestampedTxt([])).toBe("");
|
||
expect(transcriptToTimestampedTxt([chunk(" ", 0, 1)])).toBe("");
|
||
});
|
||
});
|
||
|
||
describe("exportFilename", () => {
|
||
test("swaps the extension", () => {
|
||
expect(exportFilename("talk.m4a", "srt")).toBe("talk.srt");
|
||
expect(exportFilename("talk.m4a", "txt")).toBe("talk.txt");
|
||
});
|
||
|
||
test("only the last dot counts", () => {
|
||
expect(exportFilename("2026.07.14 meeting.wav", "srt")).toBe(
|
||
"2026.07.14 meeting.srt",
|
||
);
|
||
});
|
||
|
||
test("no extension keeps the whole name", () => {
|
||
expect(exportFilename("recording", "txt")).toBe("recording.txt");
|
||
});
|
||
|
||
test("leading dot is a hidden file, not an extension", () => {
|
||
expect(exportFilename(".bashrc", "txt")).toBe(".bashrc.txt");
|
||
});
|
||
|
||
test("compound extension keeps timestamped exports distinct", () => {
|
||
expect(exportFilename("talk.m4a", "timestamps.txt")).toBe(
|
||
"talk.timestamps.txt",
|
||
);
|
||
expect(exportFilename(null, "timestamps.txt")).toBe(
|
||
"transcript.timestamps.txt",
|
||
);
|
||
});
|
||
|
||
test("null, undefined, and blank fall back to transcript.*", () => {
|
||
expect(exportFilename(null, "srt")).toBe("transcript.srt");
|
||
expect(exportFilename(undefined, "txt")).toBe("transcript.txt");
|
||
expect(exportFilename(" ", "srt")).toBe("transcript.srt");
|
||
});
|
||
});
|