import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { WikiNode } from "./api/queries.ts"; import { Tree } from "./Tree.tsx"; const N = (code: string, depth: number, parent: string | null): WikiNode => ({ code, title: `Стр ${code}`, depth, parent_code: parent, size: 1 }); const NODES = [N("A", 0, null), N("B", 1, "A"), N("C", 2, "B"), N("D", 1, "A")]; const item = (code: string) => screen.getByRole("treeitem", { name: `Стр ${code}` }); describe("Tree", () => { it("родителя открытой страницы можно свернуть клавишей и шевроном, он остаётся свёрнутым", async () => { const { container } = render( {}} />); expect(item("C")).toBeInTheDocument(); // предки выбранной страницы раскрыты item("B").focus(); await userEvent.keyboard("{ArrowLeft}"); expect(item("B")).toHaveAttribute("aria-expanded", "false"); expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull(); await userEvent.click(item("B").querySelector(".wiki-tree__chevron")!); // раскрыть шевроном expect(item("B")).toHaveAttribute("aria-expanded", "true"); await userEvent.click(item("B").querySelector(".wiki-tree__chevron")!); // и свернуть шевроном expect(item("B")).toHaveAttribute("aria-expanded", "false"); await userEvent.click(item("A").querySelector(".wiki-tree__chevron")!); // корень тоже сворачивается expect(container.querySelectorAll('[role="treeitem"]')).toHaveLength(1); }); it("клавиатура: стрелки, Home/End, вправо входит в узел, Enter открывает страницу", async () => { const onOpen = vi.fn(); render(); item("A").focus(); await userEvent.keyboard("{ArrowDown}"); expect(item("B")).toHaveFocus(); await userEvent.keyboard("{ArrowRight}"); // B свёрнут → раскрыть expect(item("B")).toHaveAttribute("aria-expanded", "true"); await userEvent.keyboard("{ArrowRight}"); // раскрыт → к первому потомку expect(item("C")).toHaveFocus(); await userEvent.keyboard("{ArrowLeft}"); // лист → к родителю expect(item("B")).toHaveFocus(); await userEvent.keyboard("{End}"); expect(item("D")).toHaveFocus(); await userEvent.keyboard("{ArrowUp}"); expect(item("C")).toHaveFocus(); await userEvent.keyboard("{Home}"); expect(item("A")).toHaveFocus(); await userEvent.keyboard("{Enter}"); expect(onOpen).toHaveBeenLastCalledWith("A"); }); it("новый массив nodes не раскрывает свёрнутого родителя; смена selected раскрывает предков нового", async () => { const { rerender } = render( {}} />); item("B").focus(); await userEvent.keyboard("{ArrowLeft}"); expect(item("B")).toHaveAttribute("aria-expanded", "false"); rerender( {}} />); // та же выборка, новая ссылка expect(item("B")).toHaveAttribute("aria-expanded", "false"); rerender( {}} />); // с добавленным узлом expect(item("B")).toHaveAttribute("aria-expanded", "false"); rerender( {}} />); // другая страница expect(item("B")).toHaveAttribute("aria-expanded", "true"); expect(item("E")).toBeInTheDocument(); }); it("выбранная страница появилась в nodes после монтирования — предки раскрываются", () => { const { rerender } = render( {}} />); expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull(); rerender( {}} />); expect(item("B")).toHaveAttribute("aria-expanded", "true"); expect(item("C")).toBeInTheDocument(); }); it("T10.2: aria-setsize и aria-posinset — среди братьев одного родителя", () => { render( {}} />); expect(item("A")).toHaveAttribute("aria-setsize", "1"); expect(item("B")).toHaveAttribute("aria-setsize", "2"); expect(item("B")).toHaveAttribute("aria-posinset", "1"); expect(item("D")).toHaveAttribute("aria-posinset", "2"); expect(item("C")).toHaveAttribute("aria-setsize", "1"); }); it("T10.6: узел выбранной страницы пришёл раньше родителя — предки раскрываются, когда цепочка до корня стала полной", () => { const { rerender } = render( {}} />); expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull(); rerender( {}} />); expect(item("C")).toBeInTheDocument(); expect(item("B")).toHaveAttribute("aria-expanded", "true"); }); }); describe("дерево: многострочные названия (#133)", () => { it("строка дерева не сжимается в переполненной колонке — перенесённый текст не налезает на соседей", () => { const css = readFileSync(resolve(__dirname, "wiki.css"), "utf8"); const rule = css.match(/\.wiki-tree__item\{[^}]*\}/)?.[0] ?? ""; expect(rule).toMatch(/flex:none|flex-shrink:0/); }); });