Files
Lyra/web/src/app/_ui/status.test.ts
T
Jonathan c49cee9a20 feat: download ETA on the press bar from measured Soulseek throughput
Show a live "~Xm left" countdown on the download progress bar. The slskd download
loop already polls byte transfer every 3s; measure the actual throughput (EWMA-
smoothed bytes/sec), compute seconds-remaining from the album's total bytes, and
surface it. New nullable Job.downloadEtaSeconds (migration; web entrypoint runs
prisma migrate deploy), threaded through the on_progress callback (optional 2nd arg,
so other adapters/callers are unaffected — they report no ETA). API exposes it;
queue.tsx renders etaLabel() after the track count. Null when unknown (no sample yet
or a source that doesn't report bytes). Worker 330 tests, web 214 tests, both green;
verified live-rendered as "42% · 6/14 tracks · ~4m left".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 20:16:58 +02:00

61 lines
2.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { describeJob, etaLabel, timeAgo } from "./status";
describe("etaLabel", () => {
it("formats seconds, minutes, and hours", () => {
expect(etaLabel(45)).toBe("~45s left");
expect(etaLabel(240)).toBe("~4m left");
expect(etaLabel(3900)).toBe("~1h 5m left");
});
it("returns null for missing or non-positive input", () => {
expect(etaLabel(null)).toBeNull();
expect(etaLabel(undefined)).toBeNull();
expect(etaLabel(0)).toBeNull();
expect(etaLabel(-5)).toBeNull();
});
});
describe("describeJob", () => {
it("maps pipeline states to literal labels + severity kind", () => {
expect(describeJob("requested", "intake")).toMatchObject({ label: "Queued", kind: "idle" });
expect(describeJob("matching", "match")).toMatchObject({ label: "Searching", kind: "working" });
expect(describeJob("matched", "match")).toMatchObject({ label: "Searching", kind: "working" });
expect(describeJob("downloading", "download")).toMatchObject({ label: "Downloading", kind: "working" });
expect(describeJob("tagging", "tag")).toMatchObject({ label: "Finishing", kind: "verify" });
expect(describeJob("imported", "import")).toMatchObject({ label: "Pressed", kind: "done" });
expect(describeJob("needs_attention", "download")).toMatchObject({ label: "Needs attention", kind: "attention" });
});
it("falls back gracefully for unknown states", () => {
expect(describeJob("weird", "intake").kind).toBe("idle");
expect(describeJob("weird", "intake").label).toBe("Weird");
});
});
describe("timeAgo", () => {
const now = new Date("2026-07-13T12:00:00Z").getTime();
it("returns 'just now' for deltas under 60s", () => {
expect(timeAgo(new Date(now - 30_000).toISOString(), now)).toBe("just now");
});
it("returns minutes for sub-hour deltas", () => {
expect(timeAgo(new Date(now - 5 * 60_000).toISOString(), now)).toBe("5m");
});
it("returns hours for sub-day deltas", () => {
expect(timeAgo(new Date(now - 2 * 3600_000).toISOString(), now)).toBe("2h");
});
it("returns days for multi-day deltas", () => {
expect(timeAgo(new Date(now - 3 * 86_400_000).toISOString(), now)).toBe("3d");
});
it("returns empty string for null/undefined/invalid input", () => {
expect(timeAgo(null, now)).toBe("");
expect(timeAgo(undefined, now)).toBe("");
expect(timeAgo("not-a-date", now)).toBe("");
});
});