Ctrl K
ComunidadGuías…

tests/folderIcons.test.ts

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

Archivo original ↗Preguntar a KiBot
// The folder glyph set (shared/folderIcons.ts).
//
// A pure module with a pure test: no vault, no server, no DOM. What it guards
// is the one failure mode a closed icon vocabulary has — the enum and the
// drawing table falling out of step. Add a name to `FolderIcon` and forget the
// paths and the picker shows a blank cell; add paths under a name the union
// does not have and the glyph is unreachable from every UI in the product.
// Neither shows up in a typecheck of the DRAWINGS, because a path table keyed
// by the union is satisfied by a table that also has extra keys.

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  cleanFolderIcons,
  FOLDER_ICONS,
  FOLDER_ICONS_MAX,
  folderIconKey,
  folderImagePaths,
  isFolderIcon,
  isFolderImage,
  isFolderMark,
} from "../shared/folderIcons.ts";
import { FOLDER_ICON_ENTRIES, FOLDER_ICON_GROUPS } from "../shared/folderIconCatalog.ts";
import { FOLDER_ICON_HAND_PATHS } from "../shared/folderIconsHand.ts";
import { FOLDER_ICON_KEYS, FOLDER_ICON_PATHS } from "../shared/folderIconPaths.ts";

/** Every point a `d` string actually lands on, absolute. Enough SVG path
 *  grammar to walk these twenty glyphs and no more: the commands they use,
 *  each one's argument arity, and the rule that a repeated argument group
 *  repeats the command (an implicit `L` after an `M`). Curve CONTROL points
 *  are walked too — a control point outside the box drags the curve out with
 *  it, which is the whole thing being checked. */
function vertices(d: string): [number, number][] {
  const ARITY: Record<string, number> = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0 };
  const tokens = d.match(/[MmLlHhVvCcSsQqTtAaZz]|-?\d*\.?\d+/g) ?? [];
  const out: [number, number][] = [];
  let x = 0;
  let y = 0;
  let startX = 0;
  let startY = 0;
  let cmd = "M";
  let i = 0;
  while (i < tokens.length) {
    if (/[A-Za-z]/.test(tokens[i])) cmd = tokens[i++];
    const up = cmd.toUpperCase();
    const rel = cmd !== up;
    const n = ARITY[up];
    if (n === undefined) throw new Error(`unknown path command "${cmd}" in ${d}`);
    const args = tokens.slice(i, i + n).map(Number);
    i += n;
    if (up === "Z") {
      x = startX;
      y = startY;
    } else if (up === "H") {
      x = rel ? x + args[0] : args[0];
    } else if (up === "V") {
      y = rel ? y + args[0] : args[0];
    } else if (up === "A") {
      // rx ry rot large sweep dx dy — only the endpoint is a position. The
      // radii are bounded by the endpoint distance, so an arc cannot bulge
      // meaningfully past a box both of its ends sit inside.
      x = rel ? x + args[5] : args[5];
      y = rel ? y + args[6] : args[6];
    } else {
      // M/L/T take one point; C/S/Q take control points then the endpoint.
      for (let k = 0; k + 1 < n; k += 2) {
        const px = rel ? x + args[k] : args[k];
        const py = rel ? y + args[k + 1] : args[k + 1];
        if (k + 2 >= n) {
          x = px;
          y = py;
        } else {
          out.push([px, py]);
        }
      }
    }
    out.push([x, y]);
    if (up === "M") {
      startX = x;
      startY = y;
      // A repeated argument group after M is an implicit lineto.
      cmd = rel ? "l" : "L";
    }
  }
  return out;
}

