157 lines
9.2 KiB
TypeScript
157 lines
9.2 KiB
TypeScript
import { readFileSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||
import type { BoardSnapshot } from "../api/types.ts";
|
||
import { canon } from "../test/canon.ts";
|
||
import { computeVisible } from "./analysis/visible.ts";
|
||
import { dictsFromSettings } from "./dicts.ts";
|
||
import { exportFileName, exportJson } from "./exportJson.ts";
|
||
import settings from "./__fixtures__/settings.json";
|
||
import storageCases from "./__fixtures__/storage.cases.json";
|
||
import { computeLayout, measuredHeight } from "./layout/layout.ts";
|
||
import { fromHub } from "./model/fromHub.ts";
|
||
import { fromJson } from "./model/fromJson.ts";
|
||
import { applyJsonSettings, colWidths, foldedSet, laneOrder, migrateMode, migrateStore, moveLane, readPrefs, resetLayout, saveLayout, savedLayout, setColWidth, setStoreProject, toggleLane, writeFlag } from "./storage.ts";
|
||
import type { BoardModel, JsonImport } from "./types.ts";
|
||
import { noFilters } from "./types.ts";
|
||
|
||
const fx = (name: string) => readFileSync(join(import.meta.dirname, "__fixtures__", name), "utf8");
|
||
const { dicts } = dictsFromSettings(settings);
|
||
const NOW = "2026-10-14T12:00:00.000Z";
|
||
const dump = () => Object.fromEntries(Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)]));
|
||
|
||
describe("хранилище = links.html@3931a88", () => {
|
||
beforeEach(() => localStorage.clear());
|
||
afterEach(() => vi.restoreAllMocks());
|
||
|
||
const expected = JSON.parse(fx("storage.expected.json"));
|
||
for (const c of storageCases as unknown as { name: string; op: string; before: Record<string, string> }[]) {
|
||
it(`миграция: ${c.name}`, () => {
|
||
Object.entries(c.before).forEach(([k, v]) => localStorage.setItem(k, v));
|
||
if (c.op === "startup") migrateMode();
|
||
migrateStore();
|
||
expect(dump()).toEqual(expected[c.name]);
|
||
});
|
||
}
|
||
|
||
it("импорт JSON старой страницы: хранилище и фильтры", () => {
|
||
const g = JSON.parse(fx("old-board.expected.json"));
|
||
const patch = applyJsonSettings(fromJson(fx("old-board.json"), "old-board.json", dicts));
|
||
expect(dump()).toEqual(g.storage);
|
||
const f = { ...noFilters(), ...patch };
|
||
expect(canon({ neighbors: f.neighbors, onlyLinked: f.onlyLinked, onlyHand: f.onlyHand, crit: f.crit,
|
||
byDate: f.byDate, hideKind: f.hideKind, hideEdge: f.hideEdge })).toEqual(g.S);
|
||
});
|
||
|
||
it("экспорт JSON после раскладки по умолчанию", () => {
|
||
const g = JSON.parse(fx("hub-basic.expected.json"));
|
||
const model = fromHub(JSON.parse(fx("hub-basic.board.json")) as BoardSnapshot, dicts);
|
||
const vis = computeVisible(model, noFilters(), [], "2026-10-14");
|
||
const lay = computeLayout(vis, { byDate: false, colWidths: {}, saved: null, today: "2026-10-14",
|
||
measure: () => ({ h: measuredHeight(117), other: 79 }) });
|
||
saveLayout(lay.rel, lay.colBounds, false);
|
||
expect(canon(exportJson(model, vis, noFilters(), NOW))).toEqual(g.export);
|
||
});
|
||
|
||
it("флаги и ширины колонок читаются обратно", () => {
|
||
writeFlag("neighbors", true);
|
||
writeFlag("snap", false);
|
||
expect(readPrefs().filters.neighbors).toBe(true);
|
||
expect(readPrefs().snap).toBe(false);
|
||
setColWidth("2026-10-20", 400);
|
||
expect(JSON.parse(localStorage.getItem("graph-colw:board")!)).toEqual({ "2026-10-20": 400 });
|
||
setColWidth("2026-10-20", null);
|
||
expect(JSON.parse(localStorage.getItem("graph-colw:board")!)).toEqual({});
|
||
});
|
||
|
||
it("закрытое хранилище: чтение даёт умолчания, запись молча пропускается", () => {
|
||
vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new DOMException("closed", "SecurityError"); });
|
||
vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { throw new DOMException("closed", "SecurityError"); });
|
||
vi.spyOn(Storage.prototype, "key").mockImplementation(() => { throw new DOMException("closed", "SecurityError"); });
|
||
expect(() => { migrateMode(); migrateStore(); writeFlag("crit", true); }).not.toThrow();
|
||
expect(readPrefs().snap).toBe(true);
|
||
});
|
||
|
||
it("раскладка неверной формы отбрасывается при чтении", () => {
|
||
localStorage.setItem("graph-pos:board", JSON.stringify({ "AURORA-1": null, "AURORA-2": { x: "1" }, "AURORA-3": { x: 1, ry: 2, w: 240, h: 90 } }));
|
||
expect(savedLayout(false)).toEqual({ "AURORA-3": { x: 1, ry: 2, w: 240, h: 90 } });
|
||
localStorage.setItem("graph-pos:board", JSON.stringify({ "AURORA-9003": { x: 30, ry: 50 } }));
|
||
expect(savedLayout(false)).toEqual({ "AURORA-9003": { x: 30, ry: 50 } }); // M-1: без w запись принимается (w и h проверяет computeLayout)
|
||
localStorage.setItem("graph-pos:board", "[1]");
|
||
expect(savedLayout(false)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("операции хранилища", () => {
|
||
beforeEach(() => localStorage.clear());
|
||
it("colWidths пропускает только конечные числа (T9.1)", () => {
|
||
localStorage.setItem("graph-colw:board", JSON.stringify({ "2026-10-20": 400, "2026-10-21": "x", "2026-10-22": null }));
|
||
expect(colWidths()).toEqual({ "2026-10-20": 400 });
|
||
});
|
||
it("toggleLane сворачивает и разворачивает", () => {
|
||
toggleLane("Гудвилл");
|
||
expect(foldedSet()).toEqual(new Set(["Гудвилл"]));
|
||
toggleLane("Гудвилл");
|
||
expect(foldedSet()).toEqual(new Set());
|
||
});
|
||
it("moveLane переставляет и сохраняет имена вне текущей выборки", () => {
|
||
localStorage.setItem("graph-lanes:board", JSON.stringify(["A", "B", "C", "Скрытая"]));
|
||
expect(moveLane(["A", "B", "C"], "A", 2)).toBe(true);
|
||
expect(laneOrder()).toEqual(["B", "C", "A", "Скрытая"]);
|
||
expect(moveLane(["A", "B", "C"], "A", 5)).toBe(false);
|
||
expect(moveLane(["A", "B", "C"], "нет", 0)).toBe(false);
|
||
});
|
||
it("resetLayout: обычный режим не трогает «по датам», режим дат чистит и ширины колонок", () => {
|
||
for (const k of ["pos", "posdate", "lanes", "fold", "colw"]) localStorage.setItem(`graph-${k}:board`, "{}");
|
||
resetLayout(false);
|
||
expect(Object.keys(localStorage).sort()).toEqual(["graph-colw:board", "graph-posdate:board"]);
|
||
resetLayout(true);
|
||
expect(Object.keys(localStorage)).toEqual([]);
|
||
});
|
||
it("exportFileName: имя файла без .csv и недопустимых символов, дата из now", () => {
|
||
expect(exportFileName({ fileName: "Jira (1).csv" } as BoardModel, "2026-10-14T12:00:00Z")).toBe("links-Jira_1_-2026-10-14.json");
|
||
expect(exportFileName({ fileName: "" } as BoardModel, "2026-10-14T12:00:00Z")).toBe("links-board-2026-10-14.json");
|
||
});
|
||
it("applyJsonSettings: mode=all — пустой выбор спринтов", () => {
|
||
applyJsonSettings({ layout: {}, layoutByDate: {}, laneOrder: [], collapsedLanes: null, dateColumns: null,
|
||
filters: { mode: "all", sprints: ["S 2"] } } as unknown as JsonImport);
|
||
expect(localStorage.getItem("graph-sprint:board")).toBe("[]");
|
||
});
|
||
it("закрытое хранилище: чтение — значения по умолчанию, запись и удаление без исключений", () => {
|
||
const boom = () => { throw new DOMException("closed", "SecurityError"); };
|
||
vi.spyOn(Storage.prototype, "getItem").mockImplementation(boom);
|
||
vi.spyOn(Storage.prototype, "setItem").mockImplementation(boom);
|
||
vi.spyOn(Storage.prototype, "removeItem").mockImplementation(boom);
|
||
expect(laneOrder()).toEqual([]);
|
||
expect(() => { toggleLane("A"); resetLayout(true); setColWidth("2026-10-20", 400); }).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe("ключи по проектам", () => {
|
||
beforeEach(() => localStorage.clear());
|
||
afterEach(() => setStoreProject(null));
|
||
|
||
it("AWPCA — прежние ключи", () => {
|
||
setStoreProject("AWPCA");
|
||
writeFlag("crit", true); toggleLane("Эпик 1");
|
||
expect(localStorage.getItem("graph-crit")).toBe("1");
|
||
expect(localStorage.getItem("graph-fold:board")).toBe('["Эпик 1"]');
|
||
});
|
||
it("другой проект — ключи с суффиксом, AWPCA не тронут", () => {
|
||
localStorage.setItem("graph-pos:board", '{"AURORA-1":{"x":1,"ry":2}}');
|
||
setStoreProject("STOCKS_DRRK");
|
||
writeFlag("crit", true); toggleLane("Эпик 1"); setColWidth("2026-10-20", 400);
|
||
saveLayout({ "AURORA-2": { x: 10, ry: 20, w: 200, h: 80, auto: false } }, {}, false);
|
||
applyJsonSettings({ layout: { "AURORA-3": { x: 1, ry: 1 } }, layoutByDate: {}, laneOrder: ["A"], filters: { neighbors: true } } as unknown as JsonImport);
|
||
expect(localStorage.getItem("graph-crit@STOCKS_DRRK")).toBe("1");
|
||
expect(localStorage.getItem("graph-fold:board@STOCKS_DRRK")).toBe('["Эпик 1"]');
|
||
expect(localStorage.getItem("graph-lanes:board@STOCKS_DRRK")).toBe('["A"]');
|
||
expect(localStorage.getItem("graph-neighbors@STOCKS_DRRK")).toBe("1");
|
||
expect(localStorage.getItem("graph-pos:board")).toBe('{"AURORA-1":{"x":1,"ry":2}}');
|
||
expect(localStorage.getItem("graph-crit")).toBeNull();
|
||
expect(laneOrder()).toEqual(["A"]);
|
||
setStoreProject("AWPCA");
|
||
expect(laneOrder()).toEqual([]);
|
||
});
|
||
});
|