Files

198 lines
5.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
});
});