Ctrl K
ComunidadGuías…

tests/publicFolders.test.ts

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

Archivo original ↗Preguntar a KiBot
// PUBLIC FOLDERS — membership (frontmatter `folders:`) and the shapes that
// carry it (shared/publicFolders.ts).
//
// The feature's whole promise is that an author declares a folder ONCE, in
// settings, and then joins notes to it by typing one line in the note. That
// line is YAML, and YAML gives three different values for what an author reads
// as one list — the same trap `aliases:` fell into and the reason parseAliases
// is tolerant. So the cases below are driven through the real indexer and read
// off PostMeta, which is what the blog actually filters on: a test over the
// parser alone would pass while the field never reached the wire.

import assert from "node:assert/strict";
import { after, before, describe, it } from "node:test";
import { initIndexer, posts as postsRaw, publicFolderCounts } from "../server/indexer.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import type { PostMeta } from "../shared/types.ts";
import {
  PUBLIC_FOLDERS_MAX,
  cleanPublicFolder,
  collectionsForPath,
  effectiveFolders,
  folderSlug,
  suggestSlug,
  vaultFolderPath,
} from "../shared/publicFolders.ts";
import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts";

const files: Record<string, string> = {
  // The three spellings of one list, plus the singular key.
  "Flow.md": note({ publish: "true", folders: "[games, books]" }, "# Flow\n\nProse.\n"),
  "Block.md": `---\npublish: true\nfolders:\n  - games\n  - films\n---\n\n# Block\n\nProse.\n`,
  "Comma.md": note({ publish: "true", folders: "games, books" }, "# Comma\n\nProse.\n"),
  "Scalar.md": note({ publish: "true", folder: "books" }, "# Scalar\n\nProse.\n"),
  // Slugs are normalized on the way in: case, stray whitespace, a trailing
  // slash and a pasted `/folder/` prefix all mean the folder they name.
  "Sloppy.md": note({ publish: "true", folders: "[ Games , /folder/books/ ]" }, "# Sloppy\n\nProse.\n"),
  // Nothing usable: an empty value, a space inside, a traversal.
  "Junk.md": note({ publish: "true", folders: "[\"a b\", \"..\", \"\"]" }, "# Junk\n\nProse.\n"),
  "Bare.md": note({ publish: "true" }, "# Bare\n\nProse.\n"),
  // A .tex note joins from its COMMENT block — the readNoteFrontmatter path.
  "Paper.tex": "%---\n% publish: true\n% folders: [games, papers]\n%---%\n\\section{Paper}\nProse.\n",
  // Unpublished: a member of `games` that no visitor may count.
  "Draft.md": note({ folders: "games" }, "# Draft\n\nProse.\n"),
};

const data = makeDir();
const root = makeVault(files);
let byPath: Map<string, PostMeta>;

before(async () => {
  initSite({ ASTROLABE_DATA: data });
  initVault(root);
  await initIndexer();
  byPath = new Map(postsRaw(false, null).map((p) => [p.path, p]));
});

after(() => {
  removeVault(root);
  removeVault(data);
});

const folders = (path: string): string[] | undefined => {
  const meta = byPath.get(path);
  assert.ok(meta, `${path} is not in posts()`);
  return meta.folders;
};

describe("frontmatter folders:", () => {
  it("reads a flow list, a block list, a comma scalar and a bare scalar", () => {
    assert.deepEqual(folders("Flow.md"), ["games", "books"]);
    assert.deepEqual(folders("Block.md"), ["games", "films"]);
    assert.deepEqual(folders("Comma.md"), ["games", "books"]);
    // `folder:` (singular) is read too — one folder is the common case.
    assert.deepEqual(folders("Scalar.md"), ["books"]);
  });

  it("normalizes case, padding and a pasted /folder/ address", () => {
    assert.deepEqual(folders("Sloppy.md"), ["games", "books"]);
  });

  it("drops values that cannot be a slug, and keeps the key off a note with none", () => {
    assert.equal(folders("Junk.md"), undefined, "an unusable list became a membership");
    assert.equal(folders("Bare.md"), undefined, "a note with no key carries the field");
  });

  it("reads a .tex note's comment block, like every other frontmatter key", () => {
    assert.deepEqual(folders("Paper.tex"), ["games", "papers"]);
  });
});

describe("folder counts", () => {
  it("counts only PUBLISHED notes, and only the slugs asked for", () => {
    const counts = publicFolderCounts(["games", "books", "nobody"], false, null);
    // Flow, Block, Comma, Sloppy, Paper — Draft.md is unpublished.
    assert.equal(counts.get("games"), 5);
    assert.equal(counts.get("books"), 4);
    // A folder nobody has joined is zero, not absent: an empty collection is a
    // real card on the home band.
    assert.equal(counts.get("nobody"), 0);
    // `films` was never asked for, so it was never counted.
    assert.equal(counts.get("films"), undefined);
  });
});

describe("the slug rule", () => {
  it("accepts the set the URL and the frontmatter share, and nothing else", () => {
    assert.equal(folderSlug("games"), "games");
    assert.equal(folderSlug("  Long-Reads "), "long-reads");
    assert.equal(folderSlug("2026"), "2026");
    for (const bad of ["-leading", "a b", "..", "a/b", "", "أ", "x".repeat(61), 7, null]) {
      assert.equal(folderSlug(bad), null, `accepted ${JSON.stringify(bad)}`);
    }
  });

  it("suggests a slug from a title, and suggests NOTHING when it cannot", () => {
    assert.equal(suggestSlug("Long Reads"), "long-reads");
    assert.equal(suggestSlug("Games & Play!"), "games-play");
    // An Arabic title leaves the field empty rather than filling it with
    // hyphens — the editor then waits for a slug instead of inventing one.
    assert.equal(suggestSlug("ألعاب"), "");
  });
});

describe("one stored row", () => {
  const id = (): string => "fixed";

  it("keeps what it can use and refuses what it cannot", () => {
    const good = cleanPublicFolder(
      { id: "abc", slug: "Games", title: "  Games  ", icon: "gamepad", description: " Play " },
      id,
    );
    assert.deepEqual(good, { id: "abc", slug: "games", title: "Games", icon: "gamepad", description: "Play" });
    // A row with no id gets one — the list is React keys and reorder.
    assert.equal(cleanPublicFolder({ slug: "a", title: "A", icon: "book" }, id)?.id, "fixed");
    for (const bad of [
      { slug: "a", title: "A", icon: "bookshelf" }, // not in the closed set
      { slug: "a b", title: "A", icon: "book" },
      { slug: "a", title: "", icon: "book" },
      { slug: "a", title: "A", icon: "book", description: "x".repeat(201) },
      null,
      "games",
    ]) {
      assert.equal(cleanPublicFolder(bad, id), null, `accepted ${JSON.stringify(bad)}`);
    }
  });

  it("carries `hidden` only when it is exactly true", () => {
    assert.equal(cleanPublicFolder({ slug: "a", title: "A", icon: "book", hidden: true }, id)?.hidden, true);
    assert.equal(
      cleanPublicFolder({ slug: "a", title: "A", icon: "book", hidden: "yes" }, id)?.hidden,
      undefined,
      "a truthy non-boolean took a folder off the site",
    );
  });

  it("caps the list at a number the nav row can actually carry", () => {
    assert.equal(PUBLIC_FOLDERS_MAX, 12);
  });
});

describe("folder-backed collections", () => {
  it("normalises a vault folder and refuses what cannot be one", () => {
    assert.equal(vaultFolderPath(" Books/Feynman/ "), "Books/Feynman");
    assert.equal(vaultFolderPath("Books\\Feynman"), "Books/Feynman");
    assert.equal(vaultFolderPath("/"), null);
    assert.equal(vaultFolderPath("a/../b"), null);
    assert.equal(vaultFolderPath(""), null);
    assert.equal(vaultFolderPath(42), null);
  });
  it("keeps a row's folder, dropping one that cannot name a folder", () => {
    const row = cleanPublicFolder({ slug: "games", title: "Games", icon: "gamepad", folder: "Play/Games/" }, () => "x");
    assert.equal(row?.folder, "Play/Games");
    const bare = cleanPublicFolder({ slug: "games", title: "Games", icon: "gamepad", folder: "../x" }, () => "x");
    assert.equal(bare?.folder, undefined);
  });
  it("puts a note in every collection whose folder holds it, boundary at the slash", () => {
    const rows = [
      { id: "a", slug: "games", title: "G", icon: "gamepad" as const, folder: "Play/Games" },
      { id: "b", slug: "play", title: "P", icon: "dice" as const, folder: "Play" },
      { id: "c", slug: "books", title: "B", icon: "book" as const },
    ];
    assert.deepEqual(collectionsForPath("Play/Games/elden.md", rows), ["games", "play"]);
    assert.deepEqual(collectionsForPath("Playground/x.md", rows), []);
    assert.deepEqual(effectiveFolders(["books", "games"], "Play/Games/elden.md", rows), ["books", "games", "play"]);
  });
});

describe("collections that are tags", () => {
  it("count a note that carries the tag as a member, and keep a row's tag", () => {
    const rows = [{ id: "t", slug: "games", title: "Games", icon: "gamepad" as const, tag: "games" }];
    assert.deepEqual(effectiveFolders([], "x/y.md", rows, ["life", "games"]), ["games"]);
    assert.deepEqual(effectiveFolders([], "x/y.md", rows, ["life"]), []);
    const row = cleanPublicFolder({ slug: "games", title: "Games", icon: "gamepad", tag: " #Games " }, () => "x");
    assert.equal(row?.tag, "games");
  });
});