453 lines
31 KiB
TypeScript
453 lines
31 KiB
TypeScript
import { act, cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||
import userEvent from "@testing-library/user-event";
|
||
import { QueryClientProvider } from "@tanstack/react-query";
|
||
import { createMemoryRouter, RouterProvider, useNavigate, useParams } from "react-router";
|
||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { qk } from "../api/queries.ts";
|
||
import { makeQueryClient } from "../api/queryClient.ts";
|
||
import { setToken } from "../auth/token.ts";
|
||
import baseSettings from "../board/__fixtures__/settings.json";
|
||
import { json, mockFetch } from "../test/fetchMock.ts";
|
||
import { issue, note } from "../test/issueFixtures.ts";
|
||
import { IssueCard } from "./IssueCard.tsx";
|
||
|
||
const settings = { ...baseSettings, project_key: "AURORA" };
|
||
const deferred = <T,>() => { let resolve!: (v: T) => void; const promise = new Promise<T>((r) => { resolve = r; }); return { promise, resolve }; };
|
||
|
||
function CardAt() {
|
||
const { key = "" } = useParams();
|
||
const navigate = useNavigate();
|
||
// key — как в IssueModal: другая задача монтирует карточку заново
|
||
return <IssueCard key={key} issueKey={key} onClose={() => void navigate("/board")} />;
|
||
}
|
||
|
||
function open(path: string, client = makeQueryClient()) {
|
||
const router = createMemoryRouter([
|
||
{ path: "/issues/:key", element: <CardAt /> },
|
||
{ path: "/board", element: <p>доска</p> },
|
||
], { initialEntries: [path] });
|
||
render(<QueryClientProvider client={client}><RouterProvider router={router} /></QueryClientProvider>);
|
||
return router;
|
||
}
|
||
|
||
// высоты элементов в jsdom нулевые: для проверок свёртки по измерению — подмена
|
||
function overflow(scroll: number, client: number) {
|
||
const s = vi.spyOn(Element.prototype, "scrollHeight", "get").mockReturnValue(scroll);
|
||
const c = vi.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(client);
|
||
return () => { s.mockRestore(); c.mockRestore(); };
|
||
}
|
||
|
||
describe("карточка задачи", () => {
|
||
beforeEach(() => setToken("tok"));
|
||
|
||
it("шапка: ключ, статус Jira, тема, исполнитель · срок · спринт · эпик по имени; ссылка в Jira", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { planned_end: "2020-01-10" })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" })).toBeInTheDocument();
|
||
const head = screen.getByTestId("issue-head");
|
||
expect(within(head).getByText("AURORA-9001")).toBeInTheDocument();
|
||
expect(within(head).getByText("В работе", { selector: ".ui-badge" })).toBeInTheDocument();
|
||
expect(head.querySelector(".badge.fact")).toBeNull(); // статуса по факту в карточке нет (hide-closed)
|
||
expect(within(head).queryByText(/факт:/)).toBeNull();
|
||
expect(within(head).getByTestId("issue-facts")).toHaveTextContent("Иванов Иван Иванович · срок 10.01 просрочено · AWPCA_Спринт 2 · эпик: Тестовый эпик");
|
||
expect(within(head).getByText("просрочено")).toHaveClass("ui-badge--error");
|
||
expect(screen.getByRole("link", { name: "Открыть в Jira" })).toHaveAttribute("href", "https://sdlc.go.rshbank.ru/jira/browse/AURORA-9001");
|
||
});
|
||
|
||
it("связи по группам с темой и статусом; задача вне зеркала; прочий тип с направлением", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", {
|
||
links_out: [{ target_key: "AURORA-9002", link_type: "ASUPR_Блокер_Начала", summary: "API свода", status: "Новая" },
|
||
{ target_key: "lower-1", link_type: "ASUPR_Связанные", summary: null, status: null }],
|
||
links_in: [{ source_key: "AURORA-9003", link_type: "ASUPR_Блокер_Завершения", summary: "Макет экрана", status: "Готово" },
|
||
{ source_key: "AURORA-9008", link_type: "Duplicate", summary: "Актуализация ТЗ", status: "В работе" }],
|
||
})));
|
||
open("/issues/AURORA-9001");
|
||
const depends = await screen.findByRole("group", { name: "Зависит от" });
|
||
expect(within(depends).getByRole("listitem")).toHaveTextContent("завершение AURORA-9003 Макет экрана Готово");
|
||
const blocks = screen.getByRole("group", { name: "Блокирует" });
|
||
expect(within(blocks).getByRole("link", { name: "AURORA-9002" })).toHaveAttribute("href", "/issues/AURORA-9002");
|
||
expect(within(blocks).getByRole("listitem")).toHaveTextContent("начало AURORA-9002 API свода Новая");
|
||
expect(within(screen.getByRole("group", { name: "Связана с" })).getByRole("listitem")).toHaveTextContent("lower-1 нет в зеркале");
|
||
expect(within(screen.getByRole("group", { name: "Прочие" })).getByRole("listitem")).toHaveTextContent("← Duplicate AURORA-9008");
|
||
expect(screen.getByRole("link", { name: "AURORA-9002" }).closest(".issue")).not.toBeNull(); // O-10: цвет темы
|
||
});
|
||
|
||
it("без связей — «связей нет»", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { links_out: [], links_in: [] })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByText("связей нет")).toBeInTheDocument();
|
||
});
|
||
|
||
it("длинное описание в рендере свёрнуто по высоте, «развернуть»/«свернуть»; «исходник» не сбрасывает «развёрнуто»", async () => {
|
||
const restore = overflow(500, 180);
|
||
const long = Array.from({ length: 20 }, (_, n) => `абзац ${n + 1}`).join("\n\n");
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: long })));
|
||
open("/issues/AURORA-9001");
|
||
const body = (await screen.findByText("абзац 1")).closest(".description")!;
|
||
expect(body.tagName).toBe("DIV");
|
||
expect(body).toHaveClass("clamped");
|
||
await userEvent.click(screen.getByRole("button", { name: "развернуть" }));
|
||
expect(screen.getByText("абзац 1").closest(".description")).not.toHaveClass("clamped");
|
||
await userEvent.click(screen.getByRole("button", { name: "исходник" }));
|
||
const pre = document.querySelector("pre.description")!;
|
||
expect(pre).not.toHaveClass("clamped");
|
||
expect(screen.getByRole("button", { name: "свернуть" })).toBeInTheDocument();
|
||
await userEvent.click(screen.getByRole("button", { name: "свернуть" }));
|
||
expect(document.querySelector("pre.description")).toHaveClass("clamped");
|
||
restore();
|
||
});
|
||
|
||
it("C-1: описание смонтировано скрытым (диалог ещё закрыт) — «развернуть» появляется, когда размеры изменились", async () => {
|
||
const observers: (() => void)[] = [];
|
||
vi.stubGlobal("ResizeObserver", class { constructor(cb: () => void) { observers.push(cb); } observe() {} disconnect() {} });
|
||
let restore = overflow(0, 0);
|
||
const long = Array.from({ length: 20 }, (_, n) => `абзац ${n + 1}`).join("\n\n");
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: long })));
|
||
open("/issues/AURORA-9001");
|
||
await screen.findByText("абзац 1");
|
||
expect(screen.queryByRole("button", { name: "развернуть" })).toBeNull();
|
||
restore();
|
||
restore = overflow(500, 180);
|
||
act(() => observers.forEach((cb) => cb()));
|
||
expect(screen.getByRole("button", { name: "развернуть" })).toBeInTheDocument();
|
||
restore();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
it("исходник длиннее 8 строк свёрнут построчно", async () => {
|
||
const long = Array.from({ length: 10 }, (_, n) => `строка ${n + 1}`).join("\n");
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: long })));
|
||
open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "исходник" }));
|
||
const pre = screen.getByText(long, { normalizer: (s) => s });
|
||
expect(pre).toHaveClass("description", "clamped");
|
||
expect(screen.getByRole("button", { name: "развернуть" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("список через пустые строки, который помещается, — в рендере без «развернуть» (design #8)", async () => {
|
||
const restore = overflow(120, 180);
|
||
const d = " * один\n\n * два\n\n * три\n\n * четыре\n\n * пять";
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: d })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByText("пять")).toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: "развернуть" })).toBeNull();
|
||
restore();
|
||
});
|
||
|
||
it("описание отрендерено; «исходник» — исходник только у описания, «текст» — обратно", async () => {
|
||
const d = "h3. Контекст\n# Загрузить ОСВ\n# Открыть документ";
|
||
const comments = [{ comment_id: "1", author: "Иванов Иван", created: null, updated: null, body: "*важно*" }];
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: d, comments })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("heading", { level: 3, name: "Контекст" })).toBeInTheDocument();
|
||
const section = screen.getByRole("heading", { name: "Описание" }).closest("section")!;
|
||
await userEvent.click(within(section).getByRole("button", { name: "исходник" }));
|
||
expect(section.querySelector("pre.description")).toHaveTextContent("h3. Контекст");
|
||
expect(screen.queryByRole("heading", { level: 3, name: "Контекст" })).toBeNull();
|
||
expect(screen.getByText("важно").tagName).toBe("STRONG");
|
||
await userEvent.click(within(section).getByRole("button", { name: "текст" }));
|
||
expect(screen.getByRole("heading", { level: 3, name: "Контекст" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("сценарий «Исходник у одного комментария»: переключается только он, после перехода всё отрендерено", async () => {
|
||
const comments = [
|
||
{ comment_id: "2", author: "Петров", created: null, updated: null, body: "*b*" },
|
||
{ comment_id: "1", author: "Иванов", created: null, updated: null, body: "*a*" }];
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue(url.pathname.split("/").pop()!, { comments })));
|
||
const router = open("/issues/AURORA-9001");
|
||
await screen.findByRole("heading", { name: "Комментарии (2)" });
|
||
const items = () => within(screen.getByTestId("comments")).getAllByRole("listitem");
|
||
await userEvent.click(within(items()[1]).getByRole("button", { name: "исходник" }));
|
||
expect(items()[1].querySelector("pre.body")).toHaveTextContent("*a*");
|
||
expect(within(items()[0]).getByText("b").tagName).toBe("STRONG");
|
||
await act(() => router.navigate("/issues/AURORA-9002"));
|
||
await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9002" });
|
||
await act(() => router.navigate(-1));
|
||
await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" });
|
||
await waitFor(() => expect(within(items()[1]).getByText("a").tagName).toBe("STRONG"));
|
||
expect(document.querySelector("pre.body")).toBeNull();
|
||
});
|
||
|
||
it("ключ задачи проекта в комментарии — ссылка на карточку", async () => {
|
||
const comments = [{ comment_id: "1", author: "Иванов", created: null, updated: null, body: "П. 1 ведётся в AURORA-9002, не UTF-8" }];
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue(url.pathname.split("/").pop()!, { comments })));
|
||
const router = open("/issues/AURORA-9001");
|
||
const comment = within(await screen.findByTestId("comments"));
|
||
expect(comment.queryByRole("link", { name: "UTF-8" })).toBeNull();
|
||
await userEvent.click(comment.getByRole("link", { name: "AURORA-9002" }));
|
||
expect(router.state.location.pathname).toBe("/issues/AURORA-9002");
|
||
});
|
||
|
||
it("короткое описание — без кнопки; нет описания — нет раздела", async () => {
|
||
let d: string | null = "строка 1\nстрока 2";
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: d })));
|
||
open("/issues/AURORA-9001");
|
||
const body = (await screen.findByText(/строка 1/)).closest(".description")!;
|
||
expect(body).toHaveTextContent("строка 1строка 2");
|
||
expect(body.querySelector("br")).not.toBeNull();
|
||
expect(screen.queryByRole("button", { name: "развернуть" })).toBeNull();
|
||
d = null;
|
||
cleanup();
|
||
open("/issues/AURORA-9001");
|
||
await screen.findByRole("heading", { level: 2 });
|
||
expect(screen.queryByRole("heading", { name: "Описание" })).toBeNull();
|
||
});
|
||
|
||
it("комментарии новые сверху; «Скрыть» запоминается для следующей карточки", async () => {
|
||
const comments = [
|
||
{ comment_id: "2", author: "Петров Пётр", created: "2026-10-15T12:00:00.000+0300", updated: null, body: "новый\nвторая строка" },
|
||
{ comment_id: "1", author: "Иванов Иван", created: "2026-10-14T12:00:00.000+0300", updated: null, body: "старый" }];
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { comments })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("heading", { name: "Комментарии (2)" })).toBeInTheDocument();
|
||
const items = within(screen.getByTestId("comments")).getAllByRole("listitem");
|
||
expect(items[0]).toHaveTextContent("Петров Пётр");
|
||
expect(items[0].querySelector(".body")).toHaveTextContent("новыйвторая строка");
|
||
expect(items[0].querySelector(".body br")).not.toBeNull();
|
||
await userEvent.click(screen.getByRole("button", { name: "Скрыть" }));
|
||
expect(screen.queryByText("старый")).toBeNull();
|
||
expect(localStorage.getItem("jira-hub-comments-hidden")).toBe("1");
|
||
cleanup();
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("button", { name: "Показать" })).toBeInTheDocument();
|
||
expect(screen.queryByText("старый")).toBeNull();
|
||
});
|
||
|
||
it("комментарий без автора и текста не ломает карточку; без комментариев — «комментариев нет»", async () => {
|
||
let comments = [{ comment_id: "1", author: null, created: null, updated: null, body: "" }];
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { comments })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("heading", { name: "Комментарии (1)" })).toBeInTheDocument();
|
||
expect(within(screen.getByTestId("comments")).getByRole("listitem")).toHaveTextContent("—");
|
||
comments = [];
|
||
cleanup();
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByText("комментариев нет")).toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: "Скрыть" })).toBeNull();
|
||
});
|
||
|
||
it("прочие поля и команды моста свёрнуты", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
open("/issues/AURORA-9001");
|
||
const other = await screen.findByRole("region", { name: "Прочие поля" });
|
||
expect(other).toHaveTextContent("Приоритет");
|
||
expect(other).toHaveTextContent("Высокий");
|
||
const details = screen.getByText("Команды моста (1)").closest("details")!;
|
||
expect(details).not.toHaveAttribute("open");
|
||
expect(within(details).getByRole("cell", { name: "ensure_fields" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("заголовок карточки — h2 с id для aria-labelledby окна", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByRole("heading", { level: 2, name: /AURORA-9001/ })).toHaveAttribute("id", "issue-modal-title");
|
||
});
|
||
|
||
it("задачи нет в зеркале — сообщение и «Закрыть»", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(404, { error: "not_found", detail: "issue" }));
|
||
const router = open("/issues/AURORA-1");
|
||
expect(await screen.findByText("Задачи AURORA-1 нет в зеркале хаба.")).toBeInTheDocument();
|
||
await userEvent.click(screen.getByRole("button", { name: "Закрыть" }));
|
||
expect(router.state.location.pathname).toBe("/board");
|
||
});
|
||
|
||
it("сохраняются только изменённые поля, очищенное — null", async () => {
|
||
let put: unknown = null;
|
||
mockFetch((m, url, body) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (m === "PUT") { put = body; return json(200, note({ rest_text: "тесты", done_text: null, updated_by: "web" })); }
|
||
return json(200, issue("AURORA-9001"));
|
||
});
|
||
open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
const rest = await screen.findByLabelText("Осталось");
|
||
await userEvent.clear(rest); await userEvent.type(rest, "тесты");
|
||
await userEvent.clear(screen.getByLabelText("Сделано"));
|
||
await userEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||
await waitFor(() => expect(put).toEqual({ rest_text: "тесты", done_text: null, updated_by: "web" }));
|
||
expect(await screen.findByText(/изменено .*web/)).toBeInTheDocument(); // снова режим чтения
|
||
expect(screen.getByRole("button", { name: "Править" })).toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: "Сохранить" })).toBeNull();
|
||
});
|
||
|
||
it("422 — текст ошибки у формы, введённое на месте", async () => {
|
||
mockFetch((m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (m === "PUT") return json(422, { error: "bad_note", detail: "неизвестные поля заметки: x" });
|
||
return json(200, issue("AURORA-9001"));
|
||
});
|
||
open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
const owner = await screen.findByLabelText("Заметка владельца");
|
||
await userEvent.type(owner, "проверить");
|
||
await userEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||
expect(await screen.findByRole("alert")).toHaveTextContent("неизвестные поля заметки: x");
|
||
expect(owner).toHaveValue("проверить");
|
||
});
|
||
|
||
it("уход с несохранёнными правками требует подтверждения", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
const router = open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
await userEvent.type(await screen.findByLabelText("Заметка владельца"), "черновик");
|
||
await act(async () => { await router.navigate("/board"); });
|
||
expect(await screen.findByText("Есть несохранённые правки заметки. Уйти без сохранения?")).toBeInTheDocument();
|
||
await userEvent.click(screen.getByRole("button", { name: "Остаться" }));
|
||
expect(router.state.location.pathname).toBe("/issues/AURORA-9001");
|
||
expect(screen.getByLabelText("Заметка владельца")).toHaveValue("черновик");
|
||
});
|
||
|
||
it("поздний ответ прежней карточки не подменяет новую", async () => {
|
||
const client = makeQueryClient();
|
||
const slow = deferred<Response>();
|
||
mockFetch((_m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (url.pathname === "/issues/AURORA-9001") return slow.promise;
|
||
return json(200, issue("AURORA-9002"));
|
||
});
|
||
const router = open("/issues/AURORA-9001", client);
|
||
await act(async () => { await router.navigate("/issues/AURORA-9002"); });
|
||
expect(await screen.findByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
slow.resolve(json(200, issue("AURORA-9001")));
|
||
await waitFor(() => expect(client.getQueryData(qk.issue("AURORA-9001"))).toBeDefined());
|
||
expect(screen.getByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
expect(screen.queryByRole("heading", { name: /AURORA-9001/ })).toBeNull();
|
||
});
|
||
|
||
it("ответ «Сохранить» после ухода в другую карточку не пишет её заметку в форму новой", async () => {
|
||
const slowPut = deferred<Response>();
|
||
mockFetch((m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (m === "PUT") return slowPut.promise;
|
||
if (url.pathname === "/issues/AURORA-9002") return json(200, issue("AURORA-9002"));
|
||
return json(200, issue("AURORA-9001"));
|
||
});
|
||
// 9002 уже в кэше: форма не размонтируется при переходе, её состояние переживает навигацию
|
||
const client = makeQueryClient();
|
||
client.setQueryData(qk.issue("AURORA-9002"), issue("AURORA-9002"));
|
||
const router = open("/issues/AURORA-9001", client);
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
await userEvent.type(await screen.findByLabelText("Заметка владельца"), "черновик");
|
||
await userEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||
await act(async () => { await router.navigate("/issues/AURORA-9002"); });
|
||
await userEvent.click(await screen.findByRole("button", { name: "Уйти" }));
|
||
expect(await screen.findByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
slowPut.resolve(json(200, note({ owner_note: "черновик", updated_by: "web" })));
|
||
await waitFor(() => expect(client.isMutating()).toBe(0));
|
||
expect(screen.getByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||
expect(screen.getByRole("button", { name: "Править" })).toBeInTheDocument(); // новая карточка — в режиме чтения
|
||
expect(screen.queryByText("черновик")).toBeNull();
|
||
});
|
||
|
||
it("T14.1: поля заметки заблокированы на время сохранения, ответ не затирает ввод", async () => {
|
||
const slowPut = deferred<Response>();
|
||
mockFetch((m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (m === "PUT") return slowPut.promise;
|
||
return json(200, issue("AURORA-9001"));
|
||
});
|
||
open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
await userEvent.type(await screen.findByLabelText("Заметка владельца"), "раз");
|
||
await userEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||
const box = screen.getByLabelText("Заметка владельца");
|
||
await waitFor(() => expect(box).toBeDisabled());
|
||
slowPut.resolve(json(200, note({ owner_note: "раз", updated_by: "web" })));
|
||
await screen.findByRole("button", { name: "Править" }); // режим чтения (findByText("раз") ловил бы ещё живой textarea)
|
||
expect(screen.getByText("раз")).toBeInTheDocument(); // с сохранённым значением
|
||
expect(screen.queryByLabelText("Заметка владельца")).toBeNull(); // форма закрыта
|
||
});
|
||
|
||
it("M-2: ошибка повторной загрузки при уже имеющихся данных — данные остаются, строка с «Повторить»", async () => {
|
||
const client = makeQueryClient();
|
||
client.setQueryData(qk.issue("AURORA-9001"), issue("AURORA-9001"));
|
||
let down = true;
|
||
mockFetch((_m, url) => {
|
||
if (url.pathname === "/settings") return json(200, settings);
|
||
if (down) throw new TypeError("Failed to fetch");
|
||
return json(200, issue("AURORA-9001"));
|
||
});
|
||
open("/issues/AURORA-9001", client);
|
||
expect(await screen.findByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
await act(async () => { await client.invalidateQueries({ queryKey: qk.issue("AURORA-9001") }); }); // повторная загрузка при данных в кэше
|
||
expect(await screen.findByRole("alert")).toHaveTextContent("хаб не отвечает");
|
||
expect(screen.getByRole("heading", { name: /AURORA-9001/ })).toBeInTheDocument();
|
||
down = false;
|
||
await userEvent.click(screen.getByRole("button", { name: "Повторить" }));
|
||
await waitFor(() => expect(screen.queryByRole("alert")).toBeNull());
|
||
});
|
||
|
||
it("O-11: задача без команд — текст вместо пустой таблицы", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { commands: [] })));
|
||
open("/issues/AURORA-9001");
|
||
expect(await screen.findByText("команд по задаче не было")).toBeInTheDocument();
|
||
expect(screen.queryByRole("columnheader", { name: "Команда" })).toBeNull();
|
||
});
|
||
|
||
it("закрытая задача со сроком в прошлом — без пометки «просрочено»", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { planned_end: "2020-01-10", status: "Готово" })));
|
||
open("/issues/AURORA-9001");
|
||
const facts = await screen.findByTestId("issue-facts");
|
||
expect(facts).toHaveTextContent("срок 10.01");
|
||
expect(within(facts).queryByText("просрочено")).toBeNull();
|
||
});
|
||
|
||
it("статуса по факту нет ни в чтении заметок, ни в форме", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
open("/issues/AURORA-9001");
|
||
const view = await screen.findByRole("region", { name: "Служебные заметки" });
|
||
expect(view).not.toHaveTextContent("Статус по факту");
|
||
expect(view).toHaveTextContent("Вид работы");
|
||
await userEvent.click(screen.getByRole("button", { name: "Править" }));
|
||
await screen.findByLabelText("Осталось");
|
||
expect(screen.queryByLabelText("Статус по факту")).toBeNull();
|
||
expect(document.querySelector("datalist")).toBeNull();
|
||
});
|
||
|
||
it("заметки в режиме чтения: пустые — «—», время и автор; «Отмена» без правок — сразу", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
open("/issues/AURORA-9001");
|
||
const view = await screen.findByRole("region", { name: "Служебные заметки" });
|
||
expect(view).toHaveTextContent("Сделано");
|
||
expect(view).toHaveTextContent("схема");
|
||
expect(within(view).getByText("Заметка владельца").nextElementSibling).toHaveTextContent("—");
|
||
expect(view).toHaveTextContent(/изменено .*chat/);
|
||
await userEvent.click(within(view).getByRole("button", { name: "Править" }));
|
||
await userEvent.click(screen.getByRole("button", { name: "Отмена" }));
|
||
expect(screen.getByRole("button", { name: "Править" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("«Отмена» с правками спрашивает; «Отменить правки» возвращает чтение без изменений", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001")));
|
||
open("/issues/AURORA-9001");
|
||
await userEvent.click(await screen.findByRole("button", { name: "Править" }));
|
||
await userEvent.type(screen.getByLabelText("Заметка владельца"), "черновик");
|
||
await userEvent.click(screen.getByRole("button", { name: "Отмена" }));
|
||
expect(screen.getByText("Отменить правки заметки?")).toBeInTheDocument();
|
||
await userEvent.click(screen.getByRole("button", { name: "Продолжить правку" }));
|
||
expect(screen.getByLabelText("Заметка владельца")).toHaveValue("черновик");
|
||
await userEvent.click(screen.getByRole("button", { name: "Отмена" }));
|
||
await userEvent.click(screen.getByRole("button", { name: "Отменить правки" }));
|
||
expect(screen.getByRole("button", { name: "Править" })).toBeInTheDocument();
|
||
expect(screen.queryByText("черновик")).toBeNull();
|
||
});
|
||
|
||
it("заметки ещё нет — режим чтения с «—» и «Править»", async () => {
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { note: null })));
|
||
open("/issues/AURORA-9001");
|
||
const view = await screen.findByRole("region", { name: "Служебные заметки" });
|
||
expect(within(view).getAllByText("—")).toHaveLength(4); // вид работы, сделано, осталось, заметка владельца
|
||
expect(within(view).getByRole("button", { name: "Править" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("T5.3: одинаковые связи не дают предупреждения о повторяющихся key", async () => {
|
||
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
||
const dup = { target_key: "AURORA-9002", link_type: "ASUPR_Блокер_Начала", summary: "API", status: "Новая" };
|
||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { links_out: [dup, dup] })));
|
||
open("/issues/AURORA-9001");
|
||
await screen.findAllByRole("link", { name: "AURORA-9002" });
|
||
expect(err.mock.calls.flat().join(" ")).not.toContain("same key");
|
||
err.mockRestore();
|
||
});
|
||
});
|