Ctrl K
ComunidadGuías…

tests/capture.test.ts

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

Archivo original ↗Preguntar a KiBot
// Capture's text arithmetic (shared/capture.ts) and the manifest that puts
// the site in a phone's share sheet (shared/manifest.ts).

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  appendCaptured,
  bookmarklet,
  clipFileName,
  clipNote,
  isClippableUrl,
  splitSharedText,
  yamlQuote,
} from "../shared/capture.ts";
import { buildManifest, themeSwatch, DEFAULT_SWATCH, MANIFEST_ICON_PATH } from "../shared/manifest.ts";

describe("appendCaptured", () => {
  it("starts the section at the end of a note that has none", () => {
    assert.equal(appendCaptured("", "a thought", "09:15"), "## Captured\n\n- 09:15 a thought\n");
    assert.equal(appendCaptured("# Day\n\nprose\n", "x", "09:15"), "# Day\n\nprose\n\n## Captured\n\n- 09:15 x\n");
    // Frontmatter-only notes are a body too.
    assert.equal(appendCaptured("---\ntags: [d]\n---\n", "x", "09:15"), "---\ntags: [d]\n---\n\n## Captured\n\n- 09:15 x\n");
  });

  it("appends to an existing section and leaves the rest of the note alone", () => {
    const note = "# Day\n\n## Captured\n\n- 08:00 first\n\n## Later\n\nmore\n";
    assert.equal(appendCaptured(note, "second", "09:15"), "# Day\n\n## Captured\n\n- 08:00 first\n- 09:15 second\n\n## Later\n\nmore\n");
    // A `###` under it is inside the section; the item goes after it.
    const nested = "## Captured\n\n- a\n\n### sub\n\n- b\n";
    assert.equal(appendCaptured(nested, "c", "10:00"), "## Captured\n\n- a\n\n### sub\n\n- b\n- 10:00 c\n");
    // An empty section gets its blank line before the first item.
    assert.equal(appendCaptured("## Captured\n## Next\n", "x", "10:00"), "## Captured\n\n- 10:00 x\n## Next\n");
  });

  it("hangs a multi-line thought under its bullet and follows the note's line endings", () => {
    assert.equal(appendCaptured("## Captured\n\n- a\n", "one\ntwo\n\nthree", "10:00"), "## Captured\n\n- a\n- 10:00 one\n  two\n\n  three\n");
    assert.equal(appendCaptured("# D\r\n\r\n## Captured\r\n\r\n- a\r\n", "b", "10:00"), "# D\r\n\r\n## Captured\r\n\r\n- a\r\n- 10:00 b\r\n");
  });

  it("matches the heading loosely and the section end strictly", () => {
    assert.equal(appendCaptured("##   captured  \n- a\n", "b", "10:00"), "##   captured  \n- a\n- 10:00 b\n");
    // A `# Top` heading ends it too; a `####` does not.
    assert.equal(appendCaptured("## Captured\n- a\n# Top\n", "b", "10:00"), "## Captured\n- a\n- 10:00 b\n# Top\n");
  });
});

describe("clips", () => {
  it("names the file from the title, within what a filesystem and a wikilink allow", () => {
    assert.equal(clipFileName("A / B: the [best]? #1 | \"quoted\""), "A B the best 1 quoted.md");
    assert.equal(clipFileName("   "), "Clip.md");
    assert.equal(clipFileName("...dots..."), "dots.md");
    assert.equal(clipFileName("x".repeat(200)).length, 123);
    assert.equal(clipFileName("ctl\u0000char"), "ctl char.md");
    // A title that would spell a dotfile once its slashes are spaces: the
    // tree never lists a name that begins with a dot, so the clip must not.
    assert.equal(clipFileName("../../etc/passwd"), "etc passwd.md");
    assert.equal(clipFileName(". . hidden ."), "hidden.md");
    assert.equal(clipFileName(`${"a".repeat(119)} .`), `${"a".repeat(119)}.md`);
  });

  it("writes the source and the day as frontmatter, the title as the heading", () => {
    assert.equal(
      clipNote({ title: "  A  \"page\" ", url: "https://ex.org/a?b=c", date: "2026-09-15", body: "\nBody.\n" }),
      '---\nsource: "https://ex.org/a?b=c"\nclipped: 2026-09-15\n---\n\n# A "page"\n\nBody.\n',
    );
    assert.equal(clipNote({ title: "T", url: null, date: "2026-09-15", body: "" }), "---\nclipped: 2026-09-15\n---\n\n# T\n");
    // The page's own H1, when it is the title, is not printed twice.
    assert.equal(clipNote({ title: "Whole", url: null, date: "2026-09-15", body: "# whole\n\nBody." }), "---\nclipped: 2026-09-15\n---\n\n# Whole\n\nBody.\n");
    assert.equal(clipNote({ title: "Whole", url: null, date: "2026-09-15", body: "# Other\n\nBody." }), "---\nclipped: 2026-09-15\n---\n\n# Whole\n\n# Other\n\nBody.\n");
    assert.equal(yamlQuote('a "b" \\ c\nd'), '"a \\"b\\" \\\\ c d"');
  });

  it("finds the one address in what a phone shares", () => {
    assert.deepEqual(splitSharedText("https://ex.org/p"), { url: "https://ex.org/p", rest: "" });
    assert.deepEqual(splitSharedText("Great read: https://ex.org/p."), { url: "https://ex.org/p", rest: "Great read:" });
    assert.deepEqual(splitSharedText("just a thought"), { url: null, rest: "just a thought" });
    assert.equal(isClippableUrl("https://ex.org"), true);
    assert.equal(isClippableUrl("ftp://ex.org"), false);
    assert.equal(isClippableUrl("javascript:alert(1)"), false);
    assert.equal(isClippableUrl("not a url"), false);
  });
});

describe("the manifest", () => {
  it("names the site, colours it like its theme, and declares the share target", () => {
    const m = buildManifest({ name: "My Reading Room", lang: "ar", background: "#16130e", theme: "#c9a227" });
    assert.equal(m.name, "My Reading Room");
    assert.equal(m.short_name, "My Reading");
    assert.equal(m.dir, "rtl");
    assert.equal(m.lang, "ar");
    assert.equal(m.display, "standalone");
    assert.equal(m.background_color, "#16130e");
    assert.equal(m.theme_color, "#c9a227");
    assert.deepEqual(m.share_target, {
      action: "/api/clip",
      method: "POST",
      enctype: "application/x-www-form-urlencoded",
      params: { title: "title", text: "text", url: "url" },
    });
    const icons = m.icons as { src: string }[];
    assert.equal(icons[icons.length - 1].src, MANIFEST_ICON_PATH);
    // A configured favicon goes first.
    const withIcon = buildManifest({ name: "X", lang: "en", background: "#000", theme: "#fff", icons: [{ src: "/favicon.ico", type: "image/png" }] });
    assert.equal((withIcon.icons as { src: string }[])[0].src, "/favicon.ico");
    assert.equal(buildManifest({ name: "  ", lang: "en", background: "#000", theme: "#fff" }).name, "Astrolabe");
  });

  it("reads a theme's swatch out of tokens.css and refuses what is not there", () => {
    const css = ":root {\n  --swatch-iron-gall-bg: #16130E;\n  --swatch-iron-gall-text: #eae2d0;\n  --swatch-iron-gall-accent: #c9a227;\n}";
    assert.deepEqual(themeSwatch(css, "iron-gall"), { bg: "#16130e", accent: "#c9a227" });
    assert.equal(themeSwatch(css, "void"), null);
    assert.equal(themeSwatch(css, "custom:mine"), null);
    assert.equal(themeSwatch(css, ".*"), null);
    assert.deepEqual(DEFAULT_SWATCH, { bg: "#0f1113", accent: "#d4a942" });
  });
});

describe("the bookmarklet", () => {
  it("is one javascript: URL that carries the token, the origin and the words, and nothing unquoted", () => {
    const words = { clipped: "Clipped: {path}", failed: "Clip failed" };
    const url = bookmarklet("https://notes.example", "abc123", words);
    assert.ok(url.startsWith("javascript:"));
    assert.equal(url.includes("\n"), false);
    const src = decodeURIComponent(url.slice("javascript:".length));
    assert.ok(src.includes('"abc123"'));
    assert.ok(src.includes('"https://notes.example/api/clip"'));
    assert.ok(src.includes('"Clipped: {path}"'));
    // A word with a quote in it stays inside its string.
    const hostile = bookmarklet("https://x", "t", { clipped: 'say "hi"', failed: "it's" });
    const hostileSrc = decodeURIComponent(hostile.slice("javascript:".length));
    assert.ok(hostileSrc.includes('"say \\"hi\\""'));
    assert.ok(hostileSrc.includes('"it\'s"'));
    // And the source parses as JavaScript.
    assert.doesNotThrow(() => new Function(src));
  });
});