Files
gitmost/apps/client/src/features/editor/components/subpages/subpages-view.utils.test.ts
claude code agent 227 623c89554a refactor(subpages): address PR #155 review
- Extract buildSubtree/mapSharedNodes/countNodes/SubpageNode into
  subpages-view.utils.ts with a unit test (subpages-view.utils.test.ts)
  covering nesting, position order, missing/unreachable parent, self-parent
  guard, empty input, countNodes and mapSharedNodes remap.
- Replace the manual useState + editor.on("transaction") subscription in
  subpages-menu.tsx with useEditorState (the idiom the sibling bubble menus
  use), so the mode icon/tooltip track the live recursive attribute without
  re-rendering on every keystroke.
- i18n: add the 6 menu/tree strings and a pluralized
  "Showing {{count}} subpages" key to en-US and ru-RU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:33:25 +03:00

115 lines
3.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
buildSubtree,
countNodes,
mapSharedNodes,
SubpageNode,
} from "./subpages-view.utils";
import { IPage } from "@/features/page/types/page.types";
// Minimal IPage fixture — buildSubtree only reads id/slugId/title/icon/position/
// parentPageId. `position` keys are fractional-indexing strings (lexicographic).
const page = (p: Partial<IPage> & { id: string }): IPage =>
({
slugId: `slug-${p.id}`,
title: `Title ${p.id}`,
icon: undefined,
position: "a0",
parentPageId: null,
...p,
}) as IPage;
const ids = (nodes: SubpageNode[]): string[] => nodes.map((n) => n.id);
describe("buildSubtree", () => {
it("nests children under the root and excludes the root itself", () => {
const pages = [
page({ id: "root" }),
page({ id: "a", parentPageId: "root", position: "a0" }),
page({ id: "b", parentPageId: "root", position: "a1" }),
page({ id: "a1", parentPageId: "a", position: "a0" }),
];
const tree = buildSubtree(pages, "root");
// Root is not rendered; only its descendants.
expect(ids(tree)).toEqual(["a", "b"]);
expect(ids(tree[0].children)).toEqual(["a1"]);
expect(tree[1].children).toEqual([]);
});
it("sorts each level by position", () => {
const pages = [
page({ id: "root" }),
page({ id: "z", parentPageId: "root", position: "a2" }),
page({ id: "x", parentPageId: "root", position: "a0" }),
page({ id: "y", parentPageId: "root", position: "a1" }),
];
expect(ids(buildSubtree(pages, "root"))).toEqual(["x", "y", "z"]);
});
it("returns [] when the root is absent from the page set", () => {
const pages = [page({ id: "a", parentPageId: "missing-root" })];
expect(buildSubtree(pages, "missing-root")).toEqual([]);
});
it("silently drops a node whose parent is absent (unreachable parent)", () => {
const pages = [
page({ id: "root" }),
page({ id: "ok", parentPageId: "root" }),
page({ id: "orphan", parentPageId: "ghost" }), // parent not in the set
];
expect(ids(buildSubtree(pages, "root"))).toEqual(["ok"]);
});
it("guards against self-parenting / attaching the root", () => {
const pages = [
// A (defensive) self-parented root must not attach to itself.
page({ id: "root", parentPageId: "root" }),
page({ id: "a", parentPageId: "root" }),
];
const tree = buildSubtree(pages, "root");
expect(ids(tree)).toEqual(["a"]);
});
it("returns [] for empty input", () => {
expect(buildSubtree([], "root")).toEqual([]);
});
});
describe("countNodes", () => {
it("counts every descendant across all levels", () => {
const tree: SubpageNode[] = [
{
id: "a",
slugId: "s",
title: "A",
children: [
{ id: "a1", slugId: "s", title: "A1", children: [] },
{ id: "a2", slugId: "s", title: "A2", children: [] },
],
},
{ id: "b", slugId: "s", title: "B", children: [] },
];
expect(countNodes(tree)).toBe(4);
expect(countNodes([])).toBe(0);
});
});
describe("mapSharedNodes", () => {
it("remaps value->id / name->title and keeps nested children", () => {
const shared = [
{
value: "p1",
slugId: "s1",
name: "Parent",
icon: "📁",
children: [
{ value: "c1", slugId: "sc1", name: "Child", children: [] },
],
},
] as any;
const mapped = mapSharedNodes(shared);
expect(mapped[0]).toMatchObject({ id: "p1", slugId: "s1", title: "Parent", icon: "📁" });
expect(mapped[0].children[0]).toMatchObject({ id: "c1", title: "Child" });
});
});