chore: extract shared tsconfig, move display-status to core, add i18n tests

- Add `@sofa/tsconfig` package with a shared `base.json` (target, lib, strict, moduleResolution, etc.) and migrate all packages/apps to `"extends": "@sofa/tsconfig/base.json"` to eliminate duplicated compiler options
- Move `display-status.ts` (and its `DisplayStatus` type + `getDisplayStatus` fn) from `@sofa/api` to `@sofa/core`; remove the `./display-status` export from `@sofa/api` and update all import sites in `discovery.ts` and `tracking.ts`
- Add `packages/core/test/display-status.test.ts`: 15 tests covering `ONGOING_TMDB_STATUSES` and all `getDisplayStatus` branches (movie, TV watchlist/in_progress/caught_up/completed, edge cases)
- Add `packages/i18n/vitest.config.ts` and two new test files: `date-buckets.test.ts` and `format.test.ts` covering date bucketing and locale formatting utilities
This commit is contained in:
2026-03-22 14:46:25 -04:00
parent 6be586bd53
commit 1d67b201a0
34 changed files with 508 additions and 180 deletions
+111
View File
@@ -0,0 +1,111 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("../src/index", () => ({
i18n: {
locale: "en",
_: (descriptor: { id?: string; message?: string } | string) => {
if (typeof descriptor === "string") return descriptor;
return descriptor.message ?? descriptor.id ?? "";
},
},
}));
vi.mock("@lingui/core/macro", () => ({
msg: (strings: TemplateStringsArray, ...values: unknown[]) => ({
id: String.raw(strings, ...values),
message: String.raw(strings, ...values),
}),
}));
import { formatLocalDate, groupByDateBucket } from "../src/date-buckets";
describe("formatLocalDate", () => {
test("formats a date as YYYY-MM-DD", () => {
expect(formatLocalDate(new Date(2024, 0, 5))).toBe("2024-01-05");
});
test("pads single-digit month and day", () => {
expect(formatLocalDate(new Date(2024, 2, 3))).toBe("2024-03-03");
});
test("handles December correctly", () => {
expect(formatLocalDate(new Date(2024, 11, 31))).toBe("2024-12-31");
});
});
describe("groupByDateBucket", () => {
// Pin "today" to a Wednesday: 2024-06-12
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-06-12T10:00:00"));
});
afterEach(() => {
vi.useRealTimers();
});
test("empty array returns empty result", () => {
expect(groupByDateBucket([])).toEqual([]);
});
test("item on today goes to 'today' bucket", () => {
const result = groupByDateBucket([{ date: "2024-06-12" }]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("today");
expect(result[0].items).toHaveLength(1);
});
test("item on tomorrow goes to 'tomorrow' bucket", () => {
const result = groupByDateBucket([{ date: "2024-06-13" }]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("tomorrow");
});
test("item within 6 days goes to 'this_week' bucket", () => {
// Today is June 12 (Wed), end of week = June 18 (Tue)
const result = groupByDateBucket([{ date: "2024-06-17" }]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("this_week");
});
test("item 7-13 days out goes to 'next_week' bucket", () => {
// End of week = June 18, next week ends June 25
const result = groupByDateBucket([{ date: "2024-06-22" }]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("next_week");
});
test("item 30+ days out goes to month bucket", () => {
const result = groupByDateBucket([{ date: "2024-08-15" }]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("month_2024-08");
});
test("multiple items in same bucket are grouped together", () => {
const result = groupByDateBucket([
{ date: "2024-06-12", name: "a" },
{ date: "2024-06-12", name: "b" },
]);
expect(result).toHaveLength(1);
expect(result[0].key).toBe("today");
expect(result[0].items).toHaveLength(2);
});
test("items across multiple buckets are ordered by first appearance", () => {
const result = groupByDateBucket([
{ date: "2024-06-13" }, // tomorrow
{ date: "2024-06-12" }, // today
{ date: "2024-08-01" }, // month
]);
expect(result).toHaveLength(3);
expect(result[0].key).toBe("tomorrow");
expect(result[1].key).toBe("today");
expect(result[2].key).toBe("month_2024-08");
});
test("bucket labels are populated", () => {
const result = groupByDateBucket([{ date: "2024-06-12" }, { date: "2024-06-13" }]);
expect(result[0].label).toBeTruthy();
expect(result[1].label).toBeTruthy();
});
});
+148
View File
@@ -0,0 +1,148 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("../src/index", () => ({
i18n: { locale: "en" },
}));
import {
formatBytes,
formatDate,
formatNumber,
formatRelativeTime,
formatShortDate,
} from "../src/format";
describe("formatDate", () => {
test("formats a Date object", () => {
const result = formatDate(new Date("2024-06-15T12:00:00Z"));
expect(result).toContain("2024");
expect(result).toContain("15");
});
test("formats date-only ISO string in UTC to avoid off-by-one", () => {
// "1990-05-15" is parsed as UTC midnight — formatting in UTC prevents
// users west of UTC from seeing May 14 instead of May 15
const result = formatDate("1990-05-15");
expect(result).toContain("15");
expect(result).toContain("1990");
});
test("respects custom options", () => {
const result = formatDate("2024-06-15", { month: "short" });
expect(result).toContain("Jun");
});
test("does not force UTC when a timeZone option is provided", () => {
const result = formatDate("2024-06-15", { timeZone: "America/New_York" });
expect(result).toBeDefined();
});
test("formats a full ISO datetime string (not date-only)", () => {
const result = formatDate("2024-06-15T18:30:00Z");
expect(result).toContain("2024");
});
});
describe("formatShortDate", () => {
test("formats with short month", () => {
// Use a local Date to avoid UTC-to-local day shift
const result = formatShortDate(new Date(2024, 0, 3));
expect(result).toContain("Jan");
expect(result).toContain("3");
expect(result).toContain("2024");
});
test("accepts a Date object", () => {
const result = formatShortDate(new Date(2024, 11, 25));
expect(result).toContain("Dec");
expect(result).toContain("25");
});
});
describe("formatRelativeTime", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-06-15T12:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
test("seconds ago", () => {
const result = formatRelativeTime("2024-06-15T11:59:30Z");
expect(result).toContain("30 seconds ago");
});
test("minutes ago", () => {
const result = formatRelativeTime("2024-06-15T11:55:00Z");
expect(result).toContain("5 minutes ago");
});
test("hours ago", () => {
const result = formatRelativeTime("2024-06-15T09:00:00Z");
expect(result).toContain("3 hours ago");
});
test("days ago", () => {
const result = formatRelativeTime("2024-06-10T12:00:00Z");
expect(result).toContain("5 days ago");
});
test("months ago", () => {
const result = formatRelativeTime("2024-03-15T12:00:00Z");
expect(result).toContain("3 months ago");
});
test("years ago", () => {
const result = formatRelativeTime("2022-06-15T12:00:00Z");
expect(result).toContain("2 years ago");
});
test("future seconds", () => {
const result = formatRelativeTime("2024-06-15T12:00:45Z");
expect(result).toContain("in 45 seconds");
});
test("future days", () => {
const result = formatRelativeTime("2024-06-20T12:00:00Z");
expect(result).toContain("in 5 days");
});
});
describe("formatNumber", () => {
test("formats a number", () => {
expect(formatNumber(1234)).toBe("1,234");
});
test("respects options", () => {
const result = formatNumber(0.5, { style: "percent" });
expect(result).toBe("50%");
});
});
describe("formatBytes", () => {
test("zero bytes", () => {
expect(formatBytes(0)).toBe("0 B");
});
test("bytes", () => {
expect(formatBytes(512)).toBe("512 B");
});
test("kilobytes", () => {
expect(formatBytes(1024)).toBe("1 KB");
});
test("megabytes", () => {
expect(formatBytes(1024 * 1024)).toBe("1 MB");
});
test("gigabytes", () => {
expect(formatBytes(1024 * 1024 * 1024)).toBe("1 GB");
});
test("fractional megabytes", () => {
expect(formatBytes(1536 * 1024)).toBe("1.5 MB");
});
});