Кнопка сброса больше не появляется и не исчезает — неактивна, пока фильтров нет, панель не сдвигается. На «Спринте» выполненное скрывают колонкой «Выполнено»: флажка нет, сохранённое hideDone не применяется (как на «Kanban»). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
9.3 KiB
TypeScript
159 lines
9.3 KiB
TypeScript
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");
|
||
});
|
||
});
|