Модуль wiki, агенты модулей, поля проектов (#91) #92
@ -45,4 +45,18 @@ describe("Tree", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,13 +12,17 @@ export function Tree({ nodes, selected, onOpen }: { nodes: WikiNode[]; selected:
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
const refs = useRef(new Map<string, HTMLDivElement>());
|
||||
|
||||
// предки выбранной страницы раскрываются при её смене; ручное сворачивание потом не откатывается (зависимости без open)
|
||||
// предки выбранной страницы раскрываются только при смене selected: новый массив nodes (перечитанное дерево) и ручное
|
||||
// сворачивание родителя этого не повторяют; актуальные узлы читаются через ref и в зависимости не входят
|
||||
const byCodeRef = useRef(byCode);
|
||||
useEffect(() => { byCodeRef.current = byCode; });
|
||||
useEffect(() => {
|
||||
if (!selected) return;
|
||||
const map = byCodeRef.current;
|
||||
const up: string[] = [];
|
||||
for (let n = byCode.get(selected)?.parent_code; n && !up.includes(n); n = byCode.get(n)?.parent_code ?? null) up.push(n);
|
||||
for (let n = map.get(selected)?.parent_code; n && !up.includes(n); n = map.get(n)?.parent_code ?? null) up.push(n);
|
||||
if (up.length) setOpen((o) => (up.every((c) => o.has(c)) ? o : new Set([...o, ...up])));
|
||||
}, [selected, byCode]);
|
||||
}, [selected]);
|
||||
|
||||
const visible = useMemo(() => nodes.filter((n) => {
|
||||
for (let p = n.parent_code, guard = 0; p && guard < nodes.length; p = byCode.get(p)?.parent_code ?? null, guard++) {
|
||||
|
||||
@ -14,11 +14,13 @@ describe("useWikiDownload", () => {
|
||||
setToken("tok");
|
||||
vi.useFakeTimers();
|
||||
clicked = [];
|
||||
URL.createObjectURL = vi.fn(() => "blob:fake");
|
||||
URL.revokeObjectURL = vi.fn();
|
||||
// в jsdom этих методов нет: создаём заглушки и снимаем в afterEach (vi.restoreAllMocks)
|
||||
Object.assign(URL, { createObjectURL: () => "", revokeObjectURL: () => {} });
|
||||
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:fake");
|
||||
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {});
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (this: HTMLAnchorElement) { clicked.push(this.download); });
|
||||
});
|
||||
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); vi.unstubAllGlobals(); });
|
||||
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); vi.unstubAllGlobals(); Reflect.deleteProperty(URL, "createObjectURL"); Reflect.deleteProperty(URL, "revokeObjectURL"); });
|
||||
|
||||
it("fetching → опрос раз в 2 с → ready → файл скачан один раз под своим именем", async () => {
|
||||
let gets = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user