Ctrl K
ComunidadGuías…

tests/phoneTree.test.ts

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

Archivo original ↗Preguntar a KiBot
// The Notes tab's two new pure parts (3.34): a folder's path folded into the
// crumbs that fit its top bar (client/phone/crumbs.ts), and the tree's rows
// with the device's memory of which folders are open (client/phone/treeRows.ts).

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { TreeNode } from "../shared/types.ts";
import { collapseCrumbs, crumbsOf } from "../client/phone/crumbs.ts";
import { countOpenable, createExpansion, filesKey, filesOf, visibleRows, type StorageLike, type TreeRules } from "../client/phone/treeRows.ts";

describe("a folder's crumbs (client/phone/crumbs.ts)", () => {
  it("are the root and every folder down to this one", () => {
    assert.deepEqual(crumbsOf("Maths/History/Egypt", "Notes"), [
      { path: "", label: "Notes" },
      { path: "Maths", label: "Maths" },
      { path: "Maths/History", label: "History" },
      { path: "Maths/History/Egypt", label: "Egypt" },
    ]);
    assert.deepEqual(crumbsOf("", "Notes"), [{ path: "", label: "Notes" }]);
  });

  it("draw everything when it fits", () => {
    assert.deepEqual(collapseCrumbs([60, 90, 110], 400, 16, 44), { items: [0, 1, 2], hidden: [] });
  });

  it("fold the middle first, keeping the root and the nearest folders", () => {
    // 60+16+90+16+80+16+110 = 388 does not fit 300; root + … + the last two
    // = 60+16+44+16+80+16+110 = 342 does not either; root + … + the last one
    // = 60+16+44+16+110 = 246 does.
    assert.deepEqual(collapseCrumbs([60, 90, 80, 110], 300, 16, 44), { items: [0, "more", 3], hidden: [1, 2] });
    assert.deepEqual(collapseCrumbs([60, 90, 80, 110], 350, 16, 44), { items: [0, "more", 2, 3], hidden: [1] });
  });

  it("never fold the current folder: at the narrowest it is … and the folder", () => {
    // A Fold's cover screen: 204px for the crumbs.
    assert.deepEqual(collapseCrumbs([61, 101, 180], 204, 16, 44), { items: ["more", 2], hidden: [0, 1] });
    const plan = collapseCrumbs([61, 101, 400], 100, 16, 44);
    assert.equal(plan.items[plan.items.length - 1], 2, "a name wider than the bar is still drawn — the bar truncates its end");
  });

  it("a single crumb is never folded", () => {
    assert.deepEqual(collapseCrumbs([500], 100, 16, 44), { items: [0], hidden: [] });
  });
});

const note = (path: string): TreeNode => ({ name: path.slice(path.lastIndexOf("/") + 1), path, type: "file" });
const pic = (path: string): TreeNode => ({ ...note(path), attachment: { kind: "image", size: 1 } as TreeNode["attachment"] });
const folder = (path: string, children: TreeNode[]): TreeNode => ({ name: path.slice(path.lastIndexOf("/") + 1), path, type: "folder", children });

const vault = folder("", [
  folder("Maths", [
    folder("Maths/History", [note("Maths/History/Euclid.md"), pic("Maths/History/map.png"), pic("Maths/History/pyramid.png")]),
    note("Maths/Index.md"),
  ]),
  folder("Empty", []),
  note("Welcome.md"),
  pic("cover.png"),
]);

const rules: TreeRules = {
  order: (kids) => [...kids],
  listed: (n) => n.type === "folder" || !n.attachment,
  file: (n) => n.type === "file" && !!n.attachment,
};

function memoryStorage(seed: Record<string, string> = {}): StorageLike & { data: Record<string, string> } {
  const data = { ...seed };
  return { data, getItem: (k) => data[k] ?? null, setItem: (k, v) => void (data[k] = v) };
}

const shape = (rows: ReturnType<typeof visibleRows>): string[] =>
  rows.map((r) => {
    switch (r.kind) {
      case "node":
        return `${"  ".repeat(r.depth)}${r.node.type === "folder" ? (r.open ? "▾ " : "▸ ") : ""}${r.node.name}`;
      case "files":
        return `${"  ".repeat(r.depth)}${r.open ? "▾" : "▸"} Files · ${r.count}`;
      case "file":
        return `${"  ".repeat(r.depth)}${r.node.name}`;
      case "empty":
        return `${"  ".repeat(r.depth)}(empty)`;
    }
  });

