Files
STTracker/apps/web/test/format.test.ts
T
Lucas WintherandClaude Opus 5.5 5fa59310f3 fix(web): review round 1: optimistic edits, friendly errors, preferences, Gelasio
- Tags and custom fields update the cached entry optimistically and build
  from the latest cache; one entry's PATCHes run in order (mutation scope).
- The review textarea follows server changes (Restore) unless edited.
- friendlyError() maps validation reasons and 413 to plain copy everywhere.
- Rating scale from me/preferences; /login and /register use auth/status;
  Attachments lists from the API.
- Gelasio vendored (official OFL variable TTFs); minors from the review.
- Docs: DESIGN (AA on orange, faint labels), DECISIONS, TODO, ARCHITECTURE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:03:51 +01:00

97 lines
5.3 KiB
TypeScript
Raw 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 { formatDateRange, formatPartialDate, formatRating, friendlyError, parsePartialDate, rollupLabel, shortDate } from "../src/format.ts";
describe("formatRating", () => {
test("5 stars with halves", () => {
expect(formatRating(90, "5half")).toBe("★★★★½");
expect(formatRating(100, "5half")).toBe("★★★★★");
expect(formatRating(80, "5half")).toBe("★★★★☆");
expect(formatRating(0, "5half")).toBe("☆☆☆☆☆");
});
test("null is empty on every scale", () => {
expect(formatRating(null, "5half")).toBe("");
expect(formatRating(null, "10")).toBe("");
expect(formatRating(undefined, "100")).toBe("");
});
test("whole stars, 10 and 100 point scales", () => {
expect(formatRating(90, "5")).toBe("★★★★★");
expect(formatRating(70, "5")).toBe("★★★★☆");
expect(formatRating(90, "10")).toBe("9/10");
expect(formatRating(85, "10")).toBe("8.5/10");
expect(formatRating(73, "100")).toBe("73/100");
});
});
describe("formatPartialDate", () => {
test("renders by precision", () => {
expect(formatPartialDate({ value: "2025-03", precision: "month" })).toBe("Mar 2025");
expect(formatPartialDate({ value: "2019", precision: "year" })).toBe("2019");
expect(formatPartialDate({ value: "2026-09-14", precision: "day" })).toBe("14 Sep 2026");
expect(formatPartialDate({ value: "2026-09-04T23:30:00-05:00", precision: "day" })).toBe("4 Sep 2026");
expect(formatPartialDate({ value: "", precision: "unknown" })).toBe("");
expect(formatPartialDate(null)).toBe("");
});
});
describe("formatDateRange", () => {
test("flags dates that are not exact", () => {
expect(formatDateRange(null, { value: "2025-03", precision: "month" })).toBe("Mar 2025 (no exact date)");
expect(formatDateRange({ value: "2026-09-02", precision: "day" }, { value: "2026-09-14", precision: "day" })).toBe(
"2 Sep – 14 Sep 2026",
);
expect(formatDateRange({ value: "2025-12-30", precision: "day" }, { value: "2026-01-02", precision: "day" })).toBe(
"30 Dec 2025 – 2 Jan 2026",
);
expect(formatDateRange({ value: "2026-09-02", precision: "day" }, null)).toBe("Started 2 Sep 2026");
expect(formatDateRange(null, null)).toBe("No dates");
});
});
describe("parsePartialDate", () => {
test("accepts year, month and day, rejects the rest", () => {
expect(parsePartialDate("2019")).toEqual({ value: "2019", precision: "year" });
expect(parsePartialDate(" 2025-03 ")).toEqual({ value: "2025-03", precision: "month" });
expect(parsePartialDate("2024-02-29")).toEqual({ value: "2024-02-29", precision: "day" });
expect(parsePartialDate("2023-02-29")).toBeNull();
expect(parsePartialDate("03/04/2019")).toBeNull();
expect(parsePartialDate("")).toBeNull();
});
});
describe("rollupLabel", () => {
test("children roll up as 'season 3 of 5'", () => {
expect(rollupLabel("show", { total: 5, completed: 3 })).toBe("season 3 of 5");
expect(rollupLabel("show", { total: 5, completed: 0 })).toBe("5 seasons");
expect(rollupLabel("podcast", { total: 200, completed: 200 })).toBe("all 200 episodes");
expect(rollupLabel("book", null)).toBeNull();
});
});
describe("friendlyError", () => {
const v = (message: string, status = 400) => ({ status, code: "validation", message });
test("maps validation reasons to plain copy", () => {
expect(friendlyError(v("customFields.Website: not_url"))).toBe("Website needs a full web address, starting with https://.");
expect(friendlyError(v("finishedAt: not_date"))).toBe("Finished needs a date like 2025, 2025-03 or 2025-03-14.");
expect(friendlyError(v("rating: out_of_range"))).toBe("Rating is out of range. Ratings go from 0 to 100.");
expect(friendlyError(v("title: required"))).toBe("Title can't be empty.");
expect(friendlyError(v("review: too_long"))).toBe("Review is too long. Shorten it and try again.");
expect(friendlyError(v("password: Too small: expected string to have >=8 characters"))).toBe("Passwords need at least 8 characters.");
expect(friendlyError(v("title: Too big: expected string to have <=1000 characters"))).toBe("Title is too long. Shorten it and try again.");
expect(friendlyError(v("mime: unsupported file type"))).toBe("That file type isn't supported. Use an image, a PDF or a text file.");
expect(friendlyError(v("customFields.Mood: unknown_option"))).toBe("Mood has an option that no longer exists. Pick one from the list.");
expect(friendlyError(v("displayName: weird_reason"))).toBe("Display name wasn't accepted. Check it and try again.");
});
test("413 means the file is too large", () => {
expect(friendlyError({ status: 413, code: "validation", message: "Request body is too large (max 50 MB)." })).toBe("That file is too large. The limit is 25 MB.");
expect(friendlyError(v("file: larger than 25 MB"))).toBe("That file is too large. The limit is 25 MB.");
});
test("other API errors keep the server's message; unknown errors get a generic line", () => {
expect(friendlyError({ status: 401, code: "unauthenticated", message: "Wrong username or password." })).toBe("Wrong username or password.");
expect(friendlyError(new Error("boom"))).toBe("Something went wrong. Try again.");
});
});
test("shortDate", () => {
expect(shortDate("2026-10-12T10:00:00")).toBe("12 Oct");
});