describe("the folder glyph set", () => {
  it("lists a few hundred glyphs, the twenty originals first, with no duplicates", () => {
    assert.ok(FOLDER_ICONS.length >= 250, `only ${FOLDER_ICONS.length} glyphs`);
    assert.equal(new Set(FOLDER_ICONS).size, FOLDER_ICONS.length);
    // The originals keep their names: a 2.6 settings.json must still draw.
    for (const original of Object.keys(FOLDER_ICON_HAND_PATHS)) {
      assert.ok(isFolderIcon(original), `${original} left the set`);
      assert.deepEqual(FOLDER_ICON_PATHS[original], FOLDER_ICON_HAND_PATHS[original]);
    }
    assert.equal(Object.keys(FOLDER_ICON_HAND_PATHS).length, 20);
  });

  it("is generated from the catalog, and the catalog names every glyph in both languages", () => {
    assert.deepEqual(
      [...FOLDER_ICONS],
      FOLDER_ICON_ENTRIES.map((e) => e.name),
    );
    for (const entry of FOLDER_ICON_ENTRIES) {
      assert.match(entry.name, /^[a-z][a-z0-9-]*$/, `${entry.name} is not a kebab name`);
      assert.ok(entry.en.trim().length > 0, `${entry.name} has no English name`);
      assert.match(entry.ar, /[\u0600-\u06FF]/, `${entry.name} has no Arabic name`);
      assert.equal(typeof FOLDER_ICON_KEYS[entry.name], "string");
    }
    assert.equal(FOLDER_ICON_GROUPS.length, 9);
  });

  it("keeps the enum and the path table in step, both directions", () => {
    assert.deepEqual([...FOLDER_ICONS].sort(), Object.keys(FOLDER_ICON_PATHS).sort());
  });

  it("gives every glyph at least one path, and every path real data", () => {
    for (const icon of FOLDER_ICONS) {
      const paths = FOLDER_ICON_PATHS[icon];
      assert.ok(Array.isArray(paths) && paths.length > 0, `${icon} has no paths`);
      for (const d of paths) {
        assert.ok(typeof d === "string" && d.length > 0, `${icon} has an empty path`);
        // Every `d` starts with an absolute moveto. A path that opens with a
        // relative command is positioned by whatever the renderer's current
        // point happens to be, which for a fresh <path> is the origin — it
        // draws, it just draws in the wrong corner.
        assert.match(d, /^M/, `${icon} path does not start with an absolute moveto: ${d}`);
        // Nothing but path grammar: numbers, commands, separators. This is the
        // one place a stray letter (a copy-pasted `stroke="…"`, an `NaN` from a
        // generator) would otherwise reach an <svg> unnoticed.
        assert.match(d, /^[MmLlHhVvCcSsQqTtAaZz0-9,.\s-]+$/, `${icon} path has stray characters: ${d}`);
      }
    }
  });

  it("draws inside the 24×24 grid it declares", () => {
    // Coordinates are not clamped anywhere, so a glyph that strays outside the
    // viewBox is simply CROPPED — and at 14px a cropped stroke reads as a
    // different drawing rather than as a bug. Checked over the VERTEX walk,
    // not over the raw numbers: half these commands are relative, and `-3.8`
    // inside `a3.8 3.8 0 0 1 3.8-3.8` is a delta, not a position.
    for (const icon of FOLDER_ICONS) {
      for (const d of FOLDER_ICON_PATHS[icon]) {
        for (const [x, y] of vertices(d)) {
          const inside = x >= -0.5 && x <= 24.5 && y >= -0.5 && y <= 24.5;
          assert.ok(inside, `${icon} leaves the grid at ${x},${y} in ${d}`);
        }
      }
    }
  });

  it("…and the walk that checks that is not vacuous", () => {
    // A gate nobody has watched FAIL is a gate nobody has. Absolute and
    // relative forms of the same square, then one that walks off the grid.
    assert.deepEqual(vertices("M2 2h20v20H2z"), [
      [2, 2],
      [22, 2],
      [22, 22],
      [2, 22],
      [2, 2],
    ]);
    assert.deepEqual(vertices("M2 2 6 4c1 1 2 2 3 3"), [
      [2, 2],
      [6, 4],
      [7, 5],
      [8, 6],
      [9, 7],
    ]);
    assert.ok(
      vertices("M2 2a3 3 0 0 1 26-1").some(([x]) => x > 24.5),
      "a path that leaves the box was walked as if it stayed inside",
    );
  });

  it("recognizes its own members and nothing else", () => {
    for (const icon of FOLDER_ICONS) assert.equal(isFolderIcon(icon), true, icon);
    for (const other of ["", "bookshelf", "Book", "toString", "__proto__", 3, null, undefined, {}]) {
      assert.equal(isFolderIcon(other), false, String(other));
    }
  });
});

describe("folderIconKey", () => {
  it("normalizes a key to the path the tree uses", () => {
    assert.equal(folderIconKey(" notes/games/ "), "notes/games");
    assert.equal(folderIconKey("Reading"), "Reading");
  });

  it("refuses anything that cannot be a vault-relative folder", () => {
    // `/Reading` and `C:/vault` are REFUSED, not rewritten: a key silently
    // turned into a different folder from the one it names is the bug
    // vaultRel()'s comment was written about.
    for (const bad of [
      "",
      "   ",
      "/Reading",
      "..",
      "a/../b",
      "a//b",
      "./a",
      "C:/vault",
      "notes\\games",
      7,
      null,
      undefined,
    ]) {
      assert.equal(folderIconKey(bad), null, JSON.stringify(bad));
    }
  });
});

describe("cleanFolderIcons", () => {
  it("keeps the readable rows and drops the rest in silence", () => {
    assert.deepEqual(
      cleanFolderIcons({
        Reading: "book",
        Games: "bookshelf", // a glyph this build cannot draw
        "../escape": "leaf", // a key that cannot be a folder
        "Films/": "film", // a key that only needs tidying
        Broken: 42,
      }),
      { Reading: "book", "Films": "film" },
    );
  });

  it("is total over garbage — it never throws", () => {
    for (const bad of [null, undefined, 7, "book", [], [["a", "book"]]]) {
      assert.deepEqual(cleanFolderIcons(bad), {});
    }
  });

  it("stops at the cap rather than letting a hand-edited file grow forever", () => {
    const big: Record<string, string> = {};
    for (let i = 0; i < FOLDER_ICONS_MAX + 50; i++) big[`f${i}`] = "star";
    assert.equal(Object.keys(cleanFolderIcons(big)).length, FOLDER_ICONS_MAX);
  });

  it("cannot be used to reach Object.prototype", () => {
    // The keys are vault paths from a JSON file, so `__proto__` arrives as an
    // OWN property and `out[key] = …` on a plain object would invoke the
    // setter instead of defining anything.
    const cleaned = cleanFolderIcons(JSON.parse('{"__proto__": "book", "Reading": "leaf"}'));
    assert.equal(({} as Record<string, unknown>).polluted, undefined);
    assert.equal(Object.getPrototypeOf({}), Object.prototype);
    assert.equal(cleaned.Reading, "leaf");
  });
});

describe("an image as a mark", () => {
  it("accepts a vault image path and refuses the rest", () => {
    assert.equal(isFolderImage("attachments/icons/rocket.svg"), true);
    assert.equal(isFolderImage("Icons/Mark.PNG"), true);
    assert.equal(isFolderImage("/etc/x.svg"), false);
    assert.equal(isFolderImage("a/../b.png"), false);
    assert.equal(isFolderImage("notes/x.md"), false);
    assert.equal(isFolderImage("book"), false);
    assert.equal(isFolderMark("book"), true);
    assert.equal(isFolderMark("attachments/a.webp"), true);
  });
  it("keeps image marks through the read-side cleaner and lists them for the file gate", () => {
    const map = cleanFolderIcons({ games: "gamepad", art: "attachments/icons/palette.svg", bad: "nope.txt" });
    assert.deepEqual(map, { games: "gamepad", art: "attachments/icons/palette.svg" });
    assert.deepEqual(folderImagePaths(map), ["attachments/icons/palette.svg"]);
  });
});