Files
kandev/apps/web/lib/task-colors.test.ts
T
2026-08-08 17:07:07 +02:00

104 lines
3.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getTaskColor,
setTaskColor,
TASK_COLORS,
TASK_COLOR_BAR_CLASS,
TASK_COLOR_LABEL_KEYS,
TASK_COLORS_CHANGED_EVENT,
TASK_COLORS_STORAGE_KEY,
} from "./task-colors";
import { i18n, t } from "@/lib/i18n";
describe("task colors storage", () => {
beforeEach(() => {
window.localStorage.clear();
// Synthesize a cross-tab storage event so the module-level cache invalidates,
// matching how a real browser would notify other tabs after localStorage.clear().
window.dispatchEvent(new StorageEvent("storage", { key: null }));
});
it("returns null when no color is stored", () => {
expect(getTaskColor("task-1")).toBeNull();
});
it("stores and reads a color", () => {
setTaskColor("task-1", "blue");
expect(getTaskColor("task-1")).toBe("blue");
});
it("removes a color when set to null", () => {
setTaskColor("task-1", "blue");
setTaskColor("task-1", null);
expect(getTaskColor("task-1")).toBeNull();
});
it("ignores invalid colors loaded from storage", () => {
window.localStorage.setItem(
TASK_COLORS_STORAGE_KEY,
JSON.stringify({ "task-1": "fuchsia", "task-2": "red" }),
);
expect(getTaskColor("task-1")).toBeNull();
expect(getTaskColor("task-2")).toBe("red");
});
it("returns null on malformed storage", () => {
window.localStorage.setItem(TASK_COLORS_STORAGE_KEY, "{not json");
expect(getTaskColor("task-1")).toBeNull();
});
it("dispatches a change event when a color is set", () => {
const listener = vi.fn();
window.addEventListener(TASK_COLORS_CHANGED_EVENT, listener);
setTaskColor("task-1", "green");
expect(listener).toHaveBeenCalledTimes(1);
window.removeEventListener(TASK_COLORS_CHANGED_EVENT, listener);
});
it("does not dispatch when setting the same color twice", () => {
setTaskColor("task-1", "green");
const listener = vi.fn();
window.addEventListener(TASK_COLORS_CHANGED_EVENT, listener);
setTaskColor("task-1", "green");
expect(listener).not.toHaveBeenCalled();
window.removeEventListener(TASK_COLORS_CHANGED_EVENT, listener);
});
it("does not dispatch when clearing a non-existent color", () => {
const listener = vi.fn();
window.addEventListener(TASK_COLORS_CHANGED_EVENT, listener);
setTaskColor("task-1", null);
expect(listener).not.toHaveBeenCalled();
window.removeEventListener(TASK_COLORS_CHANGED_EVENT, listener);
});
});
describe("TASK_COLOR_LABEL_KEYS", () => {
afterEach(async () => {
await i18n.changeLanguage("en");
});
it("covers every colour, keyed by the persisted wire value", () => {
expect(Object.keys(TASK_COLOR_LABEL_KEYS).sort()).toEqual([...TASK_COLORS].sort());
// The same wire values index the class table, so they are identity.
expect(Object.keys(TASK_COLOR_BAR_CLASS).sort()).toEqual([...TASK_COLORS].sort());
});
it("holds catalog keys that resolve to English copy", () => {
expect(TASK_COLOR_LABEL_KEYS.red).toBe("task:colorRed");
const labels = TASK_COLORS.map((c) => t(TASK_COLOR_LABEL_KEYS[c]));
expect(labels).toEqual(["Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Pink"]);
});
it("stores keys rather than resolved copy, so a locale switch takes effect", async () => {
await i18n.changeLanguage("pseudo");
for (const color of TASK_COLORS) {
const label = t(TASK_COLOR_LABEL_KEYS[color]);
// A missing key would resolve to the key itself; frozen copy would stay
// English. Both are ruled out by an accented, non-key label.
expect(label).not.toBe(TASK_COLOR_LABEL_KEYS[color]);
expect(label).toMatch(/[^\p{ASCII}]/u);
}
});
});