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.
97 lines
2.8 KiB
97 lines
2.8 KiB
import { describe, expect, it } from 'vitest'
|
|
|
|
import { validateMorfo, MorfoInvariantError } from '../schema'
|
|
import type { Morfo } from '../types'
|
|
import { toastMorfo } from './toast'
|
|
|
|
function cloneMorfo(m: typeof toastMorfo): Morfo {
|
|
return structuredClone(m as Morfo) as Morfo
|
|
}
|
|
|
|
describe('toastMorfo semantic contract', () => {
|
|
it('passes shape + invariant validation', () => {
|
|
expect(() => validateMorfo(toastMorfo)).not.toThrow()
|
|
})
|
|
|
|
it('declares announce semantics and supported intents in morfo', () => {
|
|
const announce = toastMorfo.events?.find((event) => event.name === 'announce')
|
|
expect(announce).toBeDefined()
|
|
expect(announce?.semantic).toMatchObject({
|
|
family: 'alert',
|
|
intent: {
|
|
fromProp: 'intent',
|
|
default: 'neutral',
|
|
supported: ['neutral', 'affirm', 'fulfill', 'risk', 'threat']
|
|
}
|
|
})
|
|
})
|
|
|
|
it('declares intent-driven role and aria-live on the item contract', () => {
|
|
const item = toastMorfo.parts.find((part) => part.kebab === 'item')
|
|
const role = item?.aria.find((entry) => entry.attr === 'role')
|
|
const live = item?.aria.find((entry) => entry.attr === 'aria-live')
|
|
|
|
expect(role).toMatchObject({
|
|
attr: 'role',
|
|
value: {
|
|
kind: 'mapRef',
|
|
source: { kind: 'propRef', prop: 'intent' },
|
|
map: {
|
|
neutral: 'status',
|
|
affirm: 'status',
|
|
fulfill: 'status',
|
|
risk: 'alert',
|
|
threat: 'alert'
|
|
}
|
|
}
|
|
})
|
|
|
|
expect(live).toMatchObject({
|
|
attr: 'aria-live',
|
|
value: {
|
|
kind: 'mapRef',
|
|
source: { kind: 'propRef', prop: 'intent' },
|
|
map: {
|
|
neutral: 'polite',
|
|
affirm: 'polite',
|
|
fulfill: 'polite',
|
|
risk: 'assertive',
|
|
threat: 'assertive'
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
it('fails when the default intent is not in the supported list', () => {
|
|
const broken = cloneMorfo(toastMorfo)
|
|
const announce = broken.events?.find((event) => event.name === 'announce')
|
|
if (!announce || !('intent' in announce.semantic) || typeof announce.semantic.intent === 'string') {
|
|
throw new Error('announce semantic intent binding missing in fixture')
|
|
}
|
|
|
|
announce.semantic.intent.default = 'neutral'
|
|
announce.semantic.intent.supported = ['affirm', 'risk']
|
|
|
|
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
|
|
expect(() => validateMorfo(broken)).toThrow(/default intent "neutral" must be included/)
|
|
})
|
|
|
|
it('fails when an intent map references an unknown local state', () => {
|
|
const broken = cloneMorfo(toastMorfo)
|
|
const item = broken.parts.find((part) => part.kebab === 'item')
|
|
if (!item) throw new Error('item part missing in fixture')
|
|
|
|
item.aria[0] = {
|
|
attr: 'role',
|
|
value: {
|
|
kind: 'mapRef',
|
|
source: { kind: 'stateRef', state: 'missing' },
|
|
map: { true: 'alert', false: 'status' }
|
|
}
|
|
}
|
|
|
|
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
|
|
expect(() => validateMorfo(broken)).toThrow(/stateRef "missing"/)
|
|
})
|
|
})
|