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
382 lines
12 KiB
PHP
382 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* PageSSR Tests
|
|
* Tests for server-side rendering of page sections
|
|
*/
|
|
|
|
namespace WooNooW\Tests;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use WooNooW\Frontend\PageSSR;
|
|
|
|
class PageSSRTest extends TestCase
|
|
{
|
|
/**
|
|
* Test render returns empty string for empty structure
|
|
*/
|
|
public function test_render_returns_empty_for_empty_structure()
|
|
{
|
|
$this->assertEquals('', PageSSR::render([]));
|
|
$this->assertEquals('', PageSSR::render(['sections' => []]));
|
|
}
|
|
|
|
/**
|
|
* Test render_section with unknown type falls back to generic
|
|
*/
|
|
public function test_render_section_fallback_to_generic()
|
|
{
|
|
$section = [
|
|
'id' => 'test-section',
|
|
'type' => 'unknown-type',
|
|
'props' => [
|
|
'title' => 'Test Title',
|
|
'description' => 'Test Description',
|
|
],
|
|
];
|
|
|
|
$html = PageSSR::render_section($section);
|
|
|
|
$this->assertStringContainsString('wn-unknown-type', $html);
|
|
$this->assertStringContainsString('Test Title', $html);
|
|
$this->assertStringContainsString('Test Description', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_hero section
|
|
*/
|
|
public function test_render_hero()
|
|
{
|
|
$props = [
|
|
'title' => 'Welcome',
|
|
'subtitle' => 'To our site',
|
|
'image' => 'https://example.com/hero.jpg',
|
|
'cta_text' => 'Get Started',
|
|
'cta_url' => '/start',
|
|
];
|
|
|
|
$html = PageSSR::render_hero($props, 'default', 'default', 'hero-section');
|
|
|
|
$this->assertStringContainsString('wn-hero', $html);
|
|
$this->assertStringContainsString('Welcome', $html);
|
|
$this->assertStringContainsString('To our site', $html);
|
|
$this->assertStringContainsString('Get Started', $html);
|
|
$this->assertStringContainsString('/start', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_hero with section styles
|
|
*/
|
|
public function test_render_hero_with_section_styles()
|
|
{
|
|
$props = [
|
|
'title' => 'Styled Hero',
|
|
];
|
|
|
|
$section_styles = [
|
|
'backgroundType' => 'gradient',
|
|
'gradientFrom' => '#ff0000',
|
|
'gradientTo' => '#0000ff',
|
|
'gradientAngle' => 90,
|
|
'paddingTop' => '3rem',
|
|
'paddingBottom' => '3rem',
|
|
'contentWidth' => 'contained',
|
|
];
|
|
|
|
$html = PageSSR::render_hero($props, 'default', 'default', 'hero-styled', [], $section_styles);
|
|
|
|
$this->assertStringContainsString('background:', $html);
|
|
$this->assertStringContainsString('linear-gradient', $html);
|
|
$this->assertStringContainsString('padding-top', $html);
|
|
}
|
|
|
|
/**
|
|
* Test resolve_props handles static values
|
|
*/
|
|
public function test_resolve_props_static_values()
|
|
{
|
|
$props = [
|
|
'title' => ['type' => 'static', 'value' => 'Static Title'],
|
|
'subtitle' => ['type' => 'static', 'value' => 'Static Subtitle'],
|
|
];
|
|
|
|
$resolved = PageSSR::resolve_props($props);
|
|
|
|
$this->assertEquals('Static Title', $resolved['title']);
|
|
$this->assertEquals('Static Subtitle', $resolved['subtitle']);
|
|
}
|
|
|
|
/**
|
|
* Test resolve_props handles dynamic values
|
|
*/
|
|
public function test_resolve_props_dynamic_values()
|
|
{
|
|
$props = [
|
|
'title' => ['type' => 'dynamic', 'source' => 'post_title'],
|
|
'subtitle' => ['type' => 'dynamic', 'source' => 'post_excerpt'],
|
|
];
|
|
|
|
$post_data = [
|
|
'title' => 'Dynamic Title',
|
|
'excerpt' => 'Dynamic Excerpt',
|
|
];
|
|
|
|
$resolved = PageSSR::resolve_props($props, $post_data);
|
|
|
|
$this->assertEquals('Dynamic Title', $resolved['title']);
|
|
$this->assertEquals('Dynamic Excerpt', $resolved['subtitle']);
|
|
}
|
|
|
|
/**
|
|
* Test resolve_props with options
|
|
*/
|
|
public function test_resolve_props_with_options()
|
|
{
|
|
$props = [
|
|
'title' => ['type' => 'dynamic', 'source' => 'post_title'],
|
|
];
|
|
|
|
$post_data = [
|
|
'title' => '',
|
|
];
|
|
|
|
// With fallbacks
|
|
$resolved = PageSSR::resolve_props($props, $post_data, ['use_fallbacks' => true]);
|
|
$this->assertEquals('(Untitled)', $resolved['title']);
|
|
|
|
// Without fallbacks
|
|
$resolved = PageSSR::resolve_props($props, $post_data, ['use_fallbacks' => false]);
|
|
$this->assertEquals('', $resolved['title']);
|
|
}
|
|
|
|
/**
|
|
* Test resolve_props handles non-array props
|
|
*/
|
|
public function test_resolve_props_non_array_props()
|
|
{
|
|
$props = [
|
|
'title' => 'Plain String Title',
|
|
'count' => 42,
|
|
];
|
|
|
|
$resolved = PageSSR::resolve_props($props);
|
|
|
|
$this->assertEquals('Plain String Title', $resolved['title']);
|
|
$this->assertEquals(42, $resolved['count']);
|
|
}
|
|
|
|
/**
|
|
* Test render_feature_grid section
|
|
*/
|
|
public function test_render_feature_grid()
|
|
{
|
|
$props = [
|
|
'heading' => 'Our Features',
|
|
'items' => [
|
|
['title' => 'Feature 1', 'description' => 'Desc 1', 'icon' => 'Star'],
|
|
['title' => 'Feature 2', 'description' => 'Desc 2', 'icon' => 'Heart'],
|
|
],
|
|
];
|
|
|
|
$html = PageSSR::render_feature_grid($props, 'grid-3', 'default', 'features-section');
|
|
|
|
$this->assertStringContainsString('wn-feature-grid', $html);
|
|
$this->assertStringContainsString('Our Features', $html);
|
|
$this->assertStringContainsString('Feature 1', $html);
|
|
$this->assertStringContainsString('Feature 2', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_feature_grid falls back to features prop
|
|
*/
|
|
public function test_render_feature_grid_falls_back_to_features()
|
|
{
|
|
$props = [
|
|
'heading' => 'Features',
|
|
'features' => [
|
|
['title' => 'Legacy Feature', 'description' => 'From features prop'],
|
|
],
|
|
];
|
|
|
|
$html = PageSSR::render_feature_grid($props, 'grid-2', 'default', 'features-legacy');
|
|
|
|
$this->assertStringContainsString('Legacy Feature', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_feature_grid with section styles
|
|
*/
|
|
public function test_render_feature_grid_with_styles()
|
|
{
|
|
$props = [
|
|
'heading' => 'Styled Features',
|
|
'items' => [
|
|
['title' => 'Styled Item'],
|
|
],
|
|
];
|
|
|
|
$section_styles = [
|
|
'backgroundType' => 'gradient',
|
|
'gradientFrom' => '#9333ea',
|
|
'gradientTo' => '#3b82f6',
|
|
'gradientAngle' => 135,
|
|
'heightPreset' => 'medium',
|
|
];
|
|
|
|
$html = PageSSR::render_feature_grid($props, 'grid-3', 'default', 'features-styled', [], $section_styles);
|
|
|
|
$this->assertStringContainsString('linear-gradient', $html);
|
|
$this->assertStringContainsString('py-16', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_cta_banner section
|
|
*/
|
|
public function test_render_cta_banner()
|
|
{
|
|
$props = [
|
|
'title' => 'Ready to Start?',
|
|
'text' => 'Join thousands of users today.',
|
|
'button_text' => 'Get Started',
|
|
'button_url' => '/signup',
|
|
];
|
|
|
|
$html = PageSSR::render_cta_banner($props, 'default', 'primary', 'cta-section');
|
|
|
|
$this->assertStringContainsString('wn-cta-banner', $html);
|
|
$this->assertStringContainsString('Ready to Start?', $html);
|
|
$this->assertStringContainsString('Get Started', $html);
|
|
$this->assertStringContainsString('/signup', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_cta_banner with section styles
|
|
*/
|
|
public function test_render_cta_banner_with_styles()
|
|
{
|
|
$props = [
|
|
'title' => 'Styled CTA',
|
|
];
|
|
|
|
$section_styles = [
|
|
'backgroundType' => 'solid',
|
|
'backgroundColor' => '#1a1a1a',
|
|
'contentWidth' => 'full',
|
|
];
|
|
|
|
$html = PageSSR::render_cta_banner($props, 'default', 'default', 'cta-styled', [], $section_styles);
|
|
|
|
$this->assertStringContainsString('background-color', $html);
|
|
$this->assertStringContainsString('w-full', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_contact_form section
|
|
*/
|
|
public function test_render_contact_form()
|
|
{
|
|
$props = [
|
|
'title' => 'Contact Us',
|
|
'fields' => ['name', 'email', 'message'],
|
|
];
|
|
|
|
$html = PageSSR::render_contact_form($props, 'default', 'default', 'contact-section');
|
|
|
|
$this->assertStringContainsString('wn-contact-form', $html);
|
|
$this->assertStringContainsString('Contact Us', $html);
|
|
$this->assertStringContainsString('name', $html);
|
|
$this->assertStringContainsString('email', $html);
|
|
$this->assertStringContainsString('message', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_bento_category_grid section
|
|
*/
|
|
public function test_render_bento_category_grid()
|
|
{
|
|
$props = [
|
|
'title' => 'Shop by Category',
|
|
'items' => [
|
|
['label' => 'Electronics', 'url' => '/category/electronics', 'image' => 'https://example.com/elec.jpg'],
|
|
['label' => 'Clothing', 'url' => '/category/clothing'],
|
|
],
|
|
];
|
|
|
|
$html = PageSSR::render_bento_category_grid($props, 'default', 'default', 'bento-section');
|
|
|
|
$this->assertStringContainsString('wn-bento-grid', $html);
|
|
$this->assertStringContainsString('Shop by Category', $html);
|
|
$this->assertStringContainsString('Electronics', $html);
|
|
$this->assertStringContainsString('Clothing', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render_marquee_banner section
|
|
*/
|
|
public function test_render_marquee_banner()
|
|
{
|
|
$props = [
|
|
'text' => 'Free Shipping * Easy Returns * 24/7 Support',
|
|
'separator' => '*',
|
|
];
|
|
|
|
$html = PageSSR::render_marquee_banner($props, 'default', 'default', 'marquee-section');
|
|
|
|
$this->assertStringContainsString('wn-marquee', $html);
|
|
$this->assertStringContainsString('Free Shipping', $html);
|
|
$this->assertStringContainsString('Easy Returns', $html);
|
|
}
|
|
|
|
/**
|
|
* Test render with full structure
|
|
*/
|
|
public function test_render_full_structure()
|
|
{
|
|
$structure = [
|
|
'sections' => [
|
|
[
|
|
'id' => 'hero-1',
|
|
'type' => 'hero',
|
|
'props' => [
|
|
'title' => ['type' => 'static', 'value' => 'Full Render Test'],
|
|
'subtitle' => ['type' => 'static', 'value' => 'Testing complete render'],
|
|
],
|
|
],
|
|
[
|
|
'id' => 'features-1',
|
|
'type' => 'feature-grid',
|
|
'props' => [
|
|
'heading' => ['type' => 'static', 'value' => 'Features'],
|
|
'items' => [
|
|
['title' => 'Feature A'],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$html = PageSSR::render($structure);
|
|
|
|
$this->assertStringContainsString('Full Render Test', $html);
|
|
$this->assertStringContainsString('Testing complete render', $html);
|
|
$this->assertStringContainsString('Features', $html);
|
|
$this->assertStringContainsString('Feature A', $html);
|
|
}
|
|
|
|
/**
|
|
* Test get_icon_svg returns SVG for known icons
|
|
*/
|
|
public function test_get_icon_svg_known()
|
|
{
|
|
$reflection = new \ReflectionClass(PageSSR::class);
|
|
$method = $reflection->getMethod('get_icon_svg');
|
|
$method->setAccessible(true);
|
|
|
|
$starSvg = $method->invoke(null, 'Star');
|
|
$this->assertStringContainsString('<svg', $starSvg);
|
|
$this->assertStringContainsString('polygon', $starSvg);
|
|
|
|
$heartSvg = $method->invoke(null, 'Heart');
|
|
$this->assertStringContainsString('<svg', $heartSvg);
|
|
$this->assertStringContainsString('path', $heartSvg);
|
|
}
|
|
} |