Ctrl K
ComunidadGuías…

shared/utils/tree.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { NavigationNode } from "../types";
import {
  ancestors,
  descendants,
  flattenExpandedTree,
  flattenTree,
} from "./tree";

const buildNode = (
  id: string,
  children: NavigationNode[] = []
): NavigationNode => ({
  id,
  title: `Title ${id}`,
  url: `/doc/${id}`,
  children,
});

/**
 * Annotates a tree with the `parent` and `depth` fields added at runtime by
 * useCollectionTrees, mirroring how nodes are prepared in the app.
 */
const annotate = (
  node: NavigationNode,
  parent: NavigationNode | null = null,
  depth = 0
): NavigationNode => {
  node.parent = parent;
  node.depth = depth;
  node.children.forEach((child) => annotate(child, node, depth + 1));
  return node;
};

const buildTree = () =>
  buildNode("root", [
    buildNode("a", [buildNode("a1", [buildNode("a1i")]), buildNode("a2")]),
    buildNode("b"),
  ]);

describe("#flattenTree", () => {
  it("should return all nodes in depth-first order", () => {
    expect(flattenTree(buildTree()).map((node) => node.id)).toEqual([
      "root",
      "a",
      "a1",
      "a1i",
      "a2",
      "b",
    ]);
  });

  it("should return a single node for a leaf", () => {
    expect(flattenTree(buildNode("leaf")).map((node) => node.id)).toEqual([
      "leaf",
    ]);
  });

  it("should tolerate nodes without a children array", () => {
    const node: Partial<NavigationNode> = buildNode("orphan");
    delete node.children;

    expect(flattenTree(node as NavigationNode).map((n) => n.id)).toEqual([
      "orphan",
    ]);
  });
});

describe("#ancestors", () => {
  it("should return ancestors ordered from root to direct parent", () => {
    const root = annotate(buildTree());
    const deepest = root.children[0].children[0].children[0];

    expect(ancestors(deepest).map((node) => node.id)).toEqual([
      "root",
      "a",
      "a1",
    ]);
  });

  it("should return an empty list for a root node", () => {
    const root = annotate(buildTree());
    expect(ancestors(root)).toEqual([]);
  });

  it("should return an empty list for a missing node", () => {
    expect(ancestors(null)).toEqual([]);
  });

  it("should treat nodes without a parent annotation as roots", () => {
    // Nodes straight from the server, e.g. a shared tree, carry no parent.
    expect(ancestors(buildNode("unannotated"))).toEqual([]);
  });

  it("should not loop forever on a malformed parent cycle", () => {
    const first = buildNode("first");
    const second = buildNode("second");
    first.parent = second;
    second.parent = first;

    // The node itself is never part of its own ancestors, even when the
    // parent chain cycles back to it.
    expect(ancestors(first).map((node) => node.id)).toEqual(["second"]);
  });
});

describe("#descendants", () => {
  it("should return all descendants by default", () => {
    expect(descendants(buildTree()).map((node) => node.id)).toEqual([
      "a",
      "a1",
      "a1i",
      "a2",
      "b",
    ]);
  });

  it("should limit results to the given depth", () => {
    const root = buildTree();

    expect(descendants(root, 1).map((node) => node.id)).toEqual(["a", "b"]);
    expect(descendants(root, 2).map((node) => node.id)).toEqual([
      "a",
      "a1",
      "a2",
      "b",
    ]);
  });

  it("should limit by depth relative to the node itself", () => {
    const root = annotate(buildTree());
    const child = root.children[0];

    expect(descendants(child, 1).map((node) => node.id)).toEqual(["a1", "a2"]);
  });

  it("should not require depth annotations on nodes", () => {
    // Nodes straight from the server carry no depth annotation.
    expect(descendants(buildTree(), 1).map((node) => node.id)).toEqual([
      "a",
      "b",
    ]);
  });

  it("should return an empty list for a leaf node", () => {
    expect(descendants(buildNode("leaf"))).toEqual([]);
  });
});

describe("#flattenExpandedTree", () => {
  // a
  // ├── b
  // │   ├── c
  // │   └── d
  // └── e
  // f
  const nodes = [
    buildNode("a", [
      buildNode("b", [buildNode("c"), buildNode("d")]),
      buildNode("e"),
    ]),
    buildNode("f"),
  ];

  const flatten = (
    expandedIds: string[],
    getChildren?: (node: NavigationNode) => NavigationNode[]
  ) =>
    flattenExpandedTree({
      nodes,
      depth: 2,
      parentId: "root",
      isExpanded: (id) => expandedIds.includes(id),
      getChildren,
    });

  it("should return only the root nodes when nothing is expanded", () => {
    const flattened = flatten([]);

    expect(flattened.map((item) => item.node.id)).toEqual(["a", "f"]);
    expect(flattened[0]).toMatchObject({
      depth: 2,
      index: 0,
      parentId: "root",
      hasChildren: true,
    });
    expect(flattened[1]).toMatchObject({ index: 1, hasChildren: false });
  });

  it("should include the children of expanded nodes in display order", () => {
    const flattened = flatten(["a", "b"]);

    expect(
      flattened.map((item) => [item.node.id, item.depth, item.parentId])
    ).toEqual([
      ["a", 2, "root"],
      ["b", 3, "a"],
      ["c", 4, "b"],
      ["d", 4, "b"],
      ["e", 3, "a"],
      ["f", 2, "root"],
    ]);
  });

  it("should skip the children of a collapsed node", () => {
    const flattened = flatten(["b"]);

    expect(flattened.map((item) => item.node.id)).toEqual(["a", "f"]);
  });

  it("should give the last child the ancestors that end with it", () => {
    const flattened = flatten(["a", "b"]);
    const trailing = Object.fromEntries(
      flattened.map((item) => [
        item.node.id,
        item.trailingAncestors.map((ancestor) => ancestor.node.id),
      ])
    );

    expect(trailing).toEqual({
      a: [],
      b: [],
      c: [],
      d: ["b"],
      e: ["a"],
      f: [],
    });
  });

  it("should use the children returned by getChildren", () => {
    const draft = buildNode("draft");
    const flattened = flatten(["f"], (node) =>
      node.id === "f" ? [draft] : node.children
    );

    expect(flattened.map((item) => item.node.id)).toEqual(["a", "f", "draft"]);
    expect(flattened[1].hasChildren).toBe(true);
  });
});