172 lines
12 KiB
TypeScript
172 lines
12 KiB
TypeScript
import { act, fireEvent, render, screen, waitFor, 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 board from "../board/__fixtures__/hub-basic.board.json";
|
||
import settings from "../board/__fixtures__/settings.json";
|
||
import { routes } from "../../../routes.tsx";
|
||
import { json, mockFetch } from "../../../test/fetchMock.ts";
|
||
import { issue } from "../../../test/issueFixtures.ts";
|
||
|
||
const rows = board.issues.map((i) => ({ ...i, id: i.key, description: null, reporter: null, created: null, jira_updated: null, fetched_at: "" }));
|
||
|
||
function hub() {
|
||
return mockFetch((_m, url) => {
|
||
const p = url.pathname;
|
||
if (p === "/projects") return json(200, { projects: [{ label: "AWPCA", name: "АРМ КА", board_id: null, board_type: null, created_at: "", issues_count: 9 }] });
|
||
if (p === "/settings") return json(200, settings);
|
||
if (p === "/board") return json(200, board);
|
||
if (p === "/issues") return json(200, { issues: rows });
|
||
if (p.startsWith("/issues/")) return json(200, issue(decodeURIComponent(p.slice("/issues/".length))));
|
||
if (p === "/batches") return json(200, { batches: [] });
|
||
return json(200, {});
|
||
});
|
||
}
|
||
const at = (entries: string[], index = entries.length - 1) =>
|
||
createMemoryRouter(routes, { initialEntries: entries, initialIndex: index, basename: "/app" });
|
||
const dialog = () => screen.getByRole("dialog");
|
||
|
||
describe("карточка окном", () => {
|
||
beforeEach(() => { setToken("tok"); hub(); });
|
||
|
||
it("из списка: окно поверх списка, фон не пересоздаётся, ✕ возвращает со старым фильтром, «назад» не открывает карточку", async () => {
|
||
sessionStorage.setItem("jira-hub-issue-filters", "1"); // панель «Фильтры» раскрыта
|
||
const router = at(["/app/board", "/app/issues?sprint=AWPCA_Спринт 2"]);
|
||
render(<App router={router} />);
|
||
const table = await screen.findByRole("table");
|
||
expect(screen.getByLabelText("Спринт")).toHaveValue("AWPCA_Спринт 2");
|
||
await userEvent.click(await within(table).findByRole("link", { name: "AURORA-9002" }));
|
||
expect(router.state.location.pathname).toBe("/app/issues/AURORA-9002");
|
||
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
expect(within(screen.getByRole("main")).getByRole("table")).toBe(table); // таблица команд карточки — в окне, не в main // фон не пересоздаётся
|
||
expect(screen.getByLabelText("Спринт")).toHaveValue("AWPCA_Спринт 2"); // фильтр фона — из его адреса
|
||
expect(screen.getByRole("link", { name: "Задачи" })).toHaveAttribute("aria-current", "page");
|
||
await userEvent.click(within(dialog()).getByRole("button", { name: "Закрыть" }));
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
expect(router.state.location.pathname).toBe("/app/issues");
|
||
expect(new URLSearchParams(router.state.location.search).get("sprint")).toBe("AWPCA_Спринт 2");
|
||
expect(within(screen.getByRole("main")).getByRole("table")).toBe(table); // таблица команд карточки — в окне, не в main
|
||
await act(async () => { await router.navigate(-1); });
|
||
expect(router.state.location.pathname).toBe("/app/board");
|
||
});
|
||
|
||
it("переход по связи меняет окно, «назад» возвращает прежнюю карточку, Esc закрывает всё", async () => {
|
||
const router = at(["/app/issues"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9001" }));
|
||
await userEvent.click(await within(dialog()).findByRole("link", { name: "AURORA-9002" }));
|
||
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
await act(async () => { await router.navigate(-1); });
|
||
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
await userEvent.click(within(dialog()).getByRole("link", { name: "AURORA-9002" }));
|
||
await within(dialog()).findByRole("heading", { name: /AURORA-9002/ });
|
||
fireEvent(dialog(), new Event("cancel", { cancelable: true }));
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
expect(router.state.location.pathname).toBe("/app/issues");
|
||
});
|
||
|
||
it("щелчок по фону окна закрывает его", async () => {
|
||
const router = at(["/app/issues"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9001" }));
|
||
await within(dialog()).findByRole("heading", { name: /AURORA-9001/ });
|
||
await userEvent.click(dialog()); // цель — сам <dialog>, не содержимое
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
expect(router.state.location.pathname).toBe("/app/issues");
|
||
});
|
||
|
||
it("I-2: выделение текста внутри окна, отпущенное над фоном, окно не закрывает", async () => {
|
||
const router = at(["/app/issues"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9001" }));
|
||
const title = await within(dialog()).findByRole("heading", { name: /AURORA-9001/ });
|
||
fireEvent.pointerDown(title); // нажали на тексте карточки…
|
||
fireEvent.pointerUp(dialog());
|
||
fireEvent.click(dialog()); // …отпустили над фоном: click — на <dialog>
|
||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||
expect(router.state.location.pathname).toBe("/app/issues/AURORA-9001");
|
||
});
|
||
|
||
it("с доски: окно поверх доски, раздел «Доска связей» активен, после закрытия доска та же", async () => {
|
||
const router = at(["/app/board"]);
|
||
render(<App router={router} />);
|
||
const ilink = await screen.findByRole("link", { name: "Открыть карточку AURORA-9001" });
|
||
const boardEl = screen.getByTestId("page-board");
|
||
await userEvent.click(ilink);
|
||
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
expect(screen.getByTestId("page-board")).toBe(boardEl);
|
||
expect(screen.getByRole("link", { name: "Доска связей" })).toHaveAttribute("aria-current", "page");
|
||
await userEvent.click(within(dialog()).getByRole("button", { name: "Закрыть" }));
|
||
await waitFor(() => expect(router.state.location.pathname).toBe("/app/board"));
|
||
expect(screen.getByTestId("page-board")).toBe(boardEl);
|
||
});
|
||
|
||
it("прямой адрес: под окном список задач, ✕ ведёт в список заменой записи", async () => {
|
||
const router = at(["/app/board", "/app/issues/AURORA-9001"]);
|
||
render(<App router={router} />);
|
||
expect(await within(await screen.findByRole("dialog")).findByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
expect(await screen.findByTestId("page-issues")).toBeInTheDocument();
|
||
await waitFor(() => expect(router.state.location.state).toMatchObject({ depth: 0, direct: true }));
|
||
await userEvent.click(within(dialog()).getByRole("button", { name: "Закрыть" }));
|
||
await waitFor(() => expect(router.state.location.pathname).toBe("/app/issues"));
|
||
await act(async () => { await router.navigate(-1); });
|
||
expect(router.state.location.pathname).toBe("/app/board"); // не карточка
|
||
});
|
||
|
||
it("прямой адрес и переход по связи: закрытие возвращает в список, а не на первую карточку", async () => {
|
||
const router = at(["/app/issues/AURORA-9001"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await within(await screen.findByRole("dialog")).findByRole("link", { name: "AURORA-9002" }));
|
||
await within(dialog()).findByRole("heading", { name: /AURORA-9002/ });
|
||
fireEvent(dialog(), new Event("cancel", { cancelable: true }));
|
||
await waitFor(() => expect(router.state.location.pathname).toBe("/app/issues"));
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
});
|
||
|
||
it("Esc с несохранённой заметкой спрашивает; «Остаться» оставляет окно с текстом", async () => {
|
||
const router = at(["/app/issues"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9001" }));
|
||
await userEvent.click(await within(dialog()).findByRole("button", { name: "Править" }));
|
||
const box = await within(dialog()).findByLabelText("Заметка владельца");
|
||
await userEvent.type(box, "черновик");
|
||
fireEvent(dialog(), new Event("cancel", { cancelable: true }));
|
||
expect(await within(dialog()).findByText("Есть несохранённые правки заметки. Уйти без сохранения?")).toBeInTheDocument();
|
||
await userEvent.click(within(dialog()).getByRole("button", { name: "Остаться" }));
|
||
expect(router.state.location.pathname).toBe("/app/issues/AURORA-9001");
|
||
expect(within(dialog()).getByLabelText("Заметка владельца")).toHaveValue("черновик");
|
||
});
|
||
|
||
it("T4.1: отказ от закрытия («Остаться») сбрасывает ожидание закрытия: «назад» на запись прямого адреса не уводит в список", async () => {
|
||
const router = at(["/app/issues/AURORA-9001"]);
|
||
render(<App router={router} />);
|
||
await userEvent.click(await within(await screen.findByRole("dialog")).findByRole("link", { name: "AURORA-9002" }));
|
||
await within(dialog()).findByRole("heading", { name: /AURORA-9002/ });
|
||
await userEvent.click(await within(dialog()).findByRole("button", { name: "Править" }));
|
||
await userEvent.type(await within(dialog()).findByLabelText("Заметка владельца"), "черновик");
|
||
fireEvent(dialog(), new Event("cancel", { cancelable: true }));
|
||
await userEvent.click(await within(dialog()).findByRole("button", { name: "Остаться" }));
|
||
await userEvent.click(within(dialog()).getByRole("button", { name: "Отмена" }));
|
||
await userEvent.click(await within(dialog()).findByRole("button", { name: "Отменить правки" }));
|
||
await act(async () => { await router.navigate(-1); });
|
||
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
expect(router.state.location.pathname).toBe("/app/issues/AURORA-9001");
|
||
});
|
||
|
||
it("T4.4: у окна есть имя в состоянии загрузки и 404", async () => {
|
||
mockFetch((_m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (url.pathname === "/issues/AURORA-1") return new Promise<Response>(() => {});
|
||
if (url.pathname === "/issues/AURORA-2") return json(404, { detail: "нет" });
|
||
return json(200, {});
|
||
});
|
||
const r1 = render(<App router={at(["/app/issues/AURORA-1"])} />);
|
||
expect(await screen.findByRole("dialog", { name: "AURORA-1" })).toBeInTheDocument();
|
||
r1.unmount();
|
||
render(<App router={at(["/app/issues/AURORA-2"])} />);
|
||
expect(await screen.findByRole("dialog", { name: "AURORA-2" })).toBeInTheDocument();
|
||
});
|
||
});
|