Files
WooNooW/tests/PageSSRTest.php
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

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);
}
}