diff --git a/proto/src/__tests__/cms-editor-client.test.ts b/proto/src/__tests__/cms-editor-client.test.ts new file mode 100644 index 0000000..21748e7 --- /dev/null +++ b/proto/src/__tests__/cms-editor-client.test.ts @@ -0,0 +1,91 @@ +/** + * Regression test for the Content Editor browser script: deleting an array + * item via its ❌ button must reindex the remaining items, otherwise collect() + * produces sparse arrays (null holes) that fail schema validation + * ("$.details.services[1].specs.items[0]: string érték szükséges"). + * + * Runs the REAL scripts/cms-editor-client.js in jsdom and clicks the actual + * delete buttons — earlier coverage only exercised reindexItems() directly, + * which missed that the onclick handler removed the node BEFORE looking up + * its container (detached node → closest() === null → no reindex). + */ +import fs from 'fs' +import path from 'path' + +const clientJs = fs.readFileSync(path.join(__dirname, '../../../scripts/cms-editor-client.js'), 'utf8') + +const service = (n: number) => ({ + id: `svc-${n}`, + title: `Szolgáltatás ${n}`, + description: `Leírás ${n}`, + icon: '🔧', + features: [`feature ${n}`], + ctaText: 'CTA', +}) + +const makeData = () => ({ + details: { + title: 'Részletek', + subtitle: 'Alcím', + services: [ + { icon: 'a', title: 's0', description: 'd0', specs: { title: 't0', items: ['a0', 'b0', 'c0'] } }, + { icon: 'b', title: 's1', description: 'd1', specs: { title: 't1', items: ['a1', 'b1', 'c1'] } }, + { icon: 'c', title: 's2', description: 'd2', specs: { title: 't2', items: ['a2', 'b2', 'c2'] } }, + ], + }, +}) + +function bootClient(data: unknown) { + ;(global as any).DATA = data + ;(global as any).FILE = 'services' + ;(global as any).CSRF_TOKEN = 'test-token' + ;(global as any).fetch = jest.fn() + document.body.innerHTML = '
' + // sloppy-mode eval publishes the script's functions on the global object + ;(0, eval)(clientJs) +} + +function deleteButtonFor(dataPath: string): HTMLButtonElement { + const field = document.querySelector(`[data-path="${CSS.escape(dataPath)}"]`) as HTMLElement + expect(field).not.toBeNull() + const wrap = field.closest('.str-item') as HTMLElement + expect(wrap).not.toBeNull() + return wrap.querySelector('.btn-del') as HTMLButtonElement +} + +afterEach(() => { + delete (global as any).DATA + delete (global as any).FILE + delete (global as any).CSRF_TOKEN +}) + +describe('Content Editor client delete/reindex', () => { + it('deleting a nested string array item keeps the remaining items dense', () => { + const data = makeData() + bootClient(data) + + deleteButtonFor('details.services[1].specs.items[0]').click() + + const collected = (global as any).collect() + expect(collected.details.services[1].specs.items).toEqual(['b1', 'c1']) + expect(collected.details.services[0].specs.items).toEqual(['a0', 'b0', 'c0']) + expect(collected.details.services[2].specs.items).toEqual(['a2', 'b2', 'c2']) + }) + + it('deleting an object card reindexes the outer array', () => { + const data = makeData() + bootClient(data) + + const cardHeader = Array.from(document.querySelectorAll('.card-header')) + .find(h => h.textContent === 'details.services[1]') as HTMLElement + expect(cardHeader).not.toBeNull() + const card = cardHeader.closest('.obj-card') as HTMLElement + ;(card.querySelector('.btn-del-card') as HTMLButtonElement).click() + + const collected = (global as any).collect() + expect(collected.details.services).toHaveLength(2) + expect(collected.details.services[0].title).toBe('s0') + expect(collected.details.services[1].title).toBe('s2') + expect(collected.details.services[1].specs.items).toEqual(['a2', 'b2', 'c2']) + }) +}) diff --git a/scripts/cms-editor-client.js b/scripts/cms-editor-client.js index 22c53ea..d904953 100644 --- a/scripts/cms-editor-client.js +++ b/scripts/cms-editor-client.js @@ -116,7 +116,14 @@ function makeStrItem(val, idx, path) { del.className = 'btn-del'; del.textContent = '❌'; del.title = 'Törlés'; - del.onclick = () => { wrap.remove(); reindexItems(wrap.closest('.array-items')); }; + del.onclick = () => { + // WHY: capture the container BEFORE removing — a detached node has no + // ancestors, so closest() would return null and reindexing would silently + // not run (sparse arrays → schema errors on save). + const container = wrap.closest('.array-items'); + wrap.remove(); + reindexItems(container); + }; wrap.appendChild(ta); wrap.appendChild(del); return wrap; @@ -135,7 +142,12 @@ function makeObjCard(obj, idx, path) { const del = document.createElement('button'); del.className = 'btn-del-card'; del.textContent = '❌ Törlés'; - del.onclick = () => { card.remove(); reindexItems(card.closest('.array-items')); }; + del.onclick = () => { + // Same as above: capture before detaching, or reindexing is skipped. + const container = card.closest('.array-items'); + card.remove(); + reindexItems(container); + }; card.appendChild(del); return card; }