cowrite-dev / test /structure.js
thomwolf's picture
thomwolf HF Staff
Multi-node structure moves, H1 page rename, untitled pages
86725f7
Raw
History Blame Contribute Delete
7.71 kB
// 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')