diff --git a/packages/noodl-editor/tests/models/ElementConfigRegistry.test.ts b/packages/noodl-editor/tests/models/ElementConfigRegistry.test.ts new file mode 100644 index 0000000..88813f2 --- /dev/null +++ b/packages/noodl-editor/tests/models/ElementConfigRegistry.test.ts @@ -0,0 +1,361 @@ +/** + * ElementConfigRegistry Tests + * + * Unit tests for the ElementConfigRegistry and ButtonConfig. + * Tests registration, retrieval, validation, and config application. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { describe, it, expect, beforeEach } from '@jest/globals'; + +import { ButtonConfig } from '../../src/editor/src/models/ElementConfigs/configs/ButtonConfig'; +import { ElementConfigRegistry } from '../../src/editor/src/models/ElementConfigs/ElementConfigRegistry'; +import type { ElementConfig } from '../../src/editor/src/models/ElementConfigs/ElementConfigTypes'; + +describe('ElementConfigRegistry', () => { + let registry: ElementConfigRegistry; + + beforeEach(() => { + // Get fresh registry instance and clear it + registry = ElementConfigRegistry.instance; + registry.clear(); + }); + + describe('Singleton Pattern', () => { + it('should return the same instance', () => { + const instance1 = ElementConfigRegistry.instance; + const instance2 = ElementConfigRegistry.instance; + expect(instance1).toBe(instance2); + }); + }); + + describe('Config Registration', () => { + it('should register ButtonConfig successfully', () => { + const result = registry.register(ButtonConfig); + expect(result).toBe(true); + expect(registry.getCount()).toBe(1); + }); + + it('should retrieve registered config', () => { + registry.register(ButtonConfig); + const config = registry.get('net.noodl.visual.button'); + expect(config).toBeDefined(); + expect(config?.nodeType).toBe('net.noodl.visual.button'); + }); + + it('should not register duplicate without override', () => { + registry.register(ButtonConfig); + const result = registry.register(ButtonConfig); + expect(result).toBe(false); + expect(registry.getCount()).toBe(1); + }); + + it('should override existing config when override is true', () => { + registry.register(ButtonConfig); + const result = registry.register(ButtonConfig, { override: true }); + expect(result).toBe(true); + expect(registry.getCount()).toBe(1); + }); + + it('should check if config exists', () => { + expect(registry.has('net.noodl.visual.button')).toBe(false); + registry.register(ButtonConfig); + expect(registry.has('net.noodl.visual.button')).toBe(true); + }); + }); + + describe('ButtonConfig Structure', () => { + beforeEach(() => { + registry.register(ButtonConfig); + }); + + it('should have 6 variants', () => { + const variants = registry.getVariants('net.noodl.visual.button'); + expect(variants).toHaveLength(6); + expect(variants).toContain('primary'); + expect(variants).toContain('secondary'); + expect(variants).toContain('outline'); + expect(variants).toContain('ghost'); + expect(variants).toContain('destructive'); + expect(variants).toContain('link'); + }); + + it('should have 4 size presets', () => { + const sizes = registry.getSizes('net.noodl.visual.button'); + expect(sizes).toHaveLength(4); + expect(sizes).toContain('sm'); + expect(sizes).toContain('md'); + expect(sizes).toContain('lg'); + expect(sizes).toContain('xl'); + }); + + it('should retrieve specific variant', () => { + const primary = registry.getVariant('net.noodl.visual.button', 'primary'); + expect(primary).toBeDefined(); + expect(primary?.backgroundColor).toBe('var(--primary)'); + expect(primary?.color).toBe('var(--primary-foreground)'); + }); + + it('should retrieve specific size', () => { + const md = registry.getSize('net.noodl.visual.button', 'md'); + expect(md).toBeDefined(); + expect(md?.fontSize).toBe('var(--text-sm)'); + expect(md?.height).toBe('40px'); + }); + + it('should have states in variants', () => { + const primary = registry.getVariant('net.noodl.visual.button', 'primary'); + expect(primary?.states).toBeDefined(); + expect(primary?.states?.hover).toBeDefined(); + expect(primary?.states?.active).toBeDefined(); + expect(primary?.states?.disabled).toBeDefined(); + }); + }); + + describe('Config Validation', () => { + it('should validate ButtonConfig as valid', () => { + const validation = registry.validate(ButtonConfig); + expect(validation.valid).toBe(true); + expect(validation.errors).toHaveLength(0); + }); + + it('should detect invalid config (missing nodeType)', () => { + const invalidConfig = { + defaults: {}, + variants: { test: {} } + } as unknown as ElementConfig; + + const validation = registry.validate(invalidConfig); + expect(validation.valid).toBe(false); + expect(validation.errors).toContain('nodeType is required'); + }); + + it('should detect invalid config (missing variants)', () => { + const invalidConfig = { + nodeType: 'test', + defaults: {}, + variants: {} + } as ElementConfig; + + const validation = registry.validate(invalidConfig); + expect(validation.valid).toBe(false); + expect(validation.errors).toContain('At least one variant is required'); + }); + }); + + describe('Apply Defaults', () => { + beforeEach(() => { + registry.register(ButtonConfig); + }); + + it('should apply defaults to a mock node', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-1', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: {} as Record + }; + + const result = registry.applyDefaults(mockNode); + expect(result).toBe(true); + expect(mockNode.parameters.fontSize).toBe('var(--text-sm)'); + expect(mockNode.parameters.borderRadius).toBe('var(--radius-md)'); + expect(mockNode.parameters._variant).toBe('primary'); + }); + + it('should not override existing parameters', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-2', + parameters: { + fontSize: 'custom-size' + } as Record + }; + + registry.applyDefaults(mockNode); + expect(mockNode.parameters.fontSize).toBe('custom-size'); // Should not be overridden + expect(mockNode.parameters.borderRadius).toBe('var(--radius-md)'); // Should be added + }); + + it('should return false for unknown node type', () => { + const mockNode = { + type: 'unknown.node.type', + id: 'test-unknown', + parameters: {} as Record + }; + + const result = registry.applyDefaults(mockNode); + expect(result).toBe(false); + }); + }); + + describe('Apply Variant', () => { + beforeEach(() => { + registry.register(ButtonConfig); + }); + + it('should apply variant to node', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-3', + parameters: {} as Record + }; + + const result = registry.applyVariant(mockNode, 'secondary'); + expect(result).toBe(true); + expect(mockNode.parameters.backgroundColor).toBe('var(--secondary)'); + expect(mockNode.parameters.color).toBe('var(--secondary-foreground)'); + expect(mockNode.parameters._variant).toBe('secondary'); + }); + + it('should apply variant with size', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-4', + parameters: {} as Record + }; + + const result = registry.applyVariant(mockNode, { + variantName: 'outline', + size: 'lg' + }); + + expect(result).toBe(true); + expect(mockNode.parameters.backgroundColor).toBe('transparent'); + expect(mockNode.parameters.fontSize).toBe('var(--text-base)'); // From lg size + expect(mockNode.parameters.height).toBe('48px'); // From lg size + expect(mockNode.parameters._variant).toBe('outline'); + expect(mockNode.parameters._size).toBe('lg'); + }); + + it('should preserve user overrides by default', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-5', + parameters: { + customProperty: 'user-value', + _variant: 'primary' + } as Record + }; + + registry.applyVariant(mockNode, 'secondary'); + expect(mockNode.parameters.customProperty).toBe('user-value'); // Preserved + expect(mockNode.parameters._variant).toBe('secondary'); + }); + + it('should return false for unknown variant', () => { + const mockNode = { + type: 'net.noodl.visual.button', + id: 'test-button-6', + parameters: {} as Record + }; + + const result = registry.applyVariant(mockNode, 'unknown-variant'); + expect(result).toBe(false); + }); + }); + + describe('Resolve Styles', () => { + beforeEach(() => { + registry.register(ButtonConfig); + }); + + it('should resolve styles with defaults only', () => { + const result = registry.resolveStyles({ + nodeType: 'net.noodl.visual.button' + }); + + expect(result).not.toBeNull(); + expect(result?.styles.base.fontSize).toBe('var(--text-sm)'); + expect(result?.hasUserOverrides).toBe(false); + }); + + it('should resolve styles with variant', () => { + const result = registry.resolveStyles({ + nodeType: 'net.noodl.visual.button', + variant: 'destructive' + }); + + expect(result).not.toBeNull(); + expect(result?.styles.base.backgroundColor).toBe('var(--destructive)'); + expect(result?.appliedVariant).toBe('destructive'); + }); + + it('should resolve styles with size', () => { + const result = registry.resolveStyles({ + nodeType: 'net.noodl.visual.button', + variant: 'primary', + size: 'xl' + }); + + expect(result).not.toBeNull(); + expect(result?.styles.base.fontSize).toBe('var(--text-lg)'); // From xl size + expect(result?.styles.base.height).toBe('56px'); // From xl size + expect(result?.appliedSize).toBe('xl'); + }); + + it('should prioritize user overrides', () => { + const result = registry.resolveStyles({ + nodeType: 'net.noodl.visual.button', + variant: 'primary', + userOverrides: { + backgroundColor: '#custom-color' + } + }); + + expect(result).not.toBeNull(); + expect(result?.styles.base.backgroundColor).toBe('#custom-color'); + expect(result?.hasUserOverrides).toBe(true); + expect(result?.sources?.backgroundColor).toBe('user'); + }); + + it('should track style sources', () => { + const result = registry.resolveStyles({ + nodeType: 'net.noodl.visual.button', + variant: 'primary', + size: 'md' + }); + + expect(result?.sources).toBeDefined(); + expect(result?.sources?.fontSize).toBe('size'); // From size preset + expect(result?.sources?.backgroundColor).toBe('variant'); // From variant + }); + }); + + describe('Registry Summary', () => { + it('should provide empty summary initially', () => { + const summary = registry.getSummary(); + expect(summary.totalConfigs).toBe(0); + expect(summary.nodeTypes).toHaveLength(0); + }); + + it('should provide accurate summary after registration', () => { + registry.register(ButtonConfig); + const summary = registry.getSummary(); + + expect(summary.totalConfigs).toBe(1); + expect(summary.nodeTypes).toContain('net.noodl.visual.button'); + expect(summary.configDetails).toHaveLength(1); + expect(summary.configDetails[0].variantCount).toBe(6); + expect(summary.configDetails[0].sizeCount).toBe(4); + expect(summary.configDetails[0].hasDescription).toBe(true); + }); + }); + + describe('Registry Operations', () => { + it('should return all registered node types', () => { + registry.register(ButtonConfig); + const nodeTypes = registry.getRegisteredNodeTypes(); + expect(nodeTypes).toContain('net.noodl.visual.button'); + }); + + it('should clear all configs', () => { + registry.register(ButtonConfig); + expect(registry.getCount()).toBe(1); + + registry.clear(); + expect(registry.getCount()).toBe(0); + }); + }); +});