Веб: вики — дерево не раскрывает предков при новом nodes (#91)
Эффект раскрытия предков зависит только от selected; тест с новым массивом nodes; заглушки URL.createObjectURL в тесте загрузки через spyOn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a142f772c
commit
a37ce0a59c
@ -45,4 +45,18 @@ describe("Tree", () => {
|
|||||||
await userEvent.keyboard("{Enter}");
|
await userEvent.keyboard("{Enter}");
|
||||||
expect(onOpen).toHaveBeenLastCalledWith("A");
|
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 [focus, setFocus] = useState<string | null>(null);
|
||||||
const refs = useRef(new Map<string, HTMLDivElement>());
|
const refs = useRef(new Map<string, HTMLDivElement>());
|
||||||
|
|
||||||
// предки выбранной страницы раскрываются при её смене; ручное сворачивание потом не откатывается (зависимости без open)
|
// предки выбранной страницы раскрываются только при смене selected: новый массив nodes (перечитанное дерево) и ручное
|
||||||
|
// сворачивание родителя этого не повторяют; актуальные узлы читаются через ref и в зависимости не входят
|
||||||
|
const byCodeRef = useRef(byCode);
|
||||||
|
useEffect(() => { byCodeRef.current = byCode; });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
const map = byCodeRef.current;
|
||||||
const up: string[] = [];
|
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])));
|
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) => {
|
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++) {
|
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");
|
setToken("tok");
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
clicked = [];
|
clicked = [];
|
||||||
URL.createObjectURL = vi.fn(() => "blob:fake");
|
// в jsdom этих методов нет: создаём заглушки и снимаем в afterEach (vi.restoreAllMocks)
|
||||||
URL.revokeObjectURL = vi.fn();
|
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); });
|
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 () => {
|
it("fetching → опрос раз в 2 с → ready → файл скачан один раз под своим именем", async () => {
|
||||||
let gets = 0;
|
let gets = 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user