Files
WooNooW/tests/parity.test.ts
Dwindi Ramadhana 396ca25be4 feat: Page Editor v1.0 - canonical schema, SSR parity, and migration
Major improvements to WooNooW Page Editor system:

Schema & Architecture:
- Canonical section schema with unified sectionSchema.ts
- Normalized feature-grid to use items (not features)
- Standardized default values across all section types
- Schema versioning with automatic migration on read

Backend (PHP):
- Enhanced PlaceholderRenderer with typed output contracts
- Added fallback behavior for empty/invalid dynamic sources
- Added caching support for post data resolution
- New SchemaMigration class for backward compatibility
- New Features class for feature flags
- Enhanced PageSSR with full style support
- Removed controller-level special-casing for related_posts

Frontend (Admin SPA):
- Updated CanvasRenderer with schema-aware transformation
- Enhanced InspectorPanel with canonical schema metadata
- Added new section renderers

Frontend (Customer SPA):
- New section components: BentoCategoryGrid, MarqueeBanner, ProductCarousel, ShoppableImage
- Updated FeatureGridSection for items prop contract

Testing:
- Add PHP tests: SchemaMigrationTest, PlaceholderRendererTest, PageSSRTest
- Add TypeScript tests: schema-integration, feature-grid-regression
- Add parity tests for React vs SSR content matching
- Add CI script: check-schema-drift.mjs
- Add VERIFICATION_CHECKLIST.md

Documentation:
- RELEASE_NOTES-v1.0.md with full release notes
- docs/PAGE_EDITOR_SECTION_SCHEMA_V1.md
- docs/PAGE_EDITOR_SSR_COVERAGE_AUDIT.md
2026-05-30 13:02:08 +07:00

749 lines
23 KiB
TypeScript

/**
* React vs SSR Parity Tests
* Tests that compare React-rendered content against SSR HTML output
* to ensure consistent rendering between client and server
*
* Note: These are snapshot-based tests. Run with --update to regenerate snapshots.
*/
import { describe, it, expect, vi } from 'vitest';
import { renderToStaticMarkup } from 'react-dom/server';
import {
HeroSection,
ContentSection,
ImageTextSection,
FeatureGridSection,
CTABannerSection,
ContactFormSection,
BentoCategoryGrid,
ProductCarousel,
ShoppableImage,
MarqueeBanner,
} from '../../customer-spa/src/pages/DynamicPage/sections';
// Mock Lucide icons
vi.mock('lucide-react', () => ({
Star: () => null,
Heart: () => null,
Shield: () => null,
Zap: () => null,
Award: () => null,
Clock: () => null,
Truck: () => null,
User: () => null,
Settings: () => null,
}));
// Helper to extract text content from HTML for comparison
function extractTextContent(html: string): string {
return html
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
// Helper to check if class exists in HTML
function hasClass(html: string, className: string): boolean {
return html.includes(`class="${className}"`) || html.includes(`class="${className} `) || html.includes(` ${className} `);
}
// Helper to check if style attribute exists
function hasStyle(html: string, stylePart: string): boolean {
return html.includes(`style="`) && html.includes(stylePart);
}
// Test data fixtures
const FIXTURES = {
hero: {
id: 'hero-test',
title: 'Welcome to Our Store',
subtitle: 'Discover amazing products',
image: 'https://example.com/hero.jpg',
cta_text: 'Shop Now',
cta_url: '/shop',
styles: {},
elementStyles: {},
},
content: {
id: 'content-test',
content: '<p>This is rich content with formatting.</p>',
styles: {},
elementStyles: {},
},
imageText: {
id: 'image-text-test',
title: 'About Our Company',
text: 'We are passionate about quality.',
image: 'https://example.com/about.jpg',
cta_text: 'Learn More',
cta_url: '/about',
styles: {},
elementStyles: {},
},
featureGrid: {
id: 'features-test',
heading: 'Our Features',
items: [
{ title: 'Feature 1', description: 'Description 1', icon: 'Star' },
{ title: 'Feature 2', description: 'Description 2', icon: 'Heart' },
{ title: 'Feature 3', description: 'Description 3', icon: 'Shield' },
],
layout: 'grid-3',
colorScheme: 'default',
styles: {},
elementStyles: {},
},
ctaBanner: {
id: 'cta-test',
title: 'Ready to Get Started?',
text: 'Join thousands of happy customers.',
button_text: 'Sign Up Now',
button_url: '/signup',
layout: 'default',
colorScheme: 'primary',
styles: {},
elementStyles: {},
},
contactForm: {
id: 'contact-test',
title: 'Contact Us',
webhook_url: 'https://example.com/webhook',
redirect_url: '/thank-you',
styles: {},
elementStyles: {},
},
bentoGrid: {
id: 'bento-test',
title: 'Shop by Category',
items: [
{ label: 'Electronics', url: '/category/electronics', image: 'https://example.com/elec.jpg', size: 'large' },
{ label: 'Clothing', url: '/category/clothing', size: 'medium' },
],
colorScheme: 'default',
styles: {},
elementStyles: {},
},
productCarousel: {
id: 'carousel-test',
title: 'Trending Products',
subtitle: 'Popular right now',
cta_text: 'View All',
cta_url: '/products',
products: [
{ name: 'Product 1', url: '/product/1', image: 'https://example.com/p1.jpg', price: '$29.99' },
{ name: 'Product 2', url: '/product/2', image: 'https://example.com/p2.jpg', price: '$39.99' },
],
colorScheme: 'default',
styles: {},
elementStyles: {},
},
shoppableImage: {
id: 'shoppable-test',
title: 'Shop the Look',
subtitle: 'Get inspired',
image: 'https://example.com/look.jpg',
hotspots: [
{ product_name: 'Item 1', product_slug: 'item-1', product_price: '$49.99', x: '25', y: '30' },
{ product_name: 'Item 2', product_slug: 'item-2', product_price: '$59.99', x: '65', y: '50' },
],
colorScheme: 'default',
styles: {},
elementStyles: {},
},
marquee: {
id: 'marquee-test',
text: 'Free Shipping*Easy Returns*24/7 Support',
separator: '*',
styles: {},
elementStyles: {},
},
};
describe('React vs SSR Parity Tests', () => {
describe('Hero Section', () => {
it('renders title content', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Welcome to Our Store');
});
it('renders subtitle content', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Discover amazing products');
});
it('renders CTA button', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'default',
})
);
expect(html).toContain('href="/shop"');
expect(extractTextContent(html)).toContain('Shop Now');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-hero')).toBe(true);
expect(hasClass(html, 'wn-section')).toBe(true);
});
});
describe('Content Section', () => {
it('renders content with HTML', () => {
const html = renderToStaticMarkup(
ContentSection({
...FIXTURES.content,
colorScheme: 'default',
})
);
expect(html).toContain('This is rich content');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
ContentSection({
...FIXTURES.content,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-content')).toBe(true);
expect(hasClass(html, 'wn-section')).toBe(true);
});
});
describe('Image + Text Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
ImageTextSection({
...FIXTURES.imageText,
layout: 'image-left',
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('About Our Company');
});
it('renders text content', () => {
const html = renderToStaticMarkup(
ImageTextSection({
...FIXTURES.imageText,
layout: 'image-left',
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('We are passionate');
});
it('renders image with correct src', () => {
const html = renderToStaticMarkup(
ImageTextSection({
...FIXTURES.imageText,
layout: 'image-left',
colorScheme: 'default',
})
);
expect(html).toContain('https://example.com/about.jpg');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
ImageTextSection({
...FIXTURES.imageText,
layout: 'image-left',
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-image-text')).toBe(true);
});
});
describe('Feature Grid Section', () => {
it('renders heading', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Our Features');
});
it('renders all feature items', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Feature 1');
expect(extractTextContent(html)).toContain('Feature 2');
expect(extractTextContent(html)).toContain('Feature 3');
});
it('renders feature descriptions', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Description 1');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-feature-grid')).toBe(true);
});
it('has grid layout class', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
layout: 'grid-3',
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-feature-grid--grid-3')).toBe(true);
});
});
describe('CTA Banner Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Ready to Get Started?');
});
it('renders description', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Join thousands');
});
it('renders button with href', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'default',
})
);
expect(html).toContain('href="/signup"');
expect(extractTextContent(html)).toContain('Sign Up Now');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-cta-banner')).toBe(true);
});
});
describe('Contact Form Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
ContactFormSection({
...FIXTURES.contactForm,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Contact Us');
});
it('renders form inputs', () => {
const html = renderToStaticMarkup(
ContactFormSection({
...FIXTURES.contactForm,
colorScheme: 'default',
})
);
expect(html).toContain('name="name"');
expect(html).toContain('name="email"');
expect(html).toContain('name="message"');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
ContactFormSection({
...FIXTURES.contactForm,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-contact-form')).toBe(true);
});
});
describe('Bento Category Grid Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
BentoCategoryGrid({
...FIXTURES.bentoGrid,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Shop by Category');
});
it('renders category labels', () => {
const html = renderToStaticMarkup(
BentoCategoryGrid({
...FIXTURES.bentoGrid,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Electronics');
expect(extractTextContent(html)).toContain('Clothing');
});
it('renders links', () => {
const html = renderToStaticMarkup(
BentoCategoryGrid({
...FIXTURES.bentoGrid,
colorScheme: 'default',
})
);
expect(html).toContain('href="/category/electronics"');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
BentoCategoryGrid({
...FIXTURES.bentoGrid,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-bento-grid')).toBe(true);
});
});
describe('Product Carousel Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
ProductCarousel({
...FIXTURES.productCarousel,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Trending Products');
});
it('renders products', () => {
const html = renderToStaticMarkup(
ProductCarousel({
...FIXTURES.productCarousel,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Product 1');
expect(extractTextContent(html)).toContain('Product 2');
});
it('renders product prices', () => {
const html = renderToStaticMarkup(
ProductCarousel({
...FIXTURES.productCarousel,
colorScheme: 'default',
})
);
expect(html).toContain('$29.99');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
ProductCarousel({
...FIXTURES.productCarousel,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-product-carousel')).toBe(true);
});
});
describe('Shoppable Image Section', () => {
it('renders title', () => {
const html = renderToStaticMarkup(
ShoppableImage({
...FIXTURES.shoppableImage,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Shop the Look');
});
it('renders image', () => {
const html = renderToStaticMarkup(
ShoppableImage({
...FIXTURES.shoppableImage,
colorScheme: 'default',
})
);
expect(html).toContain('https://example.com/look.jpg');
});
it('renders hotspots', () => {
const html = renderToStaticMarkup(
ShoppableImage({
...FIXTURES.shoppableImage,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Item 1');
expect(extractTextContent(html)).toContain('Item 2');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
ShoppableImage({
...FIXTURES.shoppableImage,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-shoppable-image')).toBe(true);
});
});
describe('Marquee Banner Section', () => {
it('renders text items', () => {
const html = renderToStaticMarkup(
MarqueeBanner({
...FIXTURES.marquee,
colorScheme: 'default',
})
);
expect(extractTextContent(html)).toContain('Free Shipping');
expect(extractTextContent(html)).toContain('Easy Returns');
expect(extractTextContent(html)).toContain('24/7 Support');
});
it('has section class for SSR matching', () => {
const html = renderToStaticMarkup(
MarqueeBanner({
...FIXTURES.marquee,
colorScheme: 'default',
})
);
expect(hasClass(html, 'wn-marquee')).toBe(true);
});
});
});
describe('SSR Class Matching Verification', () => {
/**
* These tests verify that React components use the same CSS classes
* as the PHP SSR renderers. This ensures visual parity.
*/
const SECTION_CLASSES = {
hero: 'wn-hero',
content: 'wn-content',
'image-text': 'wn-image-text',
'feature-grid': 'wn-feature-grid',
'cta-banner': 'wn-cta-banner',
'contact-form': 'wn-contact-form',
'bento-category-grid': 'wn-bento-grid',
'product-carousel': 'wn-product-carousel',
'shoppable-image': 'wn-shoppable-image',
'marquee-banner': 'wn-marquee',
};
it('hero section uses wn-hero class', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES.hero)).toBe(true);
});
it('content section uses wn-content class', () => {
const html = renderToStaticMarkup(
ContentSection({
...FIXTURES.content,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES.content)).toBe(true);
});
it('image-text section uses wn-image-text class', () => {
const html = renderToStaticMarkup(
ImageTextSection({
...FIXTURES.imageText,
layout: 'image-left',
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['image-text'])).toBe(true);
});
it('feature-grid section uses wn-feature-grid class', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['feature-grid'])).toBe(true);
});
it('cta-banner section uses wn-cta-banner class', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['cta-banner'])).toBe(true);
});
it('contact-form section uses wn-contact-form class', () => {
const html = renderToStaticMarkup(
ContactFormSection({
...FIXTURES.contactForm,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['contact-form'])).toBe(true);
});
it('bento-category-grid section uses wn-bento-grid class', () => {
const html = renderToStaticMarkup(
BentoCategoryGrid({
...FIXTURES.bentoGrid,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['bento-category-grid'])).toBe(true);
});
it('product-carousel section uses wn-product-carousel class', () => {
const html = renderToStaticMarkup(
ProductCarousel({
...FIXTURES.productCarousel,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['product-carousel'])).toBe(true);
});
it('shoppable-image section uses wn-shoppable-image class', () => {
const html = renderToStaticMarkup(
ShoppableImage({
...FIXTURES.shoppableImage,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['shoppable-image'])).toBe(true);
});
it('marquee-banner section uses wn-marquee class', () => {
const html = renderToStaticMarkup(
MarqueeBanner({
...FIXTURES.marquee,
colorScheme: 'default',
})
);
expect(hasClass(html, SECTION_CLASSES['marquee-banner'])).toBe(true);
});
});
describe('Color Scheme Parity', () => {
it('hero supports colorScheme prop', () => {
const html = renderToStaticMarkup(
HeroSection({
...FIXTURES.hero,
colorScheme: 'primary',
})
);
expect(hasClass(html, 'wn-scheme--primary')).toBe(true);
});
it('feature-grid supports colorScheme prop', () => {
const html = renderToStaticMarkup(
FeatureGridSection({
...FIXTURES.featureGrid,
colorScheme: 'muted',
})
);
expect(hasClass(html, 'wn-scheme--muted')).toBe(true);
});
it('cta-banner supports colorScheme prop', () => {
const html = renderToStaticMarkup(
CTABannerSection({
...FIXTURES.ctaBanner,
colorScheme: 'secondary',
})
);
expect(hasClass(html, 'wn-scheme--secondary')).toBe(true);
});
});