.wiki-tree — flex-колонка с max-height:70vh; при переполнении flex сжимал .wiki-tree__item до min-height в одну строку, и перенесённый заголовок вылезал на соседние узлы. flex:none — высота строки следует за текстом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
6.0 KiB
TypeScript
98 lines
6.0 KiB
TypeScript
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/);
|
||
});
|
||
});
|