aa6023e421
* feat(web): per-task color indicator in sidebar (localStorage) Adds a color marker to tasks in the left task list for better visual identification. Right-click a task → Color submenu picks one of seven colors (or None to clear). Color renders as a 3px bar on the left edge of the row and persists in localStorage. * fix(web): address review feedback on task color indicator - Filter storage events to TASK_COLORS_STORAGE_KEY so cross-tab writes to unrelated keys no longer wake every TaskItem subscriber. - Cache the parsed color map at module level, invalidated on writes and cross-tab storage events. Avoids re-parsing localStorage on every getSnapshot call (N tasks × N renders). - Drop redundant typeof string check on Object.entries keys. - Dim the colored bar to opacity-60 when not selected so colored rows remain distinguishable from colored-and-selected rows at the left edge. * refactor(web): remove test-only export from task-colors Drop the __resetTaskColorsCacheForTests escape hatch from the production module. Tests now invalidate the cache via a synthesized cross-tab `storage` event, exercising the same listener path the production code uses for real cross-tab updates. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
getTaskColor,
|
|
setTaskColor,
|
|
TASK_COLORS_CHANGED_EVENT,
|
|
TASK_COLORS_STORAGE_KEY,
|
|
} from "./task-colors";
|
|
|
|
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);
|
|
});
|
|
});
|