// Unit tests for the structure YAML grammar and the pure text mutations // behind sidebar drag-and-drop and groups. No server needed. import assert from 'node:assert' import { parseStructure, moveNodeInYaml, moveNodesInYaml, addGroupToYaml, renameGroupInYaml, dissolveGroupInYaml, validGroupLabel } from '../server/pages.js' const YAML = [ '- home', '- "Getting started":', ' - intro', ' - setup', '- design', ' - communication', '- references', ].join('\n') // --- parsing --- { const { tree, problems } = parseStructure(YAML) assert.deepStrictEqual(problems, []) assert.strictEqual(tree.length, 4) assert.strictEqual(tree[0].slug, 'home') assert.strictEqual(tree[1].group, 'Getting started') assert.deepStrictEqual(tree[1].children.map(c => c.slug), ['intro', 'setup']) assert.strictEqual(tree[2].children[0].slug, 'communication') } { // problem lines are reported, groups can nest, comments are skipped const { tree, problems } = parseStructure('# comment\n- "A":\n - "B":\n - x\n- ???') assert.strictEqual(problems.length, 1) assert.strictEqual(tree[0].group, 'A') assert.strictEqual(tree[0].children[0].group, 'B') assert.strictEqual(tree[0].children[0].children[0].slug, 'x') } // --- move: reorder at root --- { const { raw } = moveNodeInYaml(YAML, 'references', null, 0) assert.strictEqual(raw.split('\n')[0], '- references') assert.strictEqual(parseStructure(raw).tree[0].slug, 'references') } // --- move: nest a page (subtree travels, reindents) --- { const { raw } = moveNodeInYaml(YAML, 'design', ['"Getting started"'], 0) const t = parseStructure(raw).tree const g = t.find(n => n.group === 'Getting started') assert.deepStrictEqual(g.children.map(c => c.slug), ['design', 'intro', 'setup']) assert.strictEqual(g.children[0].children[0].slug, 'communication') assert.ok(raw.includes(' - communication')) } // --- move: out of a group to root, append --- { const { raw } = moveNodeInYaml(YAML, 'intro', null, 99) const t = parseStructure(raw).tree assert.strictEqual(t[t.length - 1].slug, 'intro') const g = t.find(n => n.group === 'Getting started') assert.deepStrictEqual(g.children.map(c => c.slug), ['setup']) } // --- move: index counts after removal (down within same parent) --- { const { raw } = moveNodeInYaml(YAML, 'home', null, 2) assert.deepStrictEqual( parseStructure(raw).tree.map(n => n.slug || n.group), ['Getting started', 'design', 'home', 'references'] ) } // --- move: under a page (pages nest pages) --- { const { raw } = moveNodeInYaml(YAML, 'references', 'design', 99) const d = parseStructure(raw).tree.find(n => n.slug === 'design') assert.deepStrictEqual(d.children.map(c => c.slug), ['communication', 'references']) } // --- move: unfiled page gets a line --- { const { raw } = moveNodeInYaml(YAML, 'notes', ['"Getting started"'], 1) const g = parseStructure(raw).tree.find(n => n.group === 'Getting started') assert.deepStrictEqual(g.children.map(c => c.slug), ['intro', 'notes', 'setup']) } // --- move: comments inside a block travel with it --- { const withComment = YAML.replace(' - communication', ' # keep\n - communication') const { raw } = moveNodeInYaml(withComment, 'design', null, 0) const lines = raw.split('\n') assert.ok(lines.indexOf(' # keep') === lines.indexOf('- design') + 1) } // --- move: rejections --- assert.ok(moveNodeInYaml(YAML, 'design', 'communication', 0).error, 'nest into own subtree') assert.ok(moveNodeInYaml(YAML, '_structure', null, 0).error, 'move _structure') assert.ok(moveNodeInYaml(YAML, 'home', ['"Nope"'], 0).error, 'unknown parent') assert.ok(moveNodeInYaml(YAML, ['"Nope"'], null, 0).error, 'unknown group node') assert.ok(moveNodeInYaml(YAML, 'bad slug!', null, 0).error, 'bad slug') // --- groups: add / rename / dissolve --- { const { raw } = addGroupToYaml(YAML, 'References & links', null, 1) const t = parseStructure(raw).tree assert.strictEqual(t[1].group, 'References & links') assert.ok(addGroupToYaml(raw, 'References & links', null, 0).error, 'duplicate sibling label') } { const { raw } = addGroupToYaml(YAML, 'Sub', ['"Getting started"'], 99) const g = parseStructure(raw).tree.find(n => n.group === 'Getting started') assert.strictEqual(g.children[2].group, 'Sub') } { const { raw } = renameGroupInYaml(YAML, ['"Getting started"'], 'Start here') const t = parseStructure(raw).tree assert.strictEqual(t[1].group, 'Start here') assert.deepStrictEqual(t[1].children.map(c => c.slug), ['intro', 'setup']) assert.ok(renameGroupInYaml(YAML, ['"Getting started"'], 'x'.repeat(61)).error, 'label too long') assert.ok(renameGroupInYaml(YAML, ['"Nope"'], 'ok').error, 'unknown group') } { const { raw } = dissolveGroupInYaml(YAML, ['"Getting started"']) const t = parseStructure(raw).tree assert.deepStrictEqual(t.map(n => n.slug), ['home', 'intro', 'setup', 'design', 'references']) assert.ok(dissolveGroupInYaml(YAML, ['home']).error, 'dissolve a page') } // --- multi-node move (sidebar multi-select) --- { // two pages from different parents land as consecutive siblings, doc order const { raw } = moveNodesInYaml(YAML, ['setup', 'references'], 'design', 0) const d = parseStructure(raw).tree.find(n => n.slug === 'design') assert.deepStrictEqual(d.children.map(c => c.slug), ['setup', 'references', 'communication']) } { // a group and its own child selected together: the child travels once const { raw } = moveNodesInYaml(YAML, [['"Getting started"'], 'intro'], 'design', 99) const t = parseStructure(raw).tree assert.deepStrictEqual(t.map(n => n.slug || n.group), ['home', 'design', 'references']) const d = t.find(n => n.slug === 'design') const g = d.children.find(n => n.group === 'Getting started') assert.deepStrictEqual(g.children.map(c => c.slug), ['intro', 'setup']) assert.strictEqual(d.children.filter(n => n.slug === 'intro').length, 0, 'intro not duplicated') } { // index counts after ALL moved nodes leave: home+intro to root index 2 const { raw } = moveNodesInYaml(YAML, ['home', 'intro'], null, 2) assert.deepStrictEqual( parseStructure(raw).tree.map(n => n.slug || n.group), ['Getting started', 'design', 'home', 'intro', 'references'] ) } { // mixed tree + unfiled refs; duplicates collapse const { raw } = moveNodesInYaml(YAML, ['setup', 'notes', 'setup'], null, 0) const t = parseStructure(raw).tree assert.deepStrictEqual(t.slice(0, 2).map(n => n.slug), ['setup', 'notes']) assert.strictEqual(JSON.stringify(t).split('"setup"').length, 2, 'setup appears once') } assert.ok(moveNodesInYaml(YAML, [], null, 0).error, 'empty selection') assert.ok(moveNodesInYaml(YAML, ['home', 'design'], 'communication', 0).error, 'parent inside a moved subtree') // --- labels --- assert.strictEqual(validGroupLabel(' Reading list '), 'Reading list') assert.strictEqual(validGroupLabel('a"b'), null) assert.strictEqual(validGroupLabel(''), null) // --- a round of moves keeps the yaml parseable with zero problems --- { let raw = YAML for (const [node, parent, index] of [ ['setup', null, 0], ['design', ['"Getting started"'], 0], ['setup', 'design', 99], ['home', 'setup', 0], ]) { const r = moveNodeInYaml(raw, node, parent, index) assert.ok(!r.error, `${node}: ${r.error}`) raw = r.raw assert.deepStrictEqual(parseStructure(raw).problems, []) } const g = parseStructure(raw).tree.find(n => n.group === 'Getting started') assert.strictEqual(g.children[0].slug, 'design') assert.strictEqual(g.children[0].children[1].slug, 'setup') assert.strictEqual(g.children[0].children[1].children[0].slug, 'home') } console.log('structure tests passed')