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>
73 lines
3.0 KiB
TypeScript
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)
|
|
})
|
|
})
|