describe("the tree's rows (client/phone/treeRows.ts)", () => {
  it("everything folded: the top level, and the root's files as ONE row after the notes", () => {
    assert.deepEqual(shape(visibleRows(vault, () => false, rules)), ["▸ Maths", "▸ Empty", "Welcome.md", "▸ Files · 1"]);
  });

  it("an open folder shows its folders and notes, then its files folded — never a wall of pictures", () => {
    const open = new Set(["Maths", "Maths/History"]);
    assert.deepEqual(shape(visibleRows(vault, (k) => open.has(k), rules)), [
      "▾ Maths",
      "  ▾ History",
      "    Euclid.md",
      "    ▸ Files · 2",
      "  Index.md",
      "▸ Empty",
      "Welcome.md",
      "▸ Files · 1",
    ]);
    open.add(filesKey("Maths/History"));
    const rows = visibleRows(vault, (k) => open.has(k), rules);
    assert.deepEqual(shape(rows).slice(3, 6), ["    ▾ Files · 2", "      map.png", "      pyramid.png"]);
    const file = rows[5];
    assert.ok(file.kind === "file" && file.folder === "Maths/History" && file.index === 1);
    assert.deepEqual(filesOf(vault.children![0].children![0], rules).map((n) => n.name), ["map.png", "pyramid.png"]);
  });

  it("an open folder with nothing in it says so", () => {
    assert.deepEqual(shape(visibleRows(vault, (k) => k === "Empty", rules)).slice(1, 3), ["▾ Empty", "  (empty)"]);
  });

  it("walks only what shows: a closed folder's subtree is never visited", () => {
    let visits = 0;
    const counting: TreeRules = { ...rules, order: (kids) => (visits++, [...kids]) };
    visibleRows(vault, () => false, counting);
    assert.equal(visits, 1, "only the root's children were ordered");
  });

  it("counts what opens, once per node", () => {
    let asked = 0;
    const opens = (n: TreeNode): boolean => (asked++, !n.attachment);
    assert.equal(countOpenable(vault, opens), 3);
    const first = asked;
    assert.equal(countOpenable(vault, opens), 3);
    assert.equal(asked, first, "the second count is remembered");
  });
});

describe("the tree remembers which folders are open, per device", () => {
  it("a toggle is written, and a new page reads it back", () => {
    const storage = memoryStorage();
    const a = createExpansion(storage, "k");
    assert.equal(a.toggle("Maths"), true);
    a.set(filesKey("Maths"), true);
    const b = createExpansion(storage, "k");
    assert.ok(b.isOpen("Maths") && b.isOpen(filesKey("Maths")));
    b.toggle("Maths");
    assert.equal(createExpansion(storage, "k").isOpen("Maths"), false);
  });

  it("a folder that moves takes its open state, and its children's, with it", () => {
    const storage = memoryStorage();
    const e = createExpansion(storage, "k");
    e.set("Maths", true);
    e.set("Maths/History", true);
    e.set(filesKey("Maths/History"), true);
    e.set("Mathsy", true);
    e.remap("Maths", "Mathematics");
    assert.deepEqual(e.keys().sort(), ["Mathematics", "Mathematics/History", filesKey("Mathematics/History"), "Mathsy"].sort());
    assert.ok(createExpansion(storage, "k").isOpen("Mathematics/History"));
  });

  it("unreadable or refusing storage is an empty, working memory", () => {
    assert.deepEqual(createExpansion(memoryStorage({ k: "{not json" }), "k").keys(), []);
    assert.deepEqual(createExpansion(memoryStorage({ k: '{"a":true}' }), "k").keys(), []);
    const refusing: StorageLike = {
      getItem: () => {
        throw new Error("denied");
      },
      setItem: () => {
        throw new Error("full");
      },
    };
    const e = createExpansion(refusing, "k");
    assert.equal(e.toggle("A"), true);
    assert.ok(e.isOpen("A"), "kept for the session");
    assert.ok(createExpansion(null, "k").toggle("A"));
  });
});