import { afterEach, describe, expect, it } from 'vitest' import { act } from 'react' import { byId, click, setup } from './harness' import type { Harness } from './harness' let h: Harness afterEach(() => h?.cleanup()) const select = async (id: string) => { await act(async () => void click(byId(id))) await h.settle() } const q = (sel: string) => h.shadow.querySelector(sel)! const qa = (sel: string) => [...h.shadow.querySelectorAll(sel)] const dialogs = () => qa('.cms-dialog') const z = (el: HTMLElement) => Number(el.style.zIndex) describe('one dialog per thing: opening again focuses the existing one', () => { it('Properties opened twice for the same block is one dialog, raised, pulsing and focused', async () => { h = await setup() await h.settle() await select('s1') await h.press('Properties') expect(dialogs()).toHaveLength(1) const first = dialogs()[0] const z0 = z(first) await act(async () => void (document.activeElement as HTMLElement | null)?.blur?.()) await h.press('Properties') // again expect(dialogs()).toHaveLength(1) expect(dialogs()[0]).toBe(first) // the same element, not a replacement (form state is kept) expect(z(first)).toBeGreaterThan(z0) expect(first.className).toContain('is-pulsing') expect(h.shadow.activeElement).toBe(first.querySelector('input')) }) it('keeps unsaved edits when the dialog is asked for again', async () => { h = await setup() await h.settle() await select('s1') await h.press('Properties') const input = q('[data-path="title"] input') as HTMLInputElement const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set! await act(async () => { set.call(input, 'Unsaved draft') input.dispatchEvent(new Event('input', { bubbles: true })) }) await h.press('Properties') expect((q('[data-path="title"] input') as HTMLInputElement).value).toBe('Unsaved draft') }) it('a double click before the first dialog has loaded still opens only one', async () => { h = await setup() await h.settle() await Promise.all([h.editor.ctrl.openEditor('properties', 's1'), h.editor.ctrl.openEditor('properties', 's1')]) await h.settle() expect(dialogs()).toHaveLength(1) }) it('different blocks or editors get their own dialogs; reopening the older one raises it above the newer', async () => { h = await setup() await h.settle() await select('s1') await h.press('Properties') await select('l1') await h.press('Properties') await h.press('Style') expect(dialogs()).toHaveLength(3) const [s1Props, l1Props, l1Style] = dialogs() expect(z(l1Style)).toBeGreaterThan(z(l1Props)) await select('s1') await h.press('Properties') // the oldest dialog comes forward expect(dialogs()).toHaveLength(3) expect(z(s1Props)).toBeGreaterThan(z(l1Style)) }) it('clicking a dialog brings it to the front; Escape closes the topmost', async () => { h = await setup() await h.settle() await select('s1') await h.press('Properties') await select('t1') await h.press('JSON').catch(() => {}) // under the … menu: open via the controller instead await h.editor.ctrl.openEditor('raw-json', 't1') await h.settle() const [a, b] = dialogs() expect(z(b)).toBeGreaterThan(z(a)) await act(async () => void a.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, composed: true }))) expect(z(a)).toBeGreaterThan(z(b)) await act(async () => void document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))) expect(dialogs()).toHaveLength(1) expect(dialogs()[0]).toBe(b) // a was on top, so a closed }) it('the same insert slot is one picker; another slot opens another', async () => { h = await setup() await h.settle() await select('s1') await act(async () => void qa('.cms-slot')[1].click()) await h.settle() await act(async () => void qa('.cms-slot')[1].click()) await h.settle() expect(dialogs()).toHaveLength(1) await act(async () => void qa('.cms-slot')[2].click()) await h.settle() expect(dialogs()).toHaveLength(2) }) it('an attribute chip opens one editor per property', async () => { h = await setup() await h.settle() await select('f1') const chip = () => qa('[aria-label="Edit href"]')[0] await act(async () => void chip().click()) await h.settle() await act(async () => void chip().click()) await h.settle() expect(dialogs()).toHaveLength(1) await act(async () => void qa('[aria-label="Edit href"]')[1].click()) await h.settle() expect(dialogs()).toHaveLength(2) }) it('after closing, the same dialog can be opened again', async () => { h = await setup() await h.settle() await select('s1') await h.press('Properties') await h.press('Close') expect(dialogs()).toHaveLength(0) await h.press('Properties') expect(dialogs()).toHaveLength(1) }) }) describe('the property editor uses icons, not text glyphs', () => { it('array, map and group controls are svg icons', async () => { h = await setup() await h.settle() await select('f1') await h.press('Properties') const form = q('.cms-dialog').textContent! expect(form).not.toMatch(/[↑↓⧉✕+⠿▸↔↺]/) for (const label of ['Move item 1 down', 'Duplicate item 1', 'Remove item 1', 'Add item']) { expect(q(`[aria-label="${label}"] svg path`), label).not.toBeNull() } expect(q('[aria-label="Add entry"] svg')).not.toBeNull() expect(q('.cms-group > summary svg.cms-chev')).not.toBeNull() expect(q('.cms-actions')).toBeNull() // nothing changed yet: no footer await act(async () => click(q('[aria-label="Add item"]')!)) expect(q('.cms-actions .cms-btn--primary svg')).not.toBeNull() }) })