/** * Page Editor Schema Tests * Tests for canonical section schema and normalization */ import { describe, it, expect, beforeEach } from 'vitest'; import { SECTION_SCHEMAS, getSectionSchema, cloneDefaultProps, cloneDefaultStyles, normalizeFeatureGridProps, } from '../../admin-spa/src/routes/Appearance/Pages/schema/sectionSchema'; describe('Section Schema', () => { describe('SECTION_SCHEMAS', () => { it('includes all required section types', () => { const expectedTypes = [ 'hero', 'content', 'image-text', 'feature-grid', 'cta-banner', 'contact-form', 'bento-category-grid', 'product-carousel', 'shoppable-image', 'marquee-banner', ]; expectedTypes.forEach((type) => { expect(SECTION_SCHEMAS).toHaveProperty(type); }); }); it('feature-grid uses items not features', () => { const featureGrid = SECTION_SCHEMAS['feature-grid']; expect(featureGrid.defaultProps).toHaveProperty('items'); expect(featureGrid.defaultProps.items.type).toBe('static'); expect(Array.isArray(featureGrid.defaultProps.items.value)).toBe(true); expect(featureGrid.defaultProps.items.value).toHaveLength(0); }); it('hero section has correct default props', () => { const hero = SECTION_SCHEMAS['hero']; expect(hero.defaultProps).toHaveProperty('title'); expect(hero.defaultProps).toHaveProperty('subtitle'); expect(hero.defaultProps).toHaveProperty('image'); expect(hero.defaultProps).toHaveProperty('cta_text'); expect(hero.defaultProps).toHaveProperty('cta_url'); }); it('all sections have defaultStyles with contentWidth', () => { Object.values(SECTION_SCHEMAS).forEach((schema) => { expect(schema.defaultStyles).toBeDefined(); expect(schema.defaultStyles).toHaveProperty('contentWidth'); }); }); it('all sections have fields defined', () => { Object.values(SECTION_SCHEMAS).forEach((schema) => { expect(Array.isArray(schema.fields)).toBe(true); expect(schema.fields.length).toBeGreaterThan(0); }); }); }); describe('getSectionSchema', () => { it('returns schema for valid section type', () => { const schema = getSectionSchema('hero'); expect(schema).toBeDefined(); expect(schema?.type).toBe('hero'); }); it('returns undefined for invalid section type', () => { const schema = getSectionSchema('nonexistent'); expect(schema).toBeUndefined(); }); }); describe('cloneDefaultProps', () => { it('returns props object for valid type', () => { const props = cloneDefaultProps('hero'); expect(props).toHaveProperty('title'); expect(props).toHaveProperty('subtitle'); }); it('returns empty object for invalid type', () => { const props = cloneDefaultProps('nonexistent'); expect(Object.keys(props)).toHaveLength(0); }); it('clones arrays correctly (no reference sharing)', () => { const props1 = cloneDefaultProps('feature-grid'); const props2 = cloneDefaultProps('feature-grid'); props1.items.value = [{ title: 'Test' }]; expect(props2.items.value).toHaveLength(0); }); }); describe('cloneDefaultStyles', () => { it('returns default styles for valid type', () => { const styles = cloneDefaultStyles('hero'); expect(styles).toHaveProperty('contentWidth'); }); it('returns undefined for invalid type', () => { const styles = cloneDefaultStyles('nonexistent'); expect(styles).toBeUndefined(); }); }); describe('normalizeFeatureGridProps', () => { it('leaves props unchanged when items exists', () => { const props = { items: [{ title: 'Feature 1' }], heading: 'Our Features', }; const normalized = normalizeFeatureGridProps(props); expect(normalized.items).toHaveLength(1); expect(normalized.items[0].title).toBe('Feature 1'); }); it('copies features to items when items is undefined', () => { const props = { features: [{ title: 'Legacy Feature' }], heading: 'Our Features', }; const normalized = normalizeFeatureGridProps(props); expect(normalized.items).toHaveLength(1); expect(normalized.items[0].title).toBe('Legacy Feature'); }); it('handles empty props', () => { const props = {}; const normalized = normalizeFeatureGridProps(props); expect(normalized).toEqual({}); }); it('handles null/undefined input gracefully', () => { expect(normalizeFeatureGridProps(null)).toBeNull(); expect(normalizeFeatureGridProps(undefined)).toBeUndefined(); }); }); }); describe('Section Editor Store Schema Integration', () => { // Test that the store uses the schema correctly it('section schema is compatible with store SectionProp interface', () => { const props = cloneDefaultProps('hero'); // Check SectionProp structure expect(props.title).toHaveProperty('type'); expect(props.title).toHaveProperty('value'); // Ensure type is valid expect(props.title.type).toMatch(/^(static|dynamic)$/); }); it('feature-grid default items is an empty array', () => { const props = cloneDefaultProps('feature-grid'); expect(Array.isArray(props.items.value)).toBe(true); expect(props.items.value).toHaveLength(0); }); }); describe('Canvas Renderer Schema Integration', () => { // Test flattenSectionProps behavior with schema const flattenSectionProps = (section: any) => { const props = section.type === 'feature-grid' ? normalizeFeatureGridProps(section.props || {}) : section.props || {}; const flattened: Record = {}; for (const [key, value] of Object.entries(props)) { if (value && typeof value === 'object' && 'type' in value && 'value' in value) { flattened[key] = value.value; } else if (value && typeof value === 'object' && 'type' in value && 'source' in value) { flattened[key] = `[${value.source}]`; } else { flattened[key] = value; } } return flattened; }; it('flattens static props correctly', () => { const section = { type: 'hero', props: { title: { type: 'static', value: 'Welcome' }, subtitle: { type: 'static', value: 'Subtitle text' }, }, }; const flattened = flattenSectionProps(section); expect(flattened.title).toBe('Welcome'); expect(flattened.subtitle).toBe('Subtitle text'); }); it('flattens dynamic props with placeholder marker', () => { const section = { type: 'hero', props: { title: { type: 'dynamic', source: 'post_title' }, }, }; const flattened = flattenSectionProps(section); expect(flattened.title).toBe('[post_title]'); }); it('flattens feature-grid items correctly', () => { const section = { type: 'feature-grid', props: { heading: { type: 'static', value: 'Features' }, items: { type: 'static', value: [ { title: 'Feature 1', icon: 'Star' }, { title: 'Feature 2', icon: 'Heart' }, ], }, }, }; const flattened = flattenSectionProps(section); expect(flattened.heading).toBe('Features'); expect(Array.isArray(flattened.items)).toBe(true); expect(flattened.items).toHaveLength(2); expect(flattened.items[0].title).toBe('Feature 1'); }); it('handles mixed static and dynamic props', () => { const section = { type: 'image-text', props: { title: { type: 'dynamic', source: 'post_title' }, text: { type: 'static', value: 'Static description' }, image: { type: 'dynamic', source: 'post_featured_image' }, }, }; const flattened = flattenSectionProps(section); expect(flattened.title).toBe('[post_title]'); expect(flattened.text).toBe('Static description'); expect(flattened.image).toBe('[post_featured_image]'); }); it('handles nested props without SectionProp wrapper', () => { const section = { type: 'feature-grid', props: { items: [ { title: 'Direct Item 1' }, { title: 'Direct Item 2' }, ], }, }; const flattened = flattenSectionProps(section); expect(Array.isArray(flattened.items)).toBe(true); expect(flattened.items).toHaveLength(2); }); });