305 lines
9.5 KiB
JavaScript
305 lines
9.5 KiB
JavaScript
/**
|
|
* Tests for threads parser - serialize/deserialize ::: threads block
|
|
*
|
|
* Run: node packages/plugins/threads/tests/parser.test.js
|
|
*/
|
|
|
|
const {
|
|
parseThreadsFromContent,
|
|
serializeThreadsBlock,
|
|
replaceThreadsBlock,
|
|
} = require('../src/plugin/parser.js');
|
|
|
|
let passed = 0;
|
|
let total = 0;
|
|
|
|
function assert(condition, msg) {
|
|
total++;
|
|
if (!condition) {
|
|
console.error(`FAIL: ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
passed++;
|
|
console.log(` PASS: ${msg}`);
|
|
}
|
|
|
|
function assertDeepEqual(actual, expected, msg) {
|
|
const a = JSON.stringify(actual);
|
|
const e = JSON.stringify(expected);
|
|
assert(a === e, `${msg}\n expected: ${e}\n actual: ${a}`);
|
|
}
|
|
|
|
// ─── Test 1: Parse threads from content with 2 threads, comments, reactions, resolved status ───
|
|
|
|
console.log('\nTest 1: Parse full threads block');
|
|
|
|
const fullContent = `# My Document
|
|
|
|
Some content here.
|
|
|
|
::: threads
|
|
::: thread t-abc123
|
|
::: comment "alice" "2026-03-07"
|
|
This is the first comment
|
|
:::
|
|
|
|
::: comment "bob" "2026-03-08" edited "2026-03-09"
|
|
I agree, let's reword it
|
|
|
|
::: reactions
|
|
- 👍 charlie, dave
|
|
- 🎉 alice
|
|
:::
|
|
:::
|
|
:::
|
|
|
|
::: thread t-def456 resolved "charlie" "2026-03-08"
|
|
::: comment "charlie" "2026-03-08"
|
|
Typo in this section
|
|
:::
|
|
:::
|
|
:::
|
|
|
|
Some trailing content.
|
|
`;
|
|
|
|
const threads = parseThreadsFromContent(fullContent);
|
|
|
|
assert(Array.isArray(threads), 'parseThreadsFromContent returns an array');
|
|
assert(threads.length === 2, 'parses 2 threads');
|
|
|
|
// Thread 1
|
|
const t1 = threads[0];
|
|
assert(t1.id === 't-abc123', 'thread 1 id');
|
|
assert(t1.resolved === false, 'thread 1 not resolved');
|
|
assert(t1.resolved_by === null, 'thread 1 resolved_by is null');
|
|
assert(t1.resolved_at === null, 'thread 1 resolved_at is null');
|
|
assert(t1.comments.length === 2, 'thread 1 has 2 comments');
|
|
|
|
// Comment 1 of thread 1
|
|
const c1 = t1.comments[0];
|
|
assert(c1.author === 'alice', 'comment 1 author');
|
|
assert(c1.date === '2026-03-07', 'comment 1 date');
|
|
assert(c1.edited_at === null, 'comment 1 not edited');
|
|
assert(c1.body === 'This is the first comment', 'comment 1 body');
|
|
assert(c1.reactions.length === 0, 'comment 1 no reactions');
|
|
assert(c1.id.startsWith('c-'), 'comment 1 id starts with c-');
|
|
assert(c1.id.length === 10, 'comment 1 id is c- + 8 hex chars');
|
|
assert(c1.thread_id === 't-abc123', 'comment 1 thread_id matches');
|
|
|
|
// Comment 2 of thread 1
|
|
const c2 = t1.comments[1];
|
|
assert(c2.author === 'bob', 'comment 2 author');
|
|
assert(c2.date === '2026-03-08', 'comment 2 date');
|
|
assert(c2.edited_at === '2026-03-09', 'comment 2 edited_at');
|
|
assert(c2.body === "I agree, let's reword it", 'comment 2 body (no reactions content)');
|
|
assert(c2.reactions.length === 2, 'comment 2 has 2 reactions');
|
|
assert(c2.reactions[0].emoji === '👍', 'reaction 1 emoji');
|
|
assertDeepEqual(c2.reactions[0].authors, ['charlie', 'dave'], 'reaction 1 authors');
|
|
assert(c2.reactions[1].emoji === '🎉', 'reaction 2 emoji');
|
|
assertDeepEqual(c2.reactions[1].authors, ['alice'], 'reaction 2 authors');
|
|
|
|
// Thread 2
|
|
const t2 = threads[1];
|
|
assert(t2.id === 't-def456', 'thread 2 id');
|
|
assert(t2.resolved === true, 'thread 2 is resolved');
|
|
assert(t2.resolved_by === 'charlie', 'thread 2 resolved_by');
|
|
assert(t2.resolved_at === '2026-03-08', 'thread 2 resolved_at');
|
|
assert(t2.comments.length === 1, 'thread 2 has 1 comment');
|
|
assert(t2.comments[0].author === 'charlie', 'thread 2 comment author');
|
|
assert(t2.comments[0].body === 'Typo in this section', 'thread 2 comment body');
|
|
|
|
// ─── Test 2: Parse content with no threads block → empty array ───
|
|
|
|
console.log('\nTest 2: No threads block');
|
|
|
|
const noThreads = `# Just a document\n\nNo threads here.\n`;
|
|
const emptyResult = parseThreadsFromContent(noThreads);
|
|
assert(Array.isArray(emptyResult), 'returns array for no threads');
|
|
assert(emptyResult.length === 0, 'returns empty array');
|
|
|
|
// ─── Test 3: Serialize and re-parse (round-trip) ───
|
|
|
|
console.log('\nTest 3: Round-trip serialize → parse');
|
|
|
|
const threadData = [
|
|
{
|
|
id: 't-111111',
|
|
resolved: false,
|
|
resolved_by: null,
|
|
resolved_at: null,
|
|
comments: [
|
|
{
|
|
id: 'c-aabbccdd',
|
|
thread_id: 't-111111',
|
|
author: 'eve',
|
|
date: '2026-01-01',
|
|
edited_at: null,
|
|
body: 'Hello world',
|
|
reactions: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 't-222222',
|
|
resolved: true,
|
|
resolved_by: 'frank',
|
|
resolved_at: '2026-02-15',
|
|
comments: [
|
|
{
|
|
id: 'c-11223344',
|
|
thread_id: 't-222222',
|
|
author: 'frank',
|
|
date: '2026-02-10',
|
|
edited_at: '2026-02-12',
|
|
body: 'Multi-line\ncontent here',
|
|
reactions: [
|
|
{ emoji: '👍', authors: ['eve'] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const serialized = serializeThreadsBlock(threadData);
|
|
assert(serialized.startsWith('::: threads\n'), 'serialized starts with ::: threads');
|
|
assert(serialized.trimEnd().endsWith(':::'), 'serialized ends with :::');
|
|
|
|
// Re-parse
|
|
const reparsed = parseThreadsFromContent(serialized);
|
|
assert(reparsed.length === 2, 'round-trip: 2 threads');
|
|
|
|
assert(reparsed[0].id === 't-111111', 'round-trip: thread 1 id');
|
|
assert(reparsed[0].resolved === false, 'round-trip: thread 1 not resolved');
|
|
assert(reparsed[0].comments[0].author === 'eve', 'round-trip: comment author');
|
|
assert(reparsed[0].comments[0].body === 'Hello world', 'round-trip: comment body');
|
|
|
|
assert(reparsed[1].id === 't-222222', 'round-trip: thread 2 id');
|
|
assert(reparsed[1].resolved === true, 'round-trip: thread 2 resolved');
|
|
assert(reparsed[1].resolved_by === 'frank', 'round-trip: resolved_by');
|
|
assert(reparsed[1].resolved_at === '2026-02-15', 'round-trip: resolved_at');
|
|
assert(reparsed[1].comments[0].author === 'frank', 'round-trip: comment 2 author');
|
|
assert(reparsed[1].comments[0].edited_at === '2026-02-12', 'round-trip: edited_at');
|
|
assert(reparsed[1].comments[0].body === 'Multi-line\ncontent here', 'round-trip: multi-line body');
|
|
assert(reparsed[1].comments[0].reactions.length === 1, 'round-trip: reactions count');
|
|
assert(reparsed[1].comments[0].reactions[0].emoji === '👍', 'round-trip: reaction emoji');
|
|
assertDeepEqual(reparsed[1].comments[0].reactions[0].authors, ['eve'], 'round-trip: reaction authors');
|
|
|
|
// ─── Test 4: Replace threads block in existing file ───
|
|
|
|
console.log('\nTest 4: Replace threads block');
|
|
|
|
const existingContent = `# Document
|
|
|
|
Some text.
|
|
|
|
::: threads
|
|
::: thread t-old
|
|
::: comment "olduser" "2025-01-01"
|
|
Old comment
|
|
:::
|
|
:::
|
|
:::
|
|
|
|
Footer text.
|
|
`;
|
|
|
|
const newThreads = [
|
|
{
|
|
id: 't-new',
|
|
resolved: false,
|
|
resolved_by: null,
|
|
resolved_at: null,
|
|
comments: [
|
|
{
|
|
id: 'c-newid000',
|
|
thread_id: 't-new',
|
|
author: 'newuser',
|
|
date: '2026-06-01',
|
|
edited_at: null,
|
|
body: 'New comment',
|
|
reactions: [],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const replaced = replaceThreadsBlock(existingContent, newThreads);
|
|
assert(replaced.includes('# Document'), 'replace preserves header');
|
|
assert(replaced.includes('Footer text.'), 'replace preserves footer');
|
|
assert(replaced.includes('t-new'), 'replace includes new thread');
|
|
assert(!replaced.includes('t-old'), 'replace removes old thread');
|
|
assert(!replaced.includes('olduser'), 'replace removes old content');
|
|
|
|
// Verify the replaced content re-parses correctly
|
|
const reparsedReplaced = parseThreadsFromContent(replaced);
|
|
assert(reparsedReplaced.length === 1, 'replaced content has 1 thread');
|
|
assert(reparsedReplaced[0].id === 't-new', 'replaced thread id correct');
|
|
|
|
// ─── Test 5: Add threads block to file that doesn't have one ───
|
|
|
|
console.log('\nTest 5: Add threads block to file without one');
|
|
|
|
const plainContent = `# My Page
|
|
|
|
Just some content.
|
|
`;
|
|
|
|
const withThreads = replaceThreadsBlock(plainContent, newThreads);
|
|
assert(withThreads.includes('# My Page'), 'add preserves original content');
|
|
assert(withThreads.includes('::: threads'), 'add includes threads block');
|
|
assert(withThreads.includes('t-new'), 'add includes thread id');
|
|
|
|
const reparsedAdded = parseThreadsFromContent(withThreads);
|
|
assert(reparsedAdded.length === 1, 'added content has 1 thread');
|
|
assert(reparsedAdded[0].id === 't-new', 'added thread id correct');
|
|
|
|
// ─── Test 6: Replace with empty threads removes the block ───
|
|
|
|
console.log('\nTest 6: Replace with empty threads removes block');
|
|
|
|
const clearedContent = replaceThreadsBlock(existingContent, []);
|
|
assert(!clearedContent.includes('::: threads'), 'empty threads removes block');
|
|
assert(clearedContent.includes('# Document'), 'preserves header after clearing');
|
|
assert(clearedContent.includes('Footer text.'), 'preserves footer after clearing');
|
|
|
|
// ─── Test 7: Comment body with multiple paragraphs ───
|
|
|
|
console.log('\nTest 7: Multi-paragraph comment body');
|
|
|
|
const multiParaContent = `::: threads
|
|
::: thread t-mp
|
|
::: comment "alice" "2026-03-01"
|
|
First paragraph.
|
|
|
|
Second paragraph.
|
|
|
|
Third paragraph.
|
|
:::
|
|
:::
|
|
:::
|
|
`;
|
|
|
|
const mpThreads = parseThreadsFromContent(multiParaContent);
|
|
assert(mpThreads.length === 1, 'multi-para: 1 thread');
|
|
assert(
|
|
mpThreads[0].comments[0].body === 'First paragraph.\n\nSecond paragraph.\n\nThird paragraph.',
|
|
'multi-para: preserves paragraphs in body'
|
|
);
|
|
|
|
// ─── Test 8: Comment IDs are unique ───
|
|
|
|
console.log('\nTest 8: Comment IDs are unique');
|
|
|
|
const idSet = new Set();
|
|
for (const thread of threads) {
|
|
for (const comment of thread.comments) {
|
|
assert(!idSet.has(comment.id), `comment id ${comment.id} is unique`);
|
|
idSet.add(comment.id);
|
|
}
|
|
}
|
|
|
|
// ─── Done ───
|
|
|
|
console.log(`\n✓ All ${passed}/${total} tests passed.\n`);
|