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.
62 lines
2.5 KiB
62 lines
2.5 KiB
import { describe, it, expect } from 'vitest';
|
|
import { validateMorfo, MorfoInvariantError } from '../schema';
|
|
import type { Morfo } from '../types';
|
|
import { dialogMorfo } from './dialog';
|
|
|
|
// `dialogMorfo` is authored `as const satisfies Morfo` so runtime
|
|
// assertions on its literal shape stay precise. Invariant tests below
|
|
// clone it into a mutable `Morfo` to deliberately corrupt fields.
|
|
function cloneMutable(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 data-last-action on Content for Sema 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('fails validation when a partRef targets a non-existent kebab', () => {
|
|
const broken = cloneMutable(dialogMorfo);
|
|
const trigger = broken.parts.find((p) => p.kebab === 'trigger')!;
|
|
const controls = trigger.aria.find((a) => a.attr === 'aria-controls')!;
|
|
// Force a partRef to a non-existent target.
|
|
(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 = cloneMutable(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 two parts share the same kebab', () => {
|
|
const broken = cloneMutable(dialogMorfo);
|
|
broken.parts[1].kebab = 'content'; // trigger → content (duplicate)
|
|
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError);
|
|
});
|
|
|
|
it('fails validation with empty scope', () => {
|
|
const broken = cloneMutable(dialogMorfo);
|
|
broken.scope = [];
|
|
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError);
|
|
});
|
|
});
|