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.
194 lines
6.0 KiB
194 lines
6.0 KiB
|
5 months ago
|
// @vitest-environment jsdom
|
||
|
|
|
||
|
|
import { existsSync } from 'node:fs';
|
||
|
|
import { describe, expect, it } from 'vitest';
|
||
|
|
|
||
|
|
import { createActiveApp, type ActiveApp } from '$active-app';
|
||
|
|
import { createActiveDom } from '$adom';
|
||
|
|
import type { LangNode } from '$libs/langs';
|
||
|
|
import { state } from '$libs/reactive';
|
||
|
|
import {
|
||
|
|
ActiveUixMissingServiceError,
|
||
|
|
attachActiveUix,
|
||
|
|
createActiveUix
|
||
|
|
} from './active-uix/active-uix.svelte';
|
||
|
|
import { defineUixServices, UIX_REQUIRED_SERVICES } from './active-uix/services';
|
||
|
|
import { createActiveEidos, ActiveEidosConfigError } from './eidos/active-eidos.svelte';
|
||
|
|
import {
|
||
|
|
_resetMorfoRegistryForTesting,
|
||
|
|
connectMorfoTranslations,
|
||
|
|
registerMorfo,
|
||
|
|
type Morfo,
|
||
|
|
v
|
||
|
|
} from './morfo';
|
||
|
|
import { toggleMorfo } from './morfo/components/toggle';
|
||
|
|
import { EngineSemantic, defineEngineSemantic } from './sema';
|
||
|
|
import { SemaConfigError } from './sema/errors';
|
||
|
|
import { createSomaRuntime } from './soma/runtime.svelte';
|
||
|
|
import { UIX_LAYER_CONTRACTS } from './contracts';
|
||
|
|
|
||
|
|
const minimalLang = { schema: {} satisfies LangNode, defaultLocale: 'es' as const };
|
||
|
|
|
||
|
|
function preferencesHarness() {
|
||
|
|
return {
|
||
|
|
getTheme: () => 'base',
|
||
|
|
getMode: () => 'light' as const,
|
||
|
|
getDensity: () => 'comfortable' as const,
|
||
|
|
onPreferenceChange: () => () => {}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('UIX layer contracts', () => {
|
||
|
|
it('pins ActiveUix public service names and rejects legacy aliases', () => {
|
||
|
|
const uix = createActiveUix({ langs: minimalLang });
|
||
|
|
try {
|
||
|
|
for (const key of UIX_LAYER_CONTRACTS.activeUix.publicSurface) {
|
||
|
|
expect(key in uix, key).toBe(true);
|
||
|
|
}
|
||
|
|
for (const key of UIX_LAYER_CONTRACTS.activeUix.forbiddenPublicSurface) {
|
||
|
|
expect(key in uix, key).toBe(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(uix.ownsApp).toBe(true);
|
||
|
|
expect(uix.app).toBeUndefined();
|
||
|
|
expect(uix.events).toBeInstanceOf(EngineSemantic);
|
||
|
|
} finally {
|
||
|
|
uix.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('keeps attach mode as consumption-only: required services are langs + dom', () => {
|
||
|
|
expect(UIX_REQUIRED_SERVICES).toEqual(UIX_LAYER_CONTRACTS.activeUix.attachRequires);
|
||
|
|
|
||
|
|
const app = createActiveApp({ services: {} });
|
||
|
|
try {
|
||
|
|
attachActiveUix(app as unknown as ActiveApp);
|
||
|
|
expect.fail('attachActiveUix should reject an app without UIX services');
|
||
|
|
} catch (err) {
|
||
|
|
if (!(err instanceof ActiveUixMissingServiceError)) throw err;
|
||
|
|
expect(err.missing).toEqual(['langs', 'dom']);
|
||
|
|
expect(err.missing).not.toContain('events');
|
||
|
|
} finally {
|
||
|
|
app.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('defineUixServices publishes events, never semantic, and cannot attach without dom', () => {
|
||
|
|
const services = defineUixServices({ langs: minimalLang });
|
||
|
|
expect(Object.keys(services).sort()).toEqual(['dom', 'events', 'format', 'langs']);
|
||
|
|
expect('semantic' in services).toBe(false);
|
||
|
|
expect('lang' in services).toBe(false);
|
||
|
|
expect('frontend' in services).toBe(false);
|
||
|
|
|
||
|
|
const noDomServices = defineUixServices({ langs: minimalLang, dom: false });
|
||
|
|
expect('dom' in noDomServices).toBe(false);
|
||
|
|
expect('events' in noDomServices).toBe(false);
|
||
|
|
|
||
|
|
const app = createActiveApp({ services: noDomServices });
|
||
|
|
try {
|
||
|
|
expect(() => attachActiveUix(app as unknown as ActiveApp)).toThrow(
|
||
|
|
ActiveUixMissingServiceError
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
app.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('SomaRuntime has one public part API and only depends on injected services', async () => {
|
||
|
|
const dom = createActiveDom();
|
||
|
|
try {
|
||
|
|
const runtime = createSomaRuntime(toggleMorfo, { dom });
|
||
|
|
for (const key of UIX_LAYER_CONTRACTS.somaRuntime.publicSurface) {
|
||
|
|
expect(key in runtime, key).toBe(true);
|
||
|
|
}
|
||
|
|
for (const key of UIX_LAYER_CONTRACTS.somaRuntime.forbiddenPublicSurface) {
|
||
|
|
expect(key in runtime, key).toBe(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
const id = state('toggle-contract');
|
||
|
|
runtime.part('provider', { id });
|
||
|
|
expect(runtime.partProps('provider')).toMatchObject({
|
||
|
|
id: 'toggle-contract',
|
||
|
|
'data-toggle': ''
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
dom.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('events/sema uses events as service name and requires dom or projector for visual projection', () => {
|
||
|
|
const factory = defineEngineSemantic({ visual: false });
|
||
|
|
expect(factory.name).toBe(UIX_LAYER_CONTRACTS.events.publicServiceName);
|
||
|
|
expect(factory.serviceDependencies).toEqual(['dom']);
|
||
|
|
expect(UIX_LAYER_CONTRACTS.events.morfoPayloadName).toBe('semantic');
|
||
|
|
expect(UIX_LAYER_CONTRACTS.events.requiresOneOfForVisualProjection).toEqual([
|
||
|
|
'dom',
|
||
|
|
'projector'
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect(() => new EngineSemantic()).toThrow(SemaConfigError);
|
||
|
|
expect(() => new EngineSemantic({ visual: false })).not.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ActiveEidos is the visual runtime and does not recreate an engine layer', () => {
|
||
|
|
expect(UIX_LAYER_CONTRACTS.activeEidos.publicRuntime).toBe('ActiveEidos');
|
||
|
|
expect(existsSync(new URL('./eidos/engine', import.meta.url))).toBe(false);
|
||
|
|
|
||
|
|
expect(() => createActiveEidos({ preferences: preferencesHarness() })).toThrow(
|
||
|
|
ActiveEidosConfigError
|
||
|
|
);
|
||
|
|
const eidos = createActiveEidos({
|
||
|
|
preferences: preferencesHarness(),
|
||
|
|
applyDom: false
|
||
|
|
});
|
||
|
|
expect(eidos.renderStaticCss()).toContain('--size-md-control-height');
|
||
|
|
eidos.dispose();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Morfo owns translations declaratively and only registers them into a target', () => {
|
||
|
|
_resetMorfoRegistryForTesting();
|
||
|
|
const applied: Array<{ namespace: string; module: LangNode }> = [];
|
||
|
|
const detach = connectMorfoTranslations({
|
||
|
|
extend(namespace, module) {
|
||
|
|
applied.push({ namespace, module });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
registerMorfo({
|
||
|
|
name: 'ContractSample',
|
||
|
|
kebab: 'contract-sample',
|
||
|
|
scope: ['soma'],
|
||
|
|
translations: {
|
||
|
|
label: { es: 'Contrato', en: 'Contract' }
|
||
|
|
},
|
||
|
|
parts: [
|
||
|
|
{
|
||
|
|
name: 'Provider',
|
||
|
|
kebab: 'provider',
|
||
|
|
kind: 'public',
|
||
|
|
defaultElement: 'div',
|
||
|
|
optional: false,
|
||
|
|
data: [],
|
||
|
|
aria: [
|
||
|
|
{
|
||
|
|
attr: 'aria-label',
|
||
|
|
value: v.translationRef('label', 'Contract')
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
} satisfies Morfo);
|
||
|
|
|
||
|
|
expect(applied).toHaveLength(1);
|
||
|
|
expect(applied[0]?.namespace).toBe('components.contract-sample');
|
||
|
|
expect(applied[0]?.module).toEqual({
|
||
|
|
label: { es: 'Contrato', en: 'Contract' }
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
detach();
|
||
|
|
_resetMorfoRegistryForTesting();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|