sdlc-hub/web/src/modules/jira/agile/KanbanPage.test.tsx
Andrey Epifancev 29b3f95404 fix(web): «Сбросить фильтры» всегда на месте, без «Скрыть выполненные» на «Спринте» (#118)
Кнопка сброса больше не появляется и не исчезает — неактивна, пока фильтров нет,
панель не сдвигается. На «Спринте» выполненное скрывают колонкой «Выполнено»:
флажка нет, сохранённое hideDone не применяется (как на «Kanban»).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:17:32 +04:00

159 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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> = {}): 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(<App router={router} />);
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(<App router={createMemoryRouter(routes, { initialEntries: ["/app/jira/kanban?project=KAN"], basename: "/app" })} />);
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");
});
});