// Unit tests for utils/markdownToAdf.js — uses Node's built-in test // runner (`node --test tests/markdownToAdf.test.js`) so we don't need // a separate test framework in package.json for this scope. import test from 'node:test'; import assert from 'node:assert/strict'; import { markdownToAdf, markdownToAdfContent, tokenizeLine, } from '../utils/markdownToAdf.js'; test('empty string produces an empty doc', () => { assert.deepEqual(markdownToAdf(''), { version: 1, type: 'doc', content: [] }); assert.deepEqual(markdownToAdfContent(''), []); }); test('whitespace-only input produces an empty doc', () => { assert.deepEqual(markdownToAdfContent(' \n\n '), []); }); test('non-string input is treated as empty', () => { assert.deepEqual(markdownToAdfContent(null), []); assert.deepEqual(markdownToAdfContent(undefined), []); assert.deepEqual(markdownToAdfContent(42), []); }); test('plain single-paragraph text becomes one paragraph', () => { const out = markdownToAdfContent('hello world'); assert.deepEqual(out, [ { type: 'paragraph', content: [{ type: 'text', text: 'hello world' }] }, ]); }); test('bold: **text**', () => { assert.deepEqual(tokenizeLine('a **bold** b'), [ { type: 'text', text: 'a ' }, { type: 'text', text: 'bold', marks: [{ type: 'strong' }] }, { type: 'text', text: ' b' }, ]); }); test('italic: *text*', () => { assert.deepEqual(tokenizeLine('a *italic* b'), [ { type: 'text', text: 'a ' }, { type: 'text', text: 'italic', marks: [{ type: 'em' }] }, { type: 'text', text: ' b' }, ]); }); test('link: [label](url)', () => { assert.deepEqual(tokenizeLine('see [here](https://x.y)'), [ { type: 'text', text: 'see ' }, { type: 'text', text: 'here', marks: [{ type: 'link', attrs: { href: 'https://x.y' } }], }, ]); }); test('all three marks combined in one line', () => { const out = tokenizeLine('**bold** and *em* and [click](https://x.y) done'); assert.deepEqual(out, [ { type: 'text', text: 'bold', marks: [{ type: 'strong' }] }, { type: 'text', text: ' and ' }, { type: 'text', text: 'em', marks: [{ type: 'em' }] }, { type: 'text', text: ' and ' }, { type: 'text', text: 'click', marks: [{ type: 'link', attrs: { href: 'https://x.y' } }], }, { type: 'text', text: ' done' }, ]); }); test('emoji at line start survives as plain text', () => { assert.deepEqual(tokenizeLine('✅ ok'), [ { type: 'text', text: '✅ ok' }, ]); }); test('arrow → and bullet • characters are plain text', () => { assert.deepEqual(tokenizeLine(' → foo • bar'), [ { type: 'text', text: ' → foo • bar' }, ]); }); test('blank line splits into two paragraphs', () => { const out = markdownToAdfContent('one\n\ntwo'); assert.equal(out.length, 2); assert.equal(out[0].type, 'paragraph'); assert.deepEqual(out[0].content, [{ type: 'text', text: 'one' }]); assert.deepEqual(out[1].content, [{ type: 'text', text: 'two' }]); }); test('single newline becomes hardBreak inside one paragraph', () => { const out = markdownToAdfContent('line1\nline2'); assert.equal(out.length, 1); assert.equal(out[0].type, 'paragraph'); assert.deepEqual(out[0].content, [ { type: 'text', text: 'line1' }, { type: 'hardBreak' }, { type: 'text', text: 'line2' }, ]); }); test('multiple blank lines collapse to a single paragraph split', () => { const out = markdownToAdfContent('a\n\n\n\nb'); assert.equal(out.length, 2); assert.deepEqual(out[0].content, [{ type: 'text', text: 'a' }]); assert.deepEqual(out[1].content, [{ type: 'text', text: 'b' }]); }); test('malformed markdown does not throw', () => { // Never throws is the only hard guarantee — the exact token shape // for ambiguous fragments is best-effort. The parser scans strong // before em, so an unclosed `**` frequently gets rescued as an em // starting at the second `*`. That's fine, just verify the concrete // behavior stays stable. assert.doesNotThrow(() => tokenizeLine('**unclosed and *also* here')); const out = tokenizeLine('**unclosed and *also* here'); // Something got marked em — the middle `*...*` window matches. assert.ok(out.some((n) => n.marks?.[0]?.type === 'em')); }); test('empty link body [](url) is treated as plain text', () => { // LINK_RE requires at least one char inside brackets; the empty case // won't match, so the raw text passes through unmarked. const out = tokenizeLine('a [](https://x.y) b'); const rendered = out.map((n) => n.text).join(''); assert.equal(rendered, 'a [](https://x.y) b'); assert.ok(!out.some((n) => n.marks?.[0]?.type === 'link')); }); test('*a**b*c does not throw and produces plausible tokens', () => { // Nasty edge case — verify we don't crash. Exact shape is not // contract, just "well-formed ADF inline nodes with no exception". assert.doesNotThrow(() => tokenizeLine('*a**b*c')); }); test('markdownToAdf full doc wraps content correctly', () => { const doc = markdownToAdf('**hi**\n\n[go](https://x)'); assert.equal(doc.version, 1); assert.equal(doc.type, 'doc'); assert.equal(doc.content.length, 2); assert.deepEqual(doc.content[0].content, [ { type: 'text', text: 'hi', marks: [{ type: 'strong' }] }, ]); assert.deepEqual(doc.content[1].content, [ { type: 'text', text: 'go', marks: [{ type: 'link', attrs: { href: 'https://x' } }], }, ]); }); test('adjacent plain-text runs collapse into one text node', () => { // Verifies the pushText coalescing behavior — cleaner ADF output. const out = tokenizeLine('abc def ghi'); assert.equal(out.length, 1); assert.deepEqual(out[0], { type: 'text', text: 'abc def ghi' }); });