sdlc-hub/web/src/modules/wiki/Tree.test.tsx
Andrey Epifancev 6b06395314 Вики: строки дерева не сжимаются, длинные названия не налезают (#133)
.wiki-tree — flex-колонка с max-height:70vh; при переполнении flex сжимал
.wiki-tree__item до min-height в одну строку, и перенесённый заголовок
вылезал на соседние узлы. flex:none — высота строки следует за текстом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:12:34 +04:00

98 lines
6.0 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 { 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(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
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(<Tree nodes={NODES} selected={null} onOpen={onOpen} />);
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(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
item("B").focus();
await userEvent.keyboard("{ArrowLeft}");
expect(item("B")).toHaveAttribute("aria-expanded", "false");
rerender(<Tree nodes={[...NODES]} selected="C" onOpen={() => {}} />); // та же выборка, новая ссылка
expect(item("B")).toHaveAttribute("aria-expanded", "false");
rerender(<Tree nodes={[...NODES, N("E", 2, "B")]} selected="C" onOpen={() => {}} />); // с добавленным узлом
expect(item("B")).toHaveAttribute("aria-expanded", "false");
rerender(<Tree nodes={[...NODES, N("E", 2, "B")]} selected="E" onOpen={() => {}} />); // другая страница
expect(item("B")).toHaveAttribute("aria-expanded", "true");
expect(item("E")).toBeInTheDocument();
});
it("выбранная страница появилась в nodes после монтирования — предки раскрываются", () => {
const { rerender } = render(<Tree nodes={[N("A", 0, null)]} selected="C" onOpen={() => {}} />);
expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull();
rerender(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
expect(item("B")).toHaveAttribute("aria-expanded", "true");
expect(item("C")).toBeInTheDocument();
});
it("T10.2: aria-setsize и aria-posinset — среди братьев одного родителя", () => {
render(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
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(<Tree nodes={[N("A", 0, null), N("C", 2, "B")]} selected="C" onOpen={() => {}} />);
expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull();
rerender(<Tree nodes={NODES} selected="C" onOpen={() => {}} />);
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/);
});
});