Rebuilt the visual site builder from scratch using Craft.js, React 18, and TypeScript. The new editor renders directly in the DOM (no iframe), supports 40+ components, multi-page with shared header/footer, 16 templates, full-spectrum color/gradient controls, custom head code injection, save/publish workflow, and auto-save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
/**
|
|
* 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<string, any>;
|
|
|
|
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<string, RegExp> = {
|
|
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
|
|
}
|
|
});
|
|
});
|