Files
cms-onpageeditor/mock/graph.test.ts
T
matsandClaude Sonnet 5.5 1af9ec3172 Add on-page editor for the go-graph-cms
Injectable React editor that is side-loaded onto the statically rendered
page and edits through the CMS HTTP API (POST /api/edit).

- Shadow-root UI over server HTML; patches morphed in, undo/redo from
  server inverses (reuses @k6n/cms-editor EditClient/morph/markdown editor)
- Sign-in via go-auth device grant as the CMS client (aud matches), token
  refresh + 401 retry; live preview swapped in on load and on tab focus
- Schema-driven property form (arrays, maps, variants, $ref, validation)
- Every string property editable where it is rendered (value-bound inline
  fields and attribute chips), markdown WYSIWYG for marked fields
- Block palette + drag & drop with ghost drop targets, insertion slots,
  sort snapping between siblings (Alt to nest)
- x-style annotations: on-page padding/margin/gap handles, Style panel,
  wrap-in-layout; x-graph label/group for titles and grouping
- Dev workflow: Vite proxy to the live site (token injected server-side)
  or an offline in-memory mock CMS; jsdom test suite

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 22:34:48 +01:00

73 lines
3.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { seedGraph } from './fixtures'
import { HttpError } from './graph'
const kids = (g: ReturnType<typeof seedGraph>, id: string) => g.children(id).map((e) => e.to)
describe('mock graph /api/edit semantics', () => {
it('insert at an index, and its inverse removes it again', () => {
const g = seedGraph()
const r = g.edit([{ op: 'insert', parent: 's1', type: 'text', index: 1, data: { body: 'x' } }])
expect(kids(g, 's1')).toEqual(['h1', r.results[0].node, 't1', 'b1'])
g.edit(r.inverse)
expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1'])
expect(g.nodes.has(r.results[0].node!)).toBe(false)
})
it('set merges, null deletes, inverse restores (including absent keys)', () => {
const g = seedGraph()
const r = g.edit([{ op: 'set', id: 'h1', data: { text: 'New', extra: 1, level: null } }])
expect(g.node('h1').data).toEqual({ text: 'New', extra: 1 })
g.edit(r.inverse)
expect(g.node('h1').data).toEqual({ text: 'Edit this headline', level: 1 })
})
it('rejects a stale version with 409 and lifecycle keys with 400', () => {
const g = seedGraph()
g.edit([{ op: 'set', id: 'h1', data: { text: 'a' } }])
expect(() => g.edit([{ op: 'set', id: 'h1', data: { text: 'b' }, version: 1 }])).toThrow(expect.objectContaining({ status: 409 }))
expect(() => g.edit([{ op: 'set', id: 'h1', data: { status: 'x' } }])).toThrow(HttpError)
})
it('move reorders and re-parents; inverse puts it back', () => {
const g = seedGraph()
const r = g.edit([{ op: 'move', edge: 'e4', parent: 's2', index: 0 }])
expect(kids(g, 's2')).toEqual(['b1', 't2'])
expect(kids(g, 's1')).toEqual(['h1', 't1'])
g.edit(r.inverse)
expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1'])
expect(kids(g, 's2')).toEqual(['t2'])
})
it('refuses cycles', () => {
const g = seedGraph()
g.edit([{ op: 'insert', parent: 's1', type: 'section', id: 'inner' }])
expect(() => g.edit([{ op: 'move', edge: 'e1', parent: 'inner' }])).toThrow(/cycle/)
})
it('delete takes the subtree; the inverse (restore) brings it back verbatim', () => {
const g = seedGraph()
const r = g.edit([{ op: 'delete', edge: 'e1' }])
expect(g.nodes.has('s1')).toBe(false)
expect(g.nodes.has('h1')).toBe(false)
g.edit(r.inverse)
expect(kids(g, 'p1')).toEqual(['s1', 's2', 'l1', 'f1'])
expect(kids(g, 's1')).toEqual(['h1', 't1', 'b1'])
})
it('duplicate deep-copies right after the original', () => {
const g = seedGraph()
const r = g.edit([{ op: 'duplicate', edge: 'e1' }])
const copy = r.results[0].node!
expect(kids(g, 'p1')).toEqual(['s1', copy, 's2', 'l1', 'f1'])
expect(kids(g, copy)).toHaveLength(3)
})
it('a failing op rolls the whole batch back and names the index', () => {
const g = seedGraph()
const before = JSON.stringify([...g.nodes])
expect(() => g.edit([{ op: 'set', id: 'h1', data: { text: 'changed' } }, { op: 'delete', edge: 'nope' }])).toThrow(/op 1/)
expect(JSON.stringify([...g.nodes])).toBe(before)
})
})