import { describe, expect, it } from "vitest"; import type { AgileBoard, AgileResponse } from "../api/types.ts"; import { boardState, daysLeft, fmtDay, fmtRange, fmtSp, isEpic, jiraDate, tasksWord, typeKind } from "./model.ts"; const resp = (over: Partial): AgileResponse => ({ board: { id: 4216, type: "scrum", columns: [{ name: "Готово", statuses: ["Готово"] }], kanban_backlog: null, fetched_at: "2026-10-24T10:00:00+00:00", ...over }, sprints: [], backlog: [], board_issues: null, }); describe("даты и числа экранов доски", () => { it("время Jira с ±HHMM и ±HH:MM — один день", () => { // Review Focus 1 expect(fmtDay("2026-10-14T09:00:00.000+0300")).toBe(fmtDay("2026-10-14T09:00:00.000+03:00")); expect(fmtDay("2026-10-14T09:00:00.000+0300")).toMatch(/^\d{2}\.\d{2}$/); expect(fmtDay(null)).toBe("—"); expect(fmtDay("мусор")).toBe("—"); }); it("jiraDate и fmtRange", () => { expect(jiraDate("2026-10-14T09:00:00.000+0300")?.getTime()).toBe(Date.parse("2026-10-14T09:00:00.000+03:00")); expect(jiraDate("мусор")).toBeNull(); expect(fmtRange(null, null)).toBe("—–—"); expect(fmtRange("2026-10-14T09:00:00.000+0300", "2026-10-28T09:00:00.000+0300")).toMatch(/^\d{2}\.\d{2}–\d{2}\.\d{2}$/); }); it("daysLeft — календарные дни до конца", () => { const now = new Date(2026, 9, 24, 23, 30); expect(daysLeft(new Date(2026, 9, 27, 1, 0).toISOString(), now)).toBe(3); expect(daysLeft(new Date(2026, 9, 23, 12, 0).toISOString(), now)).toBe(-1); expect(daysLeft(null, now)).toBeNull(); }); it("fmtSp, tasksWord", () => { expect([fmtSp(null), fmtSp(3), fmtSp(2.5)]).toEqual(["—", "3", "2.5"]); expect([1, 2, 5, 11, 21, 73].map(tasksWord)).toEqual(["задача", "задачи", "задач", "задач", "задача", "задачи"]); }); it("fmtSp — до двух знаков без хвостов (M-6)", () => { expect([fmtSp(0.1 + 0.2), fmtSp(1 / 3), fmtSp(0)]).toEqual(["0.3", "0.33", "0"]); }); it("typeKind — имена типов Jira, без учёта регистра", () => { const kinds = (types: (string | null)[]) => types.map(typeKind); expect(kinds(["Эпик", "epic", "Фича"])).toEqual(["epic", "epic", "epic"]); expect(kinds(["История", "Story"])).toEqual(["story", "story"]); expect(kinds(["Задача", "Task"])).toEqual(["task", "task"]); expect(kinds(["Ошибка", "BUG"])).toEqual(["bug", "bug"]); expect(kinds(["Подзадача", "Sub-task", "Subtask"])).toEqual(["subtask", "subtask", "subtask"]); expect(kinds([null, "", "Риск"])).toEqual(["other", "other", "other"]); }); it("isEpic — по виду типа", () => { expect(isEpic({ issue_type: "Фича" })).toBe(true); expect(isEpic({ issue_type: null })).toBe(false); expect(isEpic({ issue_type: "История" })).toBe(false); }); it("boardState — три состояния (design #23)", () => { expect(boardState({ board: null, sprints: [], backlog: null, board_issues: null })).toBe("no-data"); expect(boardState(resp({ fetched_at: null, columns: null }))).toBe("no-data"); expect(boardState(resp({ fetched_at: "2026-10-24T10:00:00+00:00", columns: null }))).toBe("no-columns"); expect(boardState(resp({}))).toBe("ok"); }); });