import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createMemoryRouter } from "react-router"; import { beforeEach, describe, expect, it } from "vitest"; import { App } from "../../../App.tsx"; import { setToken } from "../../../auth/token.ts"; import settings from "../board/__fixtures__/settings.json"; import { routes } from "../../../routes.tsx"; import { json, mockFetch } from "../../../test/fetchMock.ts"; import type { AgileIssue, AgileResponse } from "../api/types.ts"; const K = { label: "KAN", name: "Канбан", board_id: 5120, board_type: "kanban", created_at: "", issues_count: 4, jira_synced_at: null }; const S = { label: "AWPCA", name: "АРМ КА", board_id: 4216, board_type: "scrum", created_at: "", issues_count: 9, jira_synced_at: null }; const iss = (key: string, over: Partial = {}): AgileIssue => ({ key, summary: `Тема ${key}`, issue_type: "Задача", status: "Новая", priority: null, assignee: null, epic_key: null, story_points: null, rank: "0|a", done: false, ...over, }); const cols = [ { name: "Бэклог", statuses: ["Новая"] }, { name: "В работе", statuses: ["В работе"] }, { name: "Готово", statuses: ["Готово"] }, ]; const agile = (backlog: boolean | null): AgileResponse => ({ board: { id: 5120, type: "kanban", columns: cols, kanban_backlog: backlog, fetched_at: "2026-10-24T10:00:00+00:00" }, sprints: [], backlog: backlog ? [iss("AURORA-1"), iss("AURORA-2")] : null, // как отвечает сервер: при kanban_backlog = true задачи первой колонки есть только в backlog, не в board_issues board_issues: [...(backlog ? [] : [iss("AURORA-1"), iss("AURORA-2")]), iss("AURORA-3", { status: "В работе" }), iss("AURORA-4", { status: "Готово", done: true }), iss("AURORA-5", { status: "Странный" })], }); function hub(data: AgileResponse, projects = [K]) { const urls: string[] = []; mockFetch((method, url) => { const k = `${method} ${url.pathname}`; urls.push(k); if (k === "GET /agile") return json(200, data); if (k === "GET /projects") return json(200, { projects }); if (k === "GET /settings") return json(200, settings); if (k === "GET /issues") return json(200, { issues: [] }); if (k === "GET /batches") return json(200, { batches: [] }); if (k === "GET /agents") return json(200, { agents: [] }); return json(404, { error: "not_found" }); }); return urls; } const open = (path: string) => { const router = createMemoryRouter(routes, { initialEntries: [path], basename: "/app" }); render(); return router; }; const page = () => screen.findByTestId("page-kanban"); const colNames = (p: HTMLElement) => Array.from(p.querySelectorAll(".agile-col__name")).map((e) => e.textContent); describe("«Kanban»", () => { beforeEach(() => { setToken("tok"); localStorage.clear(); }); it("меню kanban-проекта: «Связи», «Kanban», «Задачи», «Лента»", async () => { hub(agile(false)); open("/app/jira/links?project=KAN"); const g = () => screen.getByRole("group", { name: "Jira" }); await within(g()).findByRole("link", { name: "Kanban" }); expect(within(g()).getAllByRole("link").map((l) => l.textContent)).toEqual(["Связи", "Kanban", "Задачи", "Лента"]); }); it("колонки из board_issues, у последней — «за 14 дней», «Без колонки» при статусе вне доски", async () => { hub(agile(false)); open("/app/jira/kanban?project=KAN"); const p = await page(); await within(p).findByText("Тема AURORA-3"); expect(colNames(p)).toEqual(["Бэклог", "В работе", "Готово", "Без колонки"]); const done = p.querySelector('[aria-label="Готово"]') as HTMLElement; expect(within(done).getByText("за 14 дней")).toBeInTheDocument(); expect(within(done).getByText("Тема AURORA-4")).toBeInTheDocument(); expect(p.querySelectorAll(".agile-col__caption")).toHaveLength(1); const rest = p.querySelector('[aria-label="Без колонки"]') as HTMLElement; expect(within(rest).getByText("Тема AURORA-5")).toBeInTheDocument(); }); it("бэклог включён: «Бэклог (2)» над колонками, первая колонка не рисуется", async () => { hub(agile(true)); open("/app/jira/kanban?project=KAN"); const p = await page(); const sum = await within(p).findByText("Бэклог (2)"); const det = sum.closest("details")!; expect(within(det).getByText("Тема AURORA-1")).toBeInTheDocument(); expect(within(det).getByText("Тема AURORA-2")).toBeInTheDocument(); expect(colNames(p)).toEqual(["В работе", "Готово", "Без колонки"]); expect(p.querySelector(".agile-columns")!.textContent).not.toContain("Тема AURORA-1"); expect(det.compareDocumentPosition(p.querySelector(".agile-columns")!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); it("бэклог выключен или неизвестен: раздела «Бэклог» нет, первая колонка на доске", async () => { for (const flag of [false, null]) { hub(agile(flag)); const { unmount } = render(); const p = await page(); await within(p).findByText("Тема AURORA-3"); expect(p.querySelector("details")).toBeNull(); expect(screen.queryByText(/Бэклог \(/)).toBeNull(); expect(colNames(p)[0]).toBe("Бэклог"); expect(within(p.querySelector('[aria-label="Бэклог"]') as HTMLElement).getByText("Тема AURORA-1")).toBeInTheDocument(); unmount(); } }); it("scrum-проект на /app/jira/kanban — «У проекта нет kanban-доски» со ссылкой «Связи»", async () => { const urls = hub(agile(false), [S]); open("/app/jira/kanban?project=AWPCA"); const p = within(await page()); expect(await p.findByText("У проекта нет kanban-доски")).toBeInTheDocument(); expect(p.getByRole("link", { name: "Связи" })).toHaveAttribute("href", "/app/jira/links?project=AWPCA"); expect(urls).not.toContain("GET /agile"); }); it("поиск и исполнитель фильтруют колонки и «Бэклог (N)»; флажка «Скрыть выполненные» нет, меню «Колонки» есть", async () => { const d = agile(true); d.backlog = [iss("AURORA-1", { assignee: "Иванов" }), iss("AURORA-2")]; d.board_issues = [iss("AURORA-3", { status: "В работе", assignee: "Иванов" }), iss("AURORA-4", { status: "Готово", done: true })]; hub(d); open("/app/jira/kanban?project=KAN"); const p = await page(); await within(p).findByText("Бэклог (2)"); expect(within(p).queryByRole("checkbox", { name: "Скрыть выполненные" })).toBeNull(); expect(within(p).getByRole("button", { name: /^Колонки/ })).toBeInTheDocument(); await userEvent.selectOptions(within(p).getByLabelText("Исполнитель"), "Иванов"); expect(within(p).getByText("Бэклог (1)")).toBeInTheDocument(); expect(within(p).queryByText("Тема AURORA-4")).toBeNull(); expect(within(p).getByText("Тема AURORA-3")).toBeInTheDocument(); await userEvent.selectOptions(within(p).getByLabelText("Исполнитель"), ""); await userEvent.type(within(p).getByRole("searchbox"), "AURORA-4"); expect(within(p).getByText("Бэклог (0)")).toBeInTheDocument(); expect(within(p).getByText("Тема AURORA-4")).toBeInTheDocument(); expect(within(p).queryByText("Тема AURORA-3")).toBeNull(); }); it("hideDone=true в хранилище на «Kanban» не применяется: выполненные остаются", async () => { localStorage.setItem("sdlc-hub-agile", JSON.stringify({ KAN: { kanban: { hideDone: true } } })); hub(agile(false)); open("/app/jira/kanban?project=KAN"); const p = await page(); expect(await within(p).findByText("Тема AURORA-4")).toBeInTheDocument(); expect(within(p).getByRole("button", { name: "Сбросить фильтры" })).toBeDisabled(); }); it("скрытая колонка: снять «Готово» — колонки нет; имя колонки, которой нет, в хранилище не ломает меню", async () => { localStorage.setItem("sdlc-hub-agile", JSON.stringify({ KAN: { kanban: { hiddenColumns: ["Старая"] } } })); hub(agile(false)); open("/app/jira/kanban?project=KAN"); const p = await page(); await within(p).findByText("Тема AURORA-3"); const btn = within(p).getByRole("button", { name: /^Колонки/ }); expect(btn).toHaveTextContent(/^Колонки$/); await userEvent.click(btn); await userEvent.click(within(screen.getByRole("group", { name: "Колонки" })).getByRole("checkbox", { name: "Готово" })); expect(colNames(p)).toEqual(["Бэклог", "В работе", "Без колонки"]); expect(btn).toHaveTextContent("Колонки1"); }); });