import { describe, expect, it } from 'vitest' import { validateMorfo, MorfoInvariantError } from '../schema' import type { Morfo, MorfoEvent } from '../types' import { dialogMorfo } from './dialog' function cloneMorfo(m: typeof dialogMorfo): Morfo { return structuredClone(m as Morfo) as Morfo } describe('dialogMorfo', () => { it('passes shape + invariant validation', () => { expect(() => validateMorfo(dialogMorfo)).not.toThrow() }) it('declares the 7 parts the provider emits', () => { const kebabs = dialogMorfo.parts.map((p) => p.kebab).sort() expect(kebabs).toEqual( ['close', 'content', 'description', 'overlay', 'provider', 'title', 'trigger'].sort() ) }) it('declares six semantic events directly in morfo', () => { const events = dialogMorfo.events?.map((event) => event.name).sort() expect(events).toEqual( [ 'open', 'close-save', 'close-cancel', 'close-dismiss', 'close-dismiss-outside', 'close-after-fail' ].sort() ) }) it('declares data-last-action on Content for causal exits', () => { const content = dialogMorfo.parts.find((p) => p.kebab === 'content')! const causal = content.data.find((d) => d.attr === 'data-last-action') expect(causal).toBeDefined() expect(causal!.values).toContain('saved') expect(causal!.values).toContain('cancelled') }) it('can declare runtime sources for data-* attrs', () => { const trigger = dialogMorfo.parts.find((p) => p.kebab === 'trigger')! const state = trigger.data.find((d) => d.attr === 'data-state') expect(state?.value).toEqual({ kind: 'stateRef', state: 'open' }) }) it('fails validation when a partRef targets a non-existent kebab', () => { const broken = cloneMorfo(dialogMorfo) const trigger = broken.parts.find((p) => p.kebab === 'trigger')! const controls = trigger.aria.find((a) => a.attr === 'aria-controls')! ;(controls.value as { kind: 'partRef'; target: string }).target = 'nonexistent-part' expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError) }) it('fails validation when stateRef refers to a state not declared in the part', () => { const broken = cloneMorfo(dialogMorfo) const trigger = broken.parts.find((p) => p.kebab === 'trigger')! const expanded = trigger.aria.find((a) => a.attr === 'aria-expanded')! ;(expanded.value as { kind: 'stateRef'; state: string }).state = 'nonexistent-state' expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError) }) it('fails validation when data.value.stateRef refers to an undeclared state', () => { const broken = cloneMorfo(dialogMorfo) const trigger = broken.parts.find((p) => p.kebab === 'trigger')! const state = trigger.data.find((d) => d.attr === 'data-state')! ;(state.value as { kind: 'stateRef'; state: string }).state = 'nonexistent-state' expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError) }) it('fails validation when two parts share the same kebab', () => { const broken = cloneMorfo(dialogMorfo) ;(broken.parts as unknown as { kebab: string }[])[1].kebab = 'content' expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError) }) it('fails validation with empty scope', () => { const broken = cloneMorfo(dialogMorfo) ;(broken as unknown as { scope: [] }).scope = [] expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError) }) it('fails when an event targets a non-existent part', () => { const broken = cloneMorfo(dialogMorfo) ;(broken.events as MorfoEvent[])[0].semantic.target.target = 'no-such-part' expect(() => validateMorfo(broken)).toThrow(/targets unknown part/) }) it('fails when a prewrite attr is not declared on the target part', () => { const broken = cloneMorfo(dialogMorfo) const action = (broken.events as MorfoEvent[]).find((event) => event.name === 'close-save')! action.prewrite![0].attr = 'data-bogus' expect(() => validateMorfo(broken)).toThrow(/data-bogus.*is not declared/) }) it('fails when a prewrite writes a value outside the declared enum', () => { const broken = cloneMorfo(dialogMorfo) const action = (broken.events as MorfoEvent[]).find((event) => event.name === 'close-save')! action.prewrite![0].value = 'not-in-enum' expect(() => validateMorfo(broken)).toThrow(/not-in-enum.*is not declared/) }) it('fails when an event commits a non-existent state', () => { const broken = cloneMorfo(dialogMorfo) const action = (broken.events as MorfoEvent[]).find((event) => event.name === 'close-save')! if (!action.commits) throw new Error('close-save commits missing') action.commits.value = 'zombied' expect(() => validateMorfo(broken)).toThrow(/zombied.*not declared/) }) it('fails when two events share the same name', () => { const broken = cloneMorfo(dialogMorfo) ;(broken.events as MorfoEvent[])[1].name = 'open' expect(() => validateMorfo(broken)).toThrow(/duplicate event name "open"/) }) it('fails when data-last-action declares a value that no event prewrites', () => { const broken = cloneMorfo(dialogMorfo) const content = broken.parts.find((part) => part.kebab === 'content')! const dataLastAction = content.data.find((entry) => entry.attr === 'data-last-action')! ;(dataLastAction.values as string[]) = [...(dataLastAction.values ?? []), 'orphan-value'] expect(() => validateMorfo(broken)).toThrow(/orphan-value.*no event prewrites/) }) })