| 1 | // 🤖 The composer serialization contract (issue #890): the vectors in |
| 2 | // test/event-vectors/composer-serialize.json pin the wire bytes the WEB |
| 3 | // serializer (composer-md.js, the authority) emits for each composer doc |
| 4 | // shape, so the native twin (ComposerSerialize.swift, run by |
| 5 | // ComposerSerializeTests over the byte-identical fixture) can never drift. |
| 6 | // node test/composer-vectors.test.mjs |
| 7 | import assert from "node:assert/strict"; |
| 8 | import { readFileSync } from "node:fs"; |
| 9 | import { dirname, join } from "node:path"; |
| 10 | import { fileURLToPath } from "node:url"; |
| 11 | import { composerSchema as schema, serializeDoc } from "../web/static/composer-md.js"; |
| 12 | import { parseMessage } from "../web/static/markdown.js"; |
| 13 | |
| 14 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 15 | |
| 16 | let failures = 0; |
| 17 | async function test(name, fn) { |
| 18 | try { |
| 19 | await fn(); |
| 20 | console.log("PASS " + name); |
| 21 | } catch (e) { |
| 22 | console.log("FAIL " + name + " — " + (e && e.message)); |
| 23 | failures++; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | // The tree vocabulary (test/event-vectors/markdown.json's) → a ProseMirror |
| 28 | // doc over the composer schema. Inline nesting becomes mark arrays; a br |
| 29 | // inside a styled wrapper carries the wrapper's marks (the marked-break case). |
| 30 | function inlineNodes(children, marks = []) { |
| 31 | const out = []; |
| 32 | for (const n of children) { |
| 33 | if (n.t === "text") out.push(schema.text(n.text, marks)); |
| 34 | else if (n.t === "br") out.push(schema.nodes.hard_break.create(null, null, marks)); |
| 35 | else if (n.t === "code") out.push(schema.text(n.text, [...marks, schema.marks.code.create()])); |
| 36 | else if (n.t === "strong" || n.t === "em") out.push(...inlineNodes(n.children, [...marks, schema.marks[n.t].create()])); |
| 37 | else if (n.t === "strike") out.push(...inlineNodes(n.children, [...marks, schema.marks.strike.create()])); |
| 38 | else if (n.t === "link") out.push(...inlineNodes(n.children, [...marks, schema.marks.link.create({ href: n.href })])); |
| 39 | else throw new Error("unknown inline " + n.t); |
| 40 | } |
| 41 | return out; |
| 42 | } |
| 43 | function blockNode(b) { |
| 44 | if (b.t === "p") return schema.nodes.paragraph.create(null, inlineNodes(b.children)); |
| 45 | if (b.t === "quote") return schema.nodes.blockquote.create(null, b.children.map(blockNode)); |
| 46 | if (b.t === "ul") return schema.nodes.bullet_list.create(null, b.items.map((item) => schema.nodes.list_item.create(null, item.map(blockNode)))); |
| 47 | if (b.t === "ol") return schema.nodes.ordered_list.create({ order: b.start }, b.items.map((item) => schema.nodes.list_item.create(null, item.map(blockNode)))); |
| 48 | if (b.t === "codeblock") return schema.nodes.code_block.create({ params: b.lang }, b.text ? [schema.text(b.text)] : []); |
| 49 | throw new Error("unknown block " + b.t); |
| 50 | } |
| 51 | export function docFromTree(tree) { |
| 52 | return schema.nodes.doc.create(null, tree.map(blockNode)); |
| 53 | } |
| 54 | |
| 55 | const canonical = readFileSync(join(__dirname, "event-vectors", "composer-serialize.json"), "utf8"); |
| 56 | const { vectors } = JSON.parse(canonical); |
| 57 | |
| 58 | await test("vector file has substantial coverage", () => { |
| 59 | assert.ok(vectors.length >= 40, `only ${vectors.length} vectors`); |
| 60 | }); |
| 61 | |
| 62 | for (const v of vectors) { |
| 63 | await test(`serialize: ${v.name}`, () => { |
| 64 | assert.equal(serializeDoc(docFromTree(v.tree)), v.md, v.note || ""); |
| 65 | }); |
| 66 | await test(`reparse: ${v.name}`, () => { |
| 67 | assert.deepEqual(parseMessage(v.md), v.reparse ?? v.tree); |
| 68 | }); |
| 69 | } |
| 70 | |
| 71 | await test("ios fixture copy is byte-identical to canonical", () => { |
| 72 | const bundled = readFileSync( |
| 73 | join(__dirname, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "composer-serialize-vector.json"), |
| 74 | "utf8" |
| 75 | ); |
| 76 | assert.strictEqual( |
| 77 | bundled, |
| 78 | canonical, |
| 79 | "run cp test/event-vectors/composer-serialize.json ios/LChatCore/Tests/LChatCoreTests/Fixtures/composer-serialize-vector.json" |
| 80 | ); |
| 81 | }); |
| 82 | |
| 83 | if (failures) { |
| 84 | console.error(`\ncomposer-vectors: ${failures} test(s) failed`); |
| 85 | process.exit(1); |
| 86 | } |
| 87 | console.log("\ncomposer-vectors: all tests passed"); |
| 88 | |