// @vitest-environment jsdom import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { createActiveApp, type ActiveApp } from '$active-app'; import { createActiveDom } from '$adom'; import { createActivePrefs, createActivePrefsDomProjection, standardPrefsDimensions } from '$prefs'; 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'; import { commonLangs } from './langs'; const minimalLang = { schema: {} satisfies LangNode, defaultLocale: 'es' as const }; const prefsProjectionSchema = standardPrefsDimensions({ languages: ['es-ES', 'ar-EG'], locales: ['es-ES', 'ar-EG'], currencies: ['EUR'], defaults: { language: 'es-ES', locale: 'es-ES', currency: 'EUR' } }); const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(HERE, '..', '..'); const SOURCE_EXTENSIONS = ['.ts', '.svelte'] as const; function preferencesHarness() { return { getTheme: () => 'base', getMode: () => 'light' as const, getDensity: () => 'comfortable' as const, onPreferenceChange: () => () => {} }; } function listSourceFiles(root: string): string[] { const stat = statSync(root); if (stat.isFile()) { return SOURCE_EXTENSIONS.some((ext) => root.endsWith(ext)) ? [root] : []; } return readdirSync(root) .flatMap((entry) => listSourceFiles(join(root, entry))) .filter( (path) => !path.endsWith('.test.ts') && !path.includes(`${join('components', 'icon', 'lib')}`) ); } function grepSources(root: string, pattern: RegExp): string[] { return listSourceFiles(root).filter((file) => pattern.test(readFileSync(file, 'utf8'))); } function stripComments(source: string): string { return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1'); } function collectKnownMorfoDataAttrs(): Set { const attrs = new Set(); for (const file of listSourceFiles(join(HERE, 'morfo', 'components'))) { const source = stripComments(readFileSync(file, 'utf8')); const component = /kebab:\s*'([^']+)'/.exec(source)?.[1]; if (!component) continue; const partRe = /kebab:\s*'([^']+)'/g; let partMatch: RegExpExecArray | null; while ((partMatch = partRe.exec(source))) { const part = partMatch[1]; attrs.add(part === 'provider' ? `data-${component}` : `data-${component}-${part}`); } const dataRe = /attr:\s*'([^']+)'/g; let dataMatch: RegExpExecArray | null; while ((dataMatch = dataRe.exec(source))) { if (dataMatch[1].startsWith('data-')) attrs.add(dataMatch[1]); } } return attrs; } function collectDataAttrs(source: string): Set { const clean = stripComments(source); const attrs = new Set(); const stringRe = /['"`](data-[a-zA-Z0-9_-]+)['"`]/g; const inlineRe = /\b(data-[a-zA-Z0-9_-]+)(?=(?:=|\s|>|\/|}|\]))/g; let match: RegExpExecArray | null; while ((match = stringRe.exec(clean))) attrs.add(match[1]); while ((match = inlineRe.exec(clean))) attrs.add(match[1]); return attrs; } function toPascalName(kebab: string): string { return kebab .split('-') .map((segment) => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`) .join(''); } function collectPublicSomaComponentDirs(): string[] { const root = join(HERE, 'soma', 'components'); return readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .filter((name) => name !== 'internal' && existsSync(join(root, name, 'index.ts'))) .sort(); } function collectMorfoComponentNamesByScope(scope: string): string[] { const root = join(HERE, 'morfo', 'components'); return readdirSync(root) .filter((file) => file.endsWith('.ts') && !file.endsWith('.test.ts')) .filter((file) => { const source = stripComments(readFileSync(join(root, file), 'utf8')); return new RegExp(`scope:\\s*\\[[^\\]]*'${scope}'`).test(source); }) .map((file) => file.replace(/\.ts$/, '')) .sort(); } function collectComponentLangPathViolations(source: string, file: string): string[] { const clean = stripComments(source); const violations: string[] = []; const refRe = /#\?components\.([^|'"`\s]+)/g; let match: RegExpExecArray | null; while ((match = refRe.exec(clean))) { const path = match[1]; const segments = path.split('.'); if (segments.some((segment) => /[A-Z]/.test(segment))) { violations.push(`${file.replace(REPO_ROOT, '')}: components.${path}`); } } return violations; } function collectLangCatalogKeyViolations(node: unknown, prefix = 'components'): string[] { if (!node || typeof node !== 'object') return []; return Object.entries(node as Record).flatMap(([key, value]) => { const path = `${prefix}.${key}`; const ownViolation = /[A-Z]/.test(key) ? [path] : []; return [...ownViolation, ...collectLangCatalogKeyViolations(value, path)]; }); } function collectBrokenSomaComponentReadmeLinks(): string[] { const root = join(HERE, 'soma', 'components'); return collectPublicSomaComponentDirs().flatMap((dir) => { const file = join(root, dir, 'README.md'); const source = readFileSync(file, 'utf8'); const violations: string[] = []; const linkRe = /\]\((\.\.\/[^)]+README\.md)\)/g; let match: RegExpExecArray | null; while ((match = linkRe.exec(source))) { const target = join(root, dir, match[1]); if (!existsSync(target)) { violations.push(`${dir}/README.md -> ${match[1]}`); } } return violations; }); } 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(['clipboard', '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('guards ActiveUix as composition root, not Soma/Eidos bootstrapper', () => { const source = readFileSync(join(HERE, 'active-uix', 'active-uix.svelte.ts'), 'utf8'); expect(source).not.toMatch(/from ['"].*\/soma/); expect(source).not.toMatch(/from ['"].*\/eidos/); expect(source).not.toMatch(/\bSoma\.create\(/); expect(source).not.toMatch(/\bActiveEidos\.create\(/); expect(source).not.toMatch(/\bEidos\.create\(/); }); it('prefs DOM projection owns only cross-modal attrs', () => { expect(UIX_LAYER_CONTRACTS.prefsDomProjection.implementation).toBe( '$prefs.createActivePrefsDomProjection' ); expect(UIX_LAYER_CONTRACTS.prefsDomProjection.requires).toEqual(['prefs', 'dom']); expect(UIX_LAYER_CONTRACTS.prefsDomProjection.ownsAttrs).toEqual([ 'dir', 'data-motion', 'data-sound', 'data-haptic' ]); expect(UIX_LAYER_CONTRACTS.prefsDomProjection.forbiddenAttrs).toEqual([ 'data-theme', 'data-mode', 'data-density' ]); }); it('implements cross-modal projection in prefs, not ActiveUix', () => { const dom = createActiveDom(); const prefs = createActivePrefs({ schema: prefsProjectionSchema }); const target = document.createElement('div'); const projection = createActivePrefsDomProjection({ prefs, dom, target }); try { for (const attr of UIX_LAYER_CONTRACTS.prefsDomProjection.ownsAttrs) { expect(target.hasAttribute(attr), attr).toBe(true); } for (const attr of UIX_LAYER_CONTRACTS.prefsDomProjection.forbiddenAttrs) { expect(target.hasAttribute(attr), attr).toBe(false); } } finally { projection.dispose(); prefs.dispose(); dom.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('guards Soma components from legacy runtime registration APIs', () => { const violations = grepSources( join(HERE, 'soma', 'components'), /registerContract\(|registerPart\(/ ); expect(violations).toEqual([]); }); it('guards Soma component barrel exports all public component modules', () => { const indexSource = readFileSync(join(HERE, 'soma', 'components', 'index.ts'), 'utf8'); const missingExports = collectPublicSomaComponentDirs() .map((dir) => ({ dir, name: toPascalName(dir), exportRe: new RegExp(`export \\* as ${toPascalName(dir)} from ['"]\\./${dir}['"];`) })) .filter(({ exportRe }) => !exportRe.test(indexSource)) .map(({ dir, name }) => `${dir} -> ${name}`); expect(missingExports).toEqual([]); }); it('guards Soma Provider exports use the canonical root wrapper filename', () => { const violations = collectPublicSomaComponentDirs() .filter((dir) => { const source = readFileSync(join(HERE, 'soma', 'components', dir, 'exports.ts'), 'utf8'); const expected = `export { default as Provider } from './components/${dir}.svelte';`; return !source.includes(expected); }) .map((dir) => `${dir}/exports.ts`); expect(violations).toEqual([]); }); it('guards Soma public barrels from provider implementation filenames', () => { const violations = collectPublicSomaComponentDirs() .map((dir) => join(HERE, 'soma', 'components', dir, 'exports.ts')) .filter((file) => /from ['"][^'"]*provider\.svelte(?:\.ts)?['"]/.test(readFileSync(file, 'utf8')) ) .map((file) => file.replace(REPO_ROOT, '')); expect(violations).toEqual([]); }); it('guards Soma public barrels from shared lib facades', () => { const violations = collectPublicSomaComponentDirs() .map((dir) => join(HERE, 'soma', 'components', dir, 'exports.ts')) .filter((file) => /from ['"]\$libs\//.test(readFileSync(file, 'utf8'))) .map((file) => file.replace(REPO_ROOT, '')); expect(violations).toEqual([]); }); it('guards Soma public component modules against Morfo soma scope', () => { expect(collectPublicSomaComponentDirs()).toEqual(collectMorfoComponentNamesByScope('soma')); }); it('guards Soma public component modules with local README docs', () => { const missingReadmes = collectPublicSomaComponentDirs().filter( (dir) => !existsSync(join(HERE, 'soma', 'components', dir, 'README.md')) ); expect(missingReadmes).toEqual([]); }); it('guards Soma component README links to existing component docs', () => { expect(collectBrokenSomaComponentReadmeLinks()).toEqual([]); }); it('guards Soma from external re-export facades', () => { expect(existsSync(join(HERE, 'soma', 'external'))).toBe(false); const violations = grepSources(join(HERE, 'soma'), /external\/(?:dates|colors)/); expect(violations).toEqual([]); }); it('guards Soma internals from importing their own public alias', () => { const violations = listSourceFiles(join(HERE, 'soma')) .filter((file) => { const source = stripComments(readFileSync(file, 'utf8')); return /from ['"]\$soma(?:['"]|\/)/.test(source); }) .map((file) => file.replace(REPO_ROOT, '')); expect(violations).toEqual([]); }); it('guards Soma from direct mutable DOM writes', () => { const violations = grepSources( join(HERE, 'soma'), /\.setAttribute\(|\.removeAttribute\(|\.toggleAttribute\(|\.classList\.|\.style\.setProperty\(|\.style\.[a-zA-Z]+\s*=/ ); expect(violations).toEqual([]); }); it('guards Soma component timers through the injected scheduler', () => { const allowedFallback = join(HERE, 'soma', 'components', 'announce', 'global.svelte.ts'); const violations = listSourceFiles(join(HERE, 'soma', 'components')).filter((file) => { if (file === allowedFallback) return false; return /\bsetTimeout\(|\bclearTimeout\(|\bsetInterval\(|\bclearInterval\(/.test( stripComments(readFileSync(file, 'utf8')) ); }); expect(violations).toEqual([]); }); it('guards Soma components through the shared typeahead helper', () => { const violations = grepSources( join(HERE, 'soma', 'components'), /class\s+Typeahead|typeaheadBuffer|typeaheadTimerKey|clearTypeaheadTimer/ ); expect(violations).toEqual([]); }); it('guards Soma component listeners through ActiveDom', () => { const violations = grepSources( join(HERE, 'soma', 'components'), /\.(addEventListener|removeEventListener)\(/ ); expect(violations).toEqual([]); }); it('guards hardcoded Soma component data attrs with morfo contracts', () => { const knownAttrs = collectKnownMorfoDataAttrs(); const violations = listSourceFiles(join(HERE, 'soma', 'components')).flatMap((file) => { const attrs = collectDataAttrs(readFileSync(file, 'utf8')); return [...attrs] .filter((attr) => !knownAttrs.has(attr)) .map((attr) => `${file.replace(REPO_ROOT, '')}: ${attr}`); }); expect(violations).toEqual([]); }); it('guards component translation namespaces as kebab-case', () => { const sourceRoots = [join(HERE, 'soma', 'components'), join(HERE, 'morfo', 'components')]; const pathViolations = sourceRoots.flatMap((root) => listSourceFiles(root).flatMap((file) => collectComponentLangPathViolations(readFileSync(file, 'utf8'), file) ) ); expect(pathViolations).toEqual([]); expect(collectLangCatalogKeyViolations(commonLangs, 'common')).toEqual([]); }); 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(existsSync(new URL('./eidos/core/headless', import.meta.url))).toBe(false); expect(UIX_LAYER_CONTRACTS.activeEidos.constructionOptionalServices).toEqual([ 'langs', 'format', 'prefs' ]); expect(UIX_LAYER_CONTRACTS.activeEidos.requiredAccessors).toEqual(['langs', 'prefs']); expect(UIX_LAYER_CONTRACTS.activeEidos.optionalAccessors).toEqual(['format']); expect(() => createActiveEidos({ preferences: preferencesHarness() })).toThrow( ActiveEidosConfigError ); const eidos = createActiveEidos({ preferences: preferencesHarness(), applyDom: false }); expect(eidos.renderStaticCss()).toContain('--size-md-control-height'); expect(() => eidos.prefs).toThrow(ActiveEidosConfigError); expect(eidos.format).toBeUndefined(); eidos.dispose(); }); it('guards Eidos public surface from wrapper/engine reintroduction', () => { const eidosRoot = join(HERE, 'eidos'); const violations = grepSources( eidosRoot, /\bEngineEidos\b|\bclass\s+Eidos\b|\bEidos\.create\(|from ['"].*\/engine/ ); expect(violations).toEqual([]); }); it('guards UIX docs shell from writing visual prefs through ActivePrefs', () => { const violations = grepSources( join(REPO_ROOT, 'web', 'routes', 'uix'), /prefs\.theme|setIntent\(['"]theme['"]/ ); expect(violations).toEqual([]); }); 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(); } }); });