Files
2026-11-01 10:27:56 +01:00

153 lines
5.8 KiB
TypeScript

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<HTMLElement>(sel)!
const qa = (sel: string) => [...h.shadow.querySelectorAll<HTMLElement>(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()
})
})