Same
Same
') expect(inferBindings(twice.block, twice.owns, collectStrings({ a: 'Same' })).text).toEqual([]) const shared = dom('Both
') expect(inferBindings(shared.block, shared.owns, collectStrings({ a: 'Both', b: 'Both' })).text).toEqual([]) }) it('only binds text leaves; whitespace differences do not matter; nested blocks bind their own data', () => { const { block, owns } = dom('Spaced \n out
Child text
World
') markInferred(block, inferBindings(block, owns, collectStrings({ a: 'Hello', b: 'World' })).text) expect(block.querySelector('h2')!.getAttribute('data-cms-field')).toBe('a') expect(block.querySelector('h2')!.getAttribute('data-cms-editor')).toBe('text') markInferred(block, inferBindings(block, owns, collectStrings({ b: 'World' })).text) expect(block.querySelector('h2')!.hasAttribute('data-cms-field')).toBe(false) expect(block.querySelector('p')!.getAttribute('data-cms-field')).toBe('b') }) }) describe('paths', () => { it('parse / get / patchForPath (top-level key rebuilt, input untouched)', () => { expect(parsePath('links.0.text')).toEqual(['links', 0, 'text']) const data = { title: 't', links: [{ text: 'A', href: '/a' }, { text: 'B' }] } expect(getIn(data, parsePath('links.1.text'))).toBe('B') expect(patchForPath(data, ['title'], 'x')).toEqual({ title: 'x' }) expect(patchForPath(data, parsePath('links.0.text'), 'Z')).toEqual({ links: [{ text: 'Z', href: '/a' }, { text: 'B' }] }) expect(data.links[0].text).toBe('A') expect(patchForPath({}, parsePath('a.b.c'), 'v')).toEqual({ a: { b: { c: 'v' } } }) expect(patchForPath({}, parsePath('a.0'), 'v')).toEqual({ a: ['v'] }) }) }) describe('what a container accepts', () => { it('x-graph.children: true means anything (the shape the real schemas use); lists and * work too', () => { expect(allowedChildren({ 'x-graph': { children: true } }, ['a', 'b'])).toEqual(['a', 'b']) expect(allowedChildren({ 'x-graph': { children: ['a'] } }, ['a', 'b'])).toEqual(['a']) expect(allowedChildren({ 'x-graph': { children: ['*'] } }, ['a', 'b'])).toEqual(['a', 'b']) expect(allowedChildren({ 'x-graph': {} }, ['a'])).toEqual([]) expect(allowedChildren(undefined, ['a'])).toEqual([]) }) it('acceptsChild is optimistic for unknown schemas, strict for known ones', () => { expect(acceptsChild(undefined, 'x', [])).toBe(true) expect(acceptsChild(schemas.section, 'heading', [])).toBe(true) expect(acceptsChild(schemas.section, 'page', [])).toBe(false) expect(acceptsChild(schemas.heading, 'text', [])).toBe(false) // a leaf type }) it('typeName tolerates the shapes /api/types might use', () => { expect([typeName('a'), typeName({ type: 'b' }), typeName({ name: 'c' }), typeName({ dataType: 'd' }), typeName(null)]).toEqual(['a', 'b', 'c', 'd', '']) }) it('dropTarget refuses containers that do not accept, falling back to an ancestor that does', () => { const r = (y: number, h = 100) => ({ x: 0, y, w: 600, h }) const blocks = [ { id: 'outer', container: true, depth: 0, rect: r(0, 400) }, { id: 'inner', parentId: 'outer', container: true, depth: 1, rect: r(50, 200) }, ] expect(dropTarget(blocks, { x: 10, y: 100 }, null)!.parentId).toBe('inner') expect(dropTarget(blocks, { x: 10, y: 100 }, null, (p) => p !== 'inner')!.parentId).toBe('outer') expect(dropTarget(blocks, { x: 10, y: 100 }, null, () => false)!.parentId).toBe('') // page level expect(dropTarget(blocks, { x: 900, y: 100 }, null, () => false)).toBeNull() // beside the page: over an editor panel }) }) import { moveTarget, rootExtent, sortTarget } from '../src/page/dnd' describe('sort snapping between siblings', () => { const r = (y: number, h: number, x = 0, w = 600) => ({ x, y, w, h }) // four root blocks (A, B, C are containers - the case that used to nest instead of sort), with children const blocks = [ { id: 'A', container: true, depth: 0, rect: r(0, 300) }, { id: 'a1', parentId: 'A', container: false, depth: 1, rect: r(10, 100, 10, 580) }, { id: 'B', container: true, depth: 0, rect: r(320, 100) }, { id: 'C', container: true, depth: 0, rect: r(440, 100) }, { id: 'D', container: false, depth: 0, rect: r(560, 50) }, ] it('snaps to the nearest boundary between root nodes, centred in the gap', () => { const t = sortTarget(blocks, { x: 50, y: 500 }, 'B', '')! // pointer is deep inside container C // siblings without B: A(0-300) C(440-540) D(560-610) -> boundaries 0, 370, 550, 610 expect(t.snaps!.map((s) => s.y + 1.5)).toEqual([0, 370, 550, 610]) expect(t).toMatchObject({ parentId: '', index: 2 }) // nearest is 550 (between C and D) expect(t.line.y + 1.5).toBe(550) expect(t.line.w).toBe(600) }) it('snaps by distance to boundaries, not by sibling midpoints (stable near the edges)', () => { expect(sortTarget(blocks, { x: 50, y: -40 }, 'B', '')!.index).toBe(0) expect(sortTarget(blocks, { x: 50, y: 5000 }, 'B', '')!.index).toBe(3) expect(sortTarget(blocks, { x: 50, y: 372 }, 'B', '')!.index).toBe(1) }) it('excludes the dragged block, and a row of siblings sorts left to right', () => { const row = [ { id: 'p', container: true, depth: 0, rect: r(0, 100, 0, 600) }, { id: 'c1', parentId: 'p', container: false, depth: 1, rect: r(0, 100, 0, 150) }, { id: 'c2', parentId: 'p', container: false, depth: 1, rect: r(0, 100, 200, 150) }, { id: 'c3', parentId: 'p', container: false, depth: 1, rect: r(0, 100, 400, 150) }, ] const t = sortTarget(row, { x: 380, y: 50 }, 'c1', 'p')! // siblings: c2, c3 expect(t.snaps).toHaveLength(3) expect(t.line.h).toBe(100) // a vertical line expect(t.line.w).toBe(3) expect(t).toMatchObject({ parentId: 'p', index: 1 }) // between c2 and c3 expect(t.line.x + 1.5).toBe(375) // centred in the 50px gap (350..400) }) it('moveTarget: dragging a root block sorts by default and nests only with Alt', () => { const sort = moveTarget(blocks, { x: 50, y: 500 }, { id: 'B' })! expect(sort).toMatchObject({ parentId: '', index: 2 }) expect(sort.snaps).toBeDefined() const nest = moveTarget(blocks, { x: 50, y: 500 }, { id: 'B' }, { nest: true })! expect(nest.parentId).toBe('C') // the container under the pointer expect(nest.snaps).toBeUndefined() }) it('moveTarget: a nested block sorts inside its parent, and leaving the parent switches to general drops', () => { const withSibs = [...blocks, { id: 'a2', parentId: 'A', container: false, depth: 1, rect: r(120, 100, 10, 580) }] expect(moveTarget(withSibs, { x: 50, y: 250 }, { id: 'a1', parentId: 'A' })).toMatchObject({ parentId: 'A', index: 1 }) expect(moveTarget(withSibs, { x: 50, y: 380 }, { id: 'a1', parentId: 'A' })!.parentId).toBe('B') // left A: dropped into B }) it('beside the page (over editor chrome) a root drag has no target', () => { expect(rootExtent(blocks)).toMatchObject({ x: -24, w: 648 }) expect(moveTarget(blocks, { x: 900, y: 100 }, { id: 'B' })).toBeNull() }) })