/** * Component Validation Tests * * Tests that every component in the resolver: * 1. Has a valid .craft config with displayName, props, rules, related.settings * 2. Has a .toHtml static method for export * 3. Can be instantiated with default props * 4. Settings panel exists and is a valid React component */ import { componentResolver } from '../src/components/resolver'; // All components that should be in the resolver const EXPECTED_COMPONENTS = [ 'Container', 'Section', 'ColumnLayout', 'BackgroundSection', 'Heading', 'TextBlock', 'ButtonLink', 'Logo', 'Menu', 'Navbar', 'Footer', 'Divider', 'Spacer', 'Icon', 'HtmlBlock', 'ImageBlock', 'VideoBlock', 'MapEmbed', 'HeroSimple', 'FeaturesGrid', 'CTASection', 'CallToAction', 'Countdown', 'Testimonials', 'Accordion', 'Tabs', 'PricingTable', 'Gallery', 'ContentSlider', 'NumberCounter', 'FormContainer', 'InputField', 'TextareaField', 'FormButton', 'ContactForm', 'SubscribeForm', 'StarRating', 'SocialLinks', 'SearchBar', ]; const resolver = componentResolver as Record; describe('Component Resolver', () => { test('contains all expected components', () => { const keys = Object.keys(resolver); for (const name of EXPECTED_COMPONENTS) { expect(keys).toContain(name); } }); test('has no undefined entries', () => { for (const [key, comp] of Object.entries(resolver)) { expect(comp).toBeDefined(); expect(typeof comp).toBe('function'); } }); }); describe('Component Craft Config', () => { for (const [name, Component] of Object.entries(resolver)) { describe(name, () => { test('has .craft config', () => { expect(Component.craft).toBeDefined(); expect(typeof Component.craft).toBe('object'); }); test('has displayName', () => { expect(Component.craft.displayName).toBeDefined(); expect(typeof Component.craft.displayName).toBe('string'); expect(Component.craft.displayName.length).toBeGreaterThan(0); }); test('has default props', () => { expect(Component.craft.props).toBeDefined(); expect(typeof Component.craft.props).toBe('object'); }); test('has rules', () => { expect(Component.craft.rules).toBeDefined(); expect(typeof Component.craft.rules.canDrag).toBe('function'); }); test('has related.settings', () => { expect(Component.craft.related).toBeDefined(); expect(Component.craft.related.settings).toBeDefined(); expect(typeof Component.craft.related.settings).toBe('function'); }); }); } }); describe('Component HTML Export', () => { for (const [name, Component] of Object.entries(resolver)) { test(`${name} has .toHtml method`, () => { expect(typeof Component.toHtml).toBe('function'); }); test(`${name} .toHtml returns valid HTML`, () => { const result = Component.toHtml(Component.craft.props || {}, ''); expect(result).toBeDefined(); expect(typeof result.html).toBe('string'); }); } }); describe('Component Default Props', () => { for (const [name, Component] of Object.entries(resolver)) { test(`${name} has no undefined required props`, () => { const props = Component.craft.props; // All props should be defined (not undefined) for (const [key, val] of Object.entries(props)) { expect(val).not.toBeUndefined(); } }); test(`${name} array props are actual arrays`, () => { const props = Component.craft.props; for (const [key, val] of Object.entries(props)) { if (key.includes('features') || key.includes('items') || key.includes('plans') || key.includes('links') || key.includes('testimonials') || key.includes('tabs') || key.includes('images') || key.includes('slides') || key.includes('counters') || key.includes('fields')) { if (val !== undefined && val !== null) { expect(Array.isArray(val)).toBe(true); } } } }); } }); describe('Display Name Uniqueness', () => { test('all display names are unique', () => { const names = Object.values(resolver).map((c: any) => c.craft?.displayName); const uniqueNames = new Set(names); expect(uniqueNames.size).toBe(names.length); }); }); describe('Style Panel Type Coverage', () => { // These regex patterns match what GuidedStyles uses const patterns: Record = { isText: /^heading$|^text$/i, isButton: /^button$/i, isImage: /^image$/i, isContainer: /^container$|^section$|^columns$|^background section$|^header zone$|^footer zone$/i, isHero: /hero/i, isNav: /^menu$|^logo$|^navbar$|^footer$/i, isMedia: /^video$|^gallery$|^map$|^content slider$/i, isForm: /^form$|^input$|^textarea$|^subscribe|^contact form$|^submit button$|^search bar$/i, isSocial: /^social links$|^icon$|^star rating$/i, isPricing: /^pricing/i, isSection: /^accordion$|^tabs$|^testimonial|^countdown$|^number counter$|^cta section$|^call to action$|^features grid$/i, isUtility: /^divider$|^spacer$|^html$/i, }; test('every component is matched by at least one pattern', () => { for (const [name, Component] of Object.entries(resolver)) { const displayName = Component.craft?.displayName || ''; const matched = Object.values(patterns).some(pattern => pattern.test(displayName)); if (!matched) { console.warn(`WARNING: ${name} (displayName: "${displayName}") is not matched by any style panel pattern`); } // This is a warning, not a hard fail -- generic editor handles unmatched types } }); });