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.
211 lines
6.8 KiB
211 lines
6.8 KiB
import { describe, expect, it } from 'vitest'
|
|
|
|
import { SEMA_VERBS, isSemaVerb, validateEventName, familyForVerb } from './verbs'
|
|
|
|
describe('SEMA_VERBS', () => {
|
|
it('groups verbs by family per book canon', () => {
|
|
// Canonical verbs per «Diseñando lo que ocurre» cap. 22-29
|
|
expect(SEMA_VERBS.contact).toContain('press')
|
|
expect(SEMA_VERBS.commit).toContain('toggle')
|
|
expect(SEMA_VERBS.commit).toContain('save')
|
|
expect(SEMA_VERBS.signal).toContain('announce')
|
|
expect(SEMA_VERBS.signal).toContain('warn')
|
|
expect(SEMA_VERBS.signal).toContain('inform') // cap. 24
|
|
expect(SEMA_VERBS.handle).toContain('drag')
|
|
expect(SEMA_VERBS.emerge).toContain('present')
|
|
expect(SEMA_VERBS.emerge).toContain('dismiss')
|
|
expect(SEMA_VERBS.shift).toContain('enter-mode')
|
|
expect(SEMA_VERBS.sustain).toContain('progress')
|
|
})
|
|
|
|
it('exposes the delegate family (book cap. 29)', () => {
|
|
// Eighth family: who acts. Required for workflows, IA, macros,
|
|
// rule-based automation. Per book Apéndice C case 3.
|
|
expect(SEMA_VERBS.delegate).toContain('offer')
|
|
expect(SEMA_VERBS.delegate).toContain('plan')
|
|
expect(SEMA_VERBS.delegate).toContain('authorize')
|
|
expect(SEMA_VERBS.delegate).toContain('act')
|
|
expect(SEMA_VERBS.delegate).toContain('review')
|
|
expect(SEMA_VERBS.delegate).toContain('escalate')
|
|
expect(SEMA_VERBS.delegate).toContain('return')
|
|
})
|
|
|
|
it('exposes contextual commit verbs used in the book case studies', () => {
|
|
// cap. 29 + cap. 30 + apéndice C
|
|
expect(SEMA_VERBS.commit).toContain('apply')
|
|
expect(SEMA_VERBS.commit).toContain('partial')
|
|
expect(SEMA_VERBS.commit).toContain('block')
|
|
expect(SEMA_VERBS.commit).toContain('upload')
|
|
})
|
|
|
|
it('has no internal duplicates per family', () => {
|
|
for (const verbs of Object.values(SEMA_VERBS)) {
|
|
expect(new Set(verbs).size).toBe(verbs.length)
|
|
}
|
|
})
|
|
|
|
it('moved select / toggle / acknowledge to commit (per doctrine)', () => {
|
|
// These were `contact` / `handle` in pre-doctrina shape.
|
|
expect(SEMA_VERBS.commit).toContain('select')
|
|
expect(SEMA_VERBS.commit).toContain('toggle')
|
|
expect(SEMA_VERBS.commit).toContain('acknowledge')
|
|
expect(SEMA_VERBS.contact as readonly string[]).not.toContain('select')
|
|
expect(SEMA_VERBS.contact as readonly string[]).not.toContain('toggle')
|
|
expect(SEMA_VERBS.handle as readonly string[]).not.toContain('acknowledge')
|
|
})
|
|
})
|
|
|
|
describe('isSemaVerb', () => {
|
|
it('accepts canonical verbs', () => {
|
|
expect(isSemaVerb('present')).toBe(true)
|
|
expect(isSemaVerb('toggle')).toBe(true)
|
|
expect(isSemaVerb('save')).toBe(true)
|
|
})
|
|
|
|
it('rejects non-canonical strings', () => {
|
|
expect(isSemaVerb('frob')).toBe(false)
|
|
expect(isSemaVerb('commit-save')).toBe(false) // composite, not a verb
|
|
})
|
|
|
|
it('rejects family names (they are not verbs)', () => {
|
|
expect(isSemaVerb('commit')).toBe(false)
|
|
expect(isSemaVerb('emerge')).toBe(false)
|
|
expect(isSemaVerb('signal')).toBe(false)
|
|
expect(isSemaVerb('delegate')).toBe(false)
|
|
})
|
|
|
|
it('rejects non-strings', () => {
|
|
expect(isSemaVerb(undefined)).toBe(false)
|
|
expect(isSemaVerb(42)).toBe(false)
|
|
expect(isSemaVerb(null)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('familyForVerb', () => {
|
|
it('resolves the family of a canonical verb', () => {
|
|
expect(familyForVerb('press')).toBe('contact')
|
|
expect(familyForVerb('toggle')).toBe('commit')
|
|
expect(familyForVerb('dismiss')).toBe('emerge')
|
|
expect(familyForVerb('warn')).toBe('signal')
|
|
expect(familyForVerb('inform')).toBe('signal')
|
|
expect(familyForVerb('drag')).toBe('handle')
|
|
expect(familyForVerb('enter-mode')).toBe('shift')
|
|
expect(familyForVerb('progress')).toBe('sustain')
|
|
expect(familyForVerb('authorize')).toBe('delegate')
|
|
expect(familyForVerb('escalate')).toBe('delegate')
|
|
})
|
|
|
|
it('returns undefined for non-canonical strings', () => {
|
|
expect(familyForVerb('frob')).toBeUndefined()
|
|
expect(familyForVerb('commit')).toBeUndefined() // family name, not a verb
|
|
})
|
|
})
|
|
|
|
describe('validateEventName', () => {
|
|
it('matches a bare canonical verb', () => {
|
|
expect(validateEventName('dismiss')).toEqual({
|
|
name: 'dismiss',
|
|
head: 'dismiss',
|
|
matchesCanonical: true,
|
|
variant: undefined,
|
|
family: 'emerge',
|
|
verb: 'dismiss'
|
|
})
|
|
})
|
|
|
|
it('extracts head + variant from verb-variant composites', () => {
|
|
expect(validateEventName('dismiss-outside')).toEqual({
|
|
name: 'dismiss-outside',
|
|
head: 'dismiss',
|
|
matchesCanonical: true,
|
|
variant: 'outside',
|
|
family: 'emerge',
|
|
verb: 'dismiss'
|
|
})
|
|
expect(validateEventName('close-cancel')).toEqual({
|
|
name: 'close-cancel',
|
|
head: 'close',
|
|
matchesCanonical: true,
|
|
variant: 'cancel',
|
|
family: 'emerge',
|
|
verb: 'close'
|
|
})
|
|
})
|
|
|
|
it('parses family-verb naming (doctrinal shape)', () => {
|
|
expect(validateEventName('commit-toggle')).toEqual({
|
|
name: 'commit-toggle',
|
|
head: 'commit',
|
|
matchesCanonical: true,
|
|
variant: 'toggle',
|
|
family: 'commit',
|
|
verb: 'toggle'
|
|
})
|
|
expect(validateEventName('commit-save')).toEqual({
|
|
name: 'commit-save',
|
|
head: 'commit',
|
|
matchesCanonical: true,
|
|
variant: 'save',
|
|
family: 'commit',
|
|
verb: 'save'
|
|
})
|
|
})
|
|
|
|
it('flags drift when neither head nor family.verb is canonical', () => {
|
|
expect(validateEventName('frob-glob')).toEqual({
|
|
name: 'frob-glob',
|
|
head: 'frob',
|
|
matchesCanonical: false,
|
|
variant: 'glob',
|
|
family: undefined,
|
|
verb: undefined
|
|
})
|
|
})
|
|
|
|
it('flags drift when family is canonical but the verb is not', () => {
|
|
expect(validateEventName('commit-frob').matchesCanonical).toBe(false)
|
|
expect(validateEventName('commit-frob').family).toBe('commit')
|
|
})
|
|
|
|
it('recognises multi-word canonical verbs (enter-mode, exit-mode)', () => {
|
|
// Family-verb shape with multi-word verb.
|
|
expect(validateEventName('shift-enter-mode')).toEqual({
|
|
name: 'shift-enter-mode',
|
|
head: 'shift',
|
|
matchesCanonical: true,
|
|
variant: 'enter-mode',
|
|
family: 'shift',
|
|
verb: 'enter-mode'
|
|
})
|
|
expect(validateEventName('shift-exit-mode').matchesCanonical).toBe(true)
|
|
// With trailing variant.
|
|
expect(validateEventName('shift-enter-mode-edit').matchesCanonical).toBe(true)
|
|
expect(validateEventName('shift-enter-mode-edit').verb).toBe('enter-mode')
|
|
// Bare verb shape — `enter-mode` alone is canonical.
|
|
expect(validateEventName('enter-mode').matchesCanonical).toBe(true)
|
|
expect(validateEventName('enter-mode').verb).toBe('enter-mode')
|
|
expect(validateEventName('enter-mode').family).toBe('shift')
|
|
})
|
|
|
|
it('reports the existing dialog event names against the catalog', () => {
|
|
const dialogEvents = [
|
|
'open',
|
|
'close-save',
|
|
'close-cancel',
|
|
'close-dismiss',
|
|
'close-dismiss-outside',
|
|
'close-after-fail'
|
|
]
|
|
for (const name of dialogEvents) {
|
|
const v = validateEventName(name)
|
|
expect(v.matchesCanonical).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('reports the existing toast event names against the catalog', () => {
|
|
for (const name of ['present', 'announce', 'dismiss']) {
|
|
expect(validateEventName(name).matchesCanonical).toBe(true)
|
|
}
|
|
})
|
|
})
|