tests/capture.test.ts
Rama main · réplica de lectura del repositorio original.
// 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));
});
});