You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/morfo/components/dialog.test.ts

129 lines
5.2 KiB

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/)
})
})

Powered by TurnKey Linux.