import { act, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { beforeEach, describe, expect, it } from "vitest"; import { setToken } from "../auth/token.ts"; import settings from "../board/__fixtures__/settings.json"; import { json, mockFetch } from "../test/fetchMock.ts"; import { renderWithProviders } from "../test/render.tsx"; import { IssuesPage } from "./IssuesPage.tsx"; const SNAP = JSON.parse(readFileSync(join(import.meta.dirname, "..", "board", "__fixtures__", "hub-basic.board.json"), "utf8")); const row = (n: number) => ({ ...SNAP.issues[1], key: `AURORA-${9000 + n}`, summary: `Задача ${n}`, fetched_at: "" }); function hub(pages: (params: URLSearchParams) => unknown[]) { return mockFetch((_m, url) => { if (url.pathname === "/settings") return json(200, settings); if (url.pathname === "/board") return json(200, SNAP); if (url.pathname === "/issues") return json(200, { issues: pages(url.searchParams) }); return json(404, {}); }); } const issuesCalls = (fetch: ReturnType) => fetch.mock.calls.map(([u]) => new URL(String(u), "http://hub.test")).filter((u) => u.pathname === "/issues"); describe("список задач", () => { beforeEach(() => setToken("tok")); it("фильтры из адреса уходят в GET /issues; ключ ведёт на карточку", async () => { const fetch = hub(() => [row(1)]); renderWithProviders(, { route: "/issues?sprint=AWPCA_Спринт 2&neighbor=1", path: "/issues" }); expect(await screen.findByRole("link", { name: "AURORA-9001" })).toHaveAttribute("href", "/issues/AURORA-9001"); const q = issuesCalls(fetch)[0].searchParams; expect(q.get("sprint")).toBe("AWPCA_Спринт 2"); expect(q.get("neighbor")).toBe("1"); expect(q.get("limit")).toBe("50"); expect(q.get("offset")).toBe("0"); }); it("выбор исполнителя пишет его в адрес и в запрос", async () => { const fetch = hub(() => [row(1)]); const { router } = renderWithProviders(, { route: "/issues", path: "/issues" }); await screen.findByRole("link", { name: "AURORA-9001" }); await userEvent.selectOptions(screen.getByLabelText("Исполнитель"), "Петров Пётр Петрович"); await waitFor(() => expect(router.state.location.search).toContain("assignee=")); await waitFor(() => expect(issuesCalls(fetch).at(-1)!.searchParams.get("assignee")).toBe("Петров Пётр Петрович")); }); it("«Показать ещё» до неполной порции", async () => { const fetch = hub((p) => (p.get("offset") === "0" ? Array.from({ length: 50 }, (_, i) => row(i + 1)) : Array.from({ length: 17 }, (_, i) => row(i + 51)))); renderWithProviders(, { route: "/issues", path: "/issues" }); await userEvent.click(await screen.findByRole("button", { name: "Показать ещё" })); await waitFor(() => expect(screen.getAllByRole("row")).toHaveLength(1 + 67)); expect(screen.queryByRole("button", { name: "Показать ещё" })).toBeNull(); expect(issuesCalls(fetch).at(-1)!.searchParams.get("offset")).toBe("50"); }); it("поиск — один запрос после паузы ввода", async () => { const fetch = hub(() => [row(1)]); renderWithProviders(, { route: "/issues", path: "/issues" }); await screen.findByRole("link", { name: "AURORA-9001" }); const before = issuesCalls(fetch).length; await userEvent.type(screen.getByLabelText("Поиск по ключу или теме"), "свод"); await waitFor(() => expect(issuesCalls(fetch).at(-1)!.searchParams.get("q")).toBe("свод")); expect(issuesCalls(fetch).length).toBe(before + 1); }); it("хаб не отвечает — сообщение и «Повторить»", async () => { let down = true; mockFetch((_m, url) => { if (url.pathname === "/issues" && down) throw new TypeError("Failed to fetch"); if (url.pathname === "/settings") return json(200, settings); if (url.pathname === "/board") return json(200, SNAP); return json(200, { issues: [row(1)] }); }); renderWithProviders(, { route: "/issues", path: "/issues" }); expect(await screen.findByRole("alert")).toHaveTextContent("хаб не отвечает"); down = false; await userEvent.click(screen.getByRole("button", { name: "Повторить" })); expect(await screen.findByRole("link", { name: "AURORA-9001" })).toBeInTheDocument(); }); it("эпик показывается названием из снимка", async () => { hub(() => [row(1)]); renderWithProviders(, { route: "/issues", path: "/issues" }); expect(await screen.findByRole("cell", { name: "Тестовый эпик" })).toBeInTheDocument(); }); it("T13.2: внешняя смена адреса обновляет поле поиска и не перезаписывается через 300 мс", async () => { hub(() => [row(1)]); const { router } = renderWithProviders(, { route: "/issues?q=свод", path: "/issues" }); const box = await screen.findByLabelText("Поиск по ключу или теме"); expect(box).toHaveValue("свод"); await act(async () => { await router.navigate("/issues"); }); await waitFor(() => expect(box).toHaveValue("")); await new Promise((r) => setTimeout(r, 450)); expect(router.state.location.search).toBe(""); expect(box).toHaveValue(""); }); it("T13.2: ввод поиска пишется в адрес через replace (шаг «назад» не застревает)", async () => { hub(() => [row(1)]); const { router } = renderWithProviders(, { route: "/issues", path: "/issues" }); await screen.findByRole("link", { name: "AURORA-9001" }); const before = router.state.historyAction; await userEvent.type(screen.getByLabelText("Поиск по ключу или теме"), "свод"); await waitFor(() => expect(router.state.location.search).toBe("?q=%D1%81%D0%B2%D0%BE%D0%B4")); expect(before).toBe("POP"); expect(router.state.historyAction).toBe("REPLACE"); }); });