eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// 🤖 Tests for the allowlisted CommonMark subset (issue #531).
2// node test/markdown.test.mjs
3//
4// The vectors in test/event-vectors/markdown.json are the cross-platform
5// contract: web's parseMessage must return each vector's tree exactly, and
6// the byte-identical fixture copy keeps the native suite (MarkdownVectorTests)
7// on the same file. When a twin disagrees, fix the implementation — never the
8// vector.
9import assert from "node:assert/strict";
10import { readFileSync } from "node:fs";
11import { dirname, join } from "node:path";
12import { fileURLToPath } from "node:url";
13import { parseMessage, houseParser, plainText } from "../web/static/markdown.js";
14import { replySnippet } from "../web/static/format.js";
15
16const __dirname = dirname(fileURLToPath(import.meta.url));
17
18let failures = 0;
19async function test(name, fn) {
20 try {
21 await fn();
22 console.log("PASS " + name);
23 } catch (e) {
24 console.log("FAIL " + name + " — " + (e && e.message));
25 failures++;
26 }
27}
28
29const canonical = readFileSync(join(__dirname, "event-vectors", "markdown.json"), "utf8");
30const { vectors } = JSON.parse(canonical);
31
32await test("vector file has substantial coverage", () => {
33 assert.ok(vectors.length >= 60, `only ${vectors.length} vectors`);
34});
35
36for (const v of vectors) {
37 await test(`vector: ${v.name}`, () => {
38 assert.deepEqual(parseMessage(v.input), v.tree, v.note || "");
39 // Vectors carrying a `plain` field also pin the plain projection (the
40 // sidebar-preview / notification text) on both platforms.
41 if (v.plain !== undefined) assert.strictEqual(plainText(v.input), v.plain);
42 });
43}
44
45await test("ios vector fixture copy is byte-identical to canonical", () => {
46 const bundled = readFileSync(
47 join(__dirname, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "markdown-vector.json"),
48 "utf8"
49 );
50 assert.strictEqual(
51 bundled,
52 canonical,
53 "run cp test/event-vectors/markdown.json ios/LChatCore/Tests/LChatCoreTests/Fixtures/markdown-vector.json"
54 );
55});
56
57// --- unit edges beyond the vector contract ----------------------------------
58
59await test("null/undefined input parses as empty", () => {
60 assert.deepEqual(parseMessage(null), []);
61 assert.deepEqual(parseMessage(undefined), []);
62});
63
64await test("houseParser returns independent instances", () => {
65 const a = houseParser();
66 const b = houseParser();
67 assert.notEqual(a, b);
68 // Both configured identically: the allowlist holds on a fresh instance.
69 const toks = a.parse("# nope", {});
70 assert.equal(toks[0].type, "paragraph_open");
71});
72
73await test("disallowed constructs parse identically on a fresh houseParser", () => {
74 // Guards against parseMessage's singleton drifting from the exported factory
75 // (the composer builds its ProseMirror MarkdownParser from houseParser()).
76 const fresh = houseParser();
77 for (const src of ["# h", "| a |", "<b>x</b>", "~~s~~", "- [x] t"]) {
78 assert.deepEqual(
79 JSON.stringify(fresh.parse(src, {})),
80 JSON.stringify(houseParser().parse(src, {}))
81 );
82 }
83});
84
85await test("adjacent text runs merge into one node", () => {
86 // Escapes and entities produce separate text_special tokens internally;
87 // the tree must present one merged run.
88 const tree = parseMessage("a \\* b &amp; c");
89 assert.deepEqual(tree, [{ t: "p", children: [{ t: "text", text: "a * b & c" }] }]);
90});
91
92await test("strike delimiters never pair across lengths", () => {
93 assert.deepEqual(parseMessage("~a~~"), [{ t: "p", children: [{ t: "text", text: "~a~~" }] }]);
94});
95
96await test("multi-paragraph message with mixed blocks", () => {
97 const tree = parseMessage("intro\n\n- one\n- two\n\n> bye");
98 assert.deepEqual(
99 tree.map((b) => b.t),
100 ["p", "ul", "quote"]
101 );
102});
103
104await test("deeply nested structure stays well-formed", () => {
105 const tree = parseMessage("> - **a** `c`\n> - [d](https://x.com)");
106 assert.equal(tree[0].t, "quote");
107 assert.equal(tree[0].children[0].t, "ul");
108 const inner = tree[0].children[0].items[0][1];
109 assert.equal(inner.t, "ul");
110 assert.equal(inner.items[0][0].children[0].t, "link");
111});
112
113await test("link hrefs are never non-http(s)", () => {
114 // Belt and braces over the whole vector corpus plus a hostile sample.
115 const walkInline = (nodes) => {
116 for (const n of nodes) {
117 if (n.t === "link") assert.match(n.href, /^https?:\/\//i);
118 if (n.children) walkInline(n.children);
119 }
120 };
121 const walk = (blocks) => {
122 for (const b of blocks) {
123 if (b.children) b.t === "quote" ? walk(b.children) : walkInline(b.children);
124 if (b.items) b.items.forEach(walk);
125 }
126 };
127 for (const v of vectors) walk(v.tree);
128 walk(parseMessage("[a](data:text/html,x) [b](vbscript:x) [c](file:///etc) <ftp://x.com>"));
129});
130
131// The reaction quote a chat-list row shows ("James reacted 👍 to “buy milk”")
132// is stamped at react time as replySnippet(plainText(text)) — the same plain
133// projection every other preview line uses. Pinned here because the stamp
134// itself lives in DOM code (chat.js toggleReaction, ChatFeedModel on native):
135// quoting raw text would show the marks the bubble renders as styling, and the
136// composer's opt-out escapes as visible backslashes.
137await test("a reaction quote is stamped through the plain projection", () => {
138 assert.equal(replySnippet({ text: plainText("**buy** milk") }), "buy milk");
139 assert.equal(replySnippet({ text: plainText("\\*lit\\*") }), "*lit*");
140 // No text (a photo someone reacted to) still labels itself.
141 assert.equal(replySnippet({ text: plainText(""), attachments: [{ type: "image/png" }] }), "📷 Photo");
142});
143
144if (failures) {
145 console.error(`\nmarkdown: ${failures} test(s) failed`);
146 process.exit(1);
147}
148console.log("\nmarkdown: all tests passed");
149