sdlc-hub/web/src/modules/jira/board/BoardPage.test.tsx

253 lines
14 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 { fireEvent, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { StrictMode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { setToken } from "../../../auth/token.ts";
import { json, mockFetch } from "../../../test/fetchMock.ts";
import { renderWithProviders } from "../../../test/render.tsx";
import settings from "./__fixtures__/settings.json";
import { BoardPage } from "./BoardPage.tsx";
const fx = (name: string) => readFileSync(join(import.meta.dirname, "__fixtures__", name), "utf8");
const SNAP = JSON.parse(fx("hub-basic.board.json"));
function hub(over: Record<string, () => Response> = {}) {
return mockFetch((method, url) => {
const k = `${method} ${url.pathname}`;
if (over[k]) return over[k]();
if (k === "GET /settings") return json(200, settings);
if (k === "GET /board") return json(200, SNAP);
if (k === "GET /batches") return json(200, { batches: [] });
if (k === "GET /agents") return json(200, { agents: [] });
if (k === "POST /refresh") return json(201, { id: 7, estimate_seconds: 5, count: 1 });
if (k === "GET /batches/7") return json(200, { id: 7, author: "viewer", status: "running", remaining_seconds: 3, counts: {} });
return json(404, { error: "not_found" });
});
}
const page = (project = "AWPCA") => renderWithProviders(<BoardPage project={project} />, { route: "/board", path: "/board" });
// канбан-проект: спринтов нет ни у одной своей задачи (соседи — из другого проекта — могут иметь)
const withSprints = (keep: (key: string) => boolean) => ({ ...SNAP, issues: SNAP.issues.map((i: { key: string; sprint: string | null }) => ({ ...i, sprint: keep(i.key) ? i.sprint : null })) });
const KANBAN = withSprints(() => false);
const NEIGHBOR_ONLY = withSprints((k) => k === "AURORA-9007");
const NO_SPRINT_TEXT = "колонки «Спринт» нет — показаны все задачи";
describe("страница доски", () => {
beforeEach(() => setToken("tok"));
it("шапка: вторичная кнопка «Обновить из Jira», primary нет; пустого блока .board-src под шапкой нет", async () => {
hub();
const { container } = page();
await screen.findByText(/задач из/);
expect(screen.getByRole("button", { name: "Обновить из Jira" })).toHaveClass("ui-btn--secondary");
expect(container.querySelector(".ui-btn--primary")).toBeNull();
expect(container.querySelector(".board-src")).toBeNull();
});
it("строит доску из хаба: источник, статистика, выбор самого наполненного спринта", async () => {
hub();
page();
expect(await screen.findByText(/задач из/)).toBeInTheDocument();
expect(screen.getByText(/задач из 9/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /AWPCA_Спринт 2/ })).toBeInTheDocument();
});
it("снимок запрашивается по проекту страницы", async () => {
const fetch = hub();
page();
await screen.findByText(/задач из/);
const urls = fetch.mock.calls.map(([u]) => new URL(String(u), "http://hub.test")).filter((u) => u.pathname === "/board");
expect(urls[0].searchParams.get("project")).toBe("AWPCA");
});
it("пустые словари в настройках — сообщение, а не пустая доска", async () => {
hub({ "GET /settings": () => json(200, { kind_labels: {}, link_types: {} }) });
page();
expect(await screen.findByText(/В настройках хаба нет kind_labels и link_types/)).toBeInTheDocument();
expect(document.querySelector(".board")).toBeNull();
});
it("переключатели и легенда сохраняются под прежними ключами", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
const nb = screen.getByRole("button", { name: "Соседи по связям" });
await userEvent.click(nb);
expect(nb).toHaveAttribute("aria-pressed", "true");
expect(localStorage.getItem("graph-neighbors")).toBe("1");
await userEvent.click(within(document.querySelector(".legend") as HTMLElement).getByRole("button", { name: /разработка/ }));
expect(localStorage.getItem("graph-hidekind")).toBe('["dev"]');
});
it("«Проблемы» открывают ящик, счётчик — сроки, исполнители, статусы", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
await userEvent.click(screen.getByRole("button", { name: /Проблемы/ }));
expect(screen.getByText("Проблемы на доске")).toBeInTheDocument();
expect(localStorage.getItem("graph-prob")).toBe("1");
expect(screen.getByRole("button", { name: /^Проблемы/ })).toHaveAttribute("aria-pressed", "true");
});
it("кнопка «Проблемы» несёт счётчик и aria-pressed", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
const b = screen.getByRole("button", { name: /^Проблемы/ });
expect(b).toHaveAttribute("aria-pressed", "false");
expect(within(b).getByText(/^\d+$/)).toBeInTheDocument();
});
it("экспорт JSON показывает выгрузку arm-ka-links/1 с раскладкой", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
await userEvent.click(screen.getByRole("button", { name: "Экспорт JSON" }));
const text = (screen.getByRole("textbox") as HTMLTextAreaElement).value;
expect(JSON.parse(text)).toMatchObject({ format: "arm-ka-links/1", source: "Хаб" });
expect(JSON.parse(text).nodes.find((n: { key: string }) => n.key === "AURORA-9001").layout).not.toBeNull();
});
it("сброс раскладки — с подтверждением в меню", async () => {
hub();
localStorage.setItem("graph-pos:board", '{"AURORA-9001":{"x":1,"ry":40}}');
page();
await screen.findByText(/задач из 9/);
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
await userEvent.click(screen.getByRole("button", { name: "Сбросить раскладку" }));
await userEvent.click(screen.getByRole("button", { name: "Да, сбросить" }));
expect(localStorage.getItem("graph-pos:board")).toBeNull();
});
it("загрузка CSV — источник «из файла»", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
fireEvent.change(input, { target: { files: [new File([fx("jira-export.csv")], "jira-export.csv", { type: "text/csv" })] } });
expect(await screen.findByText("из файла: jira-export.csv")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Вернуться к данным хаба" }));
expect(await screen.findByText(/задач из/)).toBeInTheDocument();
expect(screen.queryByText(/^из файла:/)).not.toBeInTheDocument();
});
it("«Загрузить файл…» в меню «Ещё» открывает выбор файла", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
const click = vi.spyOn(input, "click");
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
await userEvent.click(screen.getByRole("button", { name: "Загрузить файл…" }));
expect(click).toHaveBeenCalled();
});
it("свёрнутая легенда: скрыт «тестирование», «Легенда» свёрнута, после перемонтирования свёрнута, «скрыто: 1»", async () => {
hub();
const r = page();
await screen.findByText(/задач из 9/);
const legend = () => document.querySelector(".legend");
await userEvent.click(within(legend() as HTMLElement).getByRole("button", { name: /тестирование/ }));
expect(screen.getByText("скрыто: 1")).toBeInTheDocument();
const toggle = screen.getByRole("button", { name: /^Легенда/ });
expect(toggle).toHaveAttribute("aria-expanded", "true");
await userEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(legend()).toBeNull();
expect(localStorage.getItem("sdlc-hub-legend")).toBe("0");
r.unmount();
page();
await screen.findByText(/задач из 9/);
expect(screen.getByRole("button", { name: /^Легенда/ })).toHaveAttribute("aria-expanded", "false");
expect(legend()).toBeNull();
expect(screen.getByText("скрыто: 1")).toBeInTheDocument();
});
it("кнопка «Обновить из Jira» в шапке есть, хода пакета и «Тема» на доске нет", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
expect(screen.getByRole("button", { name: "Обновить из Jira" })).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Ещё" }));
expect(screen.getByRole("button", { name: "Ещё" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("button", { name: "Перечитать" })).toBeInTheDocument(); // меню действительно открыто
expect(screen.queryByRole("button", { name: /^Тема/ })).not.toBeInTheDocument();
});
it("заголовок страницы — h1, в шапке label; строка состояния — только события доски", async () => {
hub();
page();
await screen.findByText(/задач из 9/);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
expect(screen.queryByText(/^Хаб · данные на/)).not.toBeInTheDocument(); // данные хаба пометки не имеют (спека, решение 13)
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
it("пустой link_types при заполненном kind_labels — называет только недостающий словарь", async () => {
hub({ "GET /settings": () => json(200, { kind_labels: { dev: "разработка" }, link_types: {} }) });
page();
expect(await screen.findByText(/В настройках хаба нет link_types —/)).toBeInTheDocument();
});
it("T12.3: --head-h снимается при уходе со страницы", async () => {
hub();
const r = page();
await screen.findByText(/задач из/);
r.unmount();
expect(document.documentElement.style.getPropertyValue("--head-h")).toBe("");
});
it("T12.2: переключатель пишет флаг в хранилище один раз, в том числе в StrictMode", async () => {
hub();
renderWithProviders(<StrictMode><BoardPage project="AWPCA" /></StrictMode>, { route: "/board", path: "/board" });
await screen.findByText(/задач из/);
const set = vi.spyOn(Storage.prototype, "setItem");
fireEvent.click(screen.getByRole("button", { name: "Критический путь" }));
expect(set.mock.calls.filter(([k]) => k === "graph-crit")).toEqual([["graph-crit", "1"]]);
});
it("T12.4: список исполнителей — группа флажков с подписью", async () => {
hub();
page();
await userEvent.click(await screen.findByRole("button", { name: /Все исполнители/ }));
expect(screen.getByRole("group", { name: "Все исполнители" })).toBeInTheDocument();
expect(screen.queryByRole("listbox")).toBeNull();
});
it("T9: старая раскладка переносится только в проект AWPCA", async () => {
hub();
localStorage.setItem("graph-pos:old.json", '{"AURORA-9001":{"x":1,"ry":40}}');
const r = page("STOCKS_DRRK");
await screen.findByText(/задач из/);
expect(localStorage.getItem("graph-pos:board@STOCKS_DRRK")).toBeNull();
expect(localStorage.getItem("graph-pos:board")).toBeNull();
r.unmount();
page("AWPCA");
await screen.findByText(/задач из/);
expect(localStorage.getItem("graph-pos:board")).toBe('{"AURORA-9001":{"x":1,"ry":40}}');
});
it("T9: канбан-проект — без спринтов в снимке: надпись вместо фильтра спринтов", async () => {
hub({ "GET /board": () => json(200, KANBAN) });
page("STOCKS_DRRK");
expect(await screen.findByText(NO_SPRINT_TEXT)).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /AWPCA_Спринт/ })).toBeNull();
});
it("T9: канбан-проект, спринт только у соседа — надпись, карточка соседа на доске", async () => {
hub({ "GET /board": () => json(200, NEIGHBOR_ONLY) });
page("STOCKS_DRRK");
expect(await screen.findByText(NO_SPRINT_TEXT)).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /AWPCA_Спринт/ })).toBeNull();
expect(screen.getAllByText("AURORA-9007").length).toBeGreaterThan(0);
});
it("T9: проект AWPCA с фикстурой hub-basic — выбор спринта есть", async () => {
hub();
page("AWPCA");
expect(await screen.findByRole("button", { name: /AWPCA_Спринт 2/ })).toBeInTheDocument();
expect(screen.queryByText(NO_SPRINT_TEXT)).toBeNull();
});
});