slotManager = new SlotManager(); }); describe('CardComponent', function () { it('renders with custom header, body and footer slots', function () { $component = new CardComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); $providedSlots = [ SlotContent::named('header', '

User Profile

'), SlotContent::named('body', '

User details...

'), SlotContent::named('footer', ''), ]; // Validate slots $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toBeEmpty(); // Resolve each slot $definitions = $component->getSlotDefinitions(); $headerContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], // header $providedSlots ); $bodyContent = $this->slotManager->resolveSlotContent( $component, $definitions[1], // body $providedSlots ); $footerContent = $this->slotManager->resolveSlotContent( $component, $definitions[2], // footer $providedSlots ); expect($headerContent)->toContain('User Profile'); expect($bodyContent)->toContain('User details'); expect($footerContent)->toContain('Edit'); }); it('validates that body slot is required', function () { $component = new CardComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); // Only provide header, skip required body $providedSlots = [ SlotContent::named('header', '

Header

'), ]; $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toContain("Required slot 'body' is not filled"); }); it('uses default header when not provided', function () { $component = new CardComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); $providedSlots = [ SlotContent::named('body', '

Body content

'), ]; $definitions = $component->getSlotDefinitions(); $headerContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], // header $providedSlots ); expect($headerContent)->toContain('card-header-default'); }); }); describe('ModalComponent', function () { it('renders with scoped context in content and actions slots', function () { $modalId = ComponentId::generate(); $component = new ModalComponent( id: $modalId, state: ComponentState::fromArray(['isOpen' => true]) ); $providedSlots = [ SlotContent::named('title', '

Confirm

'), SlotContent::named('content', '

Modal ID: {context.modalId}

'), SlotContent::named('actions', ''), ]; $definitions = $component->getSlotDefinitions(); $contentSlot = $this->slotManager->resolveSlotContent( $component, $definitions[1], // content (scoped) $providedSlots ); $actionsSlot = $this->slotManager->resolveSlotContent( $component, $definitions[2], // actions (scoped) $providedSlots ); // Check that context was injected expect($contentSlot)->toContain($modalId->toString()); expect($actionsSlot)->toContain("closeModal('{$modalId->toString()}')"); }); it('validates that content slot is required', function () { $component = new ModalComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); // Only provide title, skip required content $providedSlots = [ SlotContent::named('title', '

Title

'), ]; $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toContain("Required slot 'content' is not filled"); }); }); describe('LayoutComponent', function () { it('renders with sidebar, main, header and footer slots', function () { $component = new LayoutComponent( id: ComponentId::generate(), state: ComponentState::fromArray([ 'sidebarWidth' => '300px', 'sidebarCollapsed' => false, ]) ); $providedSlots = [ SlotContent::named('header', '
App Header
'), SlotContent::named('sidebar', ''), SlotContent::named('main', '
Main content
'), SlotContent::named('footer', ''), ]; $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toBeEmpty(); $definitions = $component->getSlotDefinitions(); $sidebarContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], // sidebar (scoped) $providedSlots ); // Check scoped context injection expect($sidebarContent)->toContain('300px'); }); it('validates that main slot is required', function () { $component = new LayoutComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); $providedSlots = [ SlotContent::named('sidebar', ''), ]; $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toContain("Required slot 'main' is not filled"); }); }); describe('ContainerComponent', function () { it('renders with default slot and actions', function () { $component = new ContainerComponent( id: ComponentId::generate(), state: ComponentState::fromArray(['padding' => 'large']) ); $providedSlots = [ SlotContent::default('

Welcome

Content

'), SlotContent::named('title', '

Container

'), SlotContent::named('actions', ''), ]; $errors = $this->slotManager->validateSlots($component, $providedSlots); expect($errors)->toBeEmpty(); $definitions = $component->getSlotDefinitions(); $defaultContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], // default $providedSlots ); expect($defaultContent)->toContain('Welcome'); expect($defaultContent)->toContain('container-padding-large'); }); it('uses default content when default slot is not provided', function () { $component = new ContainerComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); $providedSlots = [ SlotContent::named('title', '

Container

'), ]; $definitions = $component->getSlotDefinitions(); $defaultContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], // default $providedSlots ); expect($defaultContent)->toContain('empty-container'); }); }); describe('Slot Content Processing', function () { it('processes slot content through component hook', function () { $component = new CardComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); $providedSlots = [ SlotContent::named('header', '

Header

'), SlotContent::named('body', '

Body

'), ]; $definitions = $component->getSlotDefinitions(); // Header gets wrapped in div.card-header $headerContent = $this->slotManager->resolveSlotContent( $component, $definitions[0], $providedSlots ); expect($headerContent)->toContain('
'); expect($headerContent)->toContain('
'); // Body gets wrapped in div.card-body $bodyContent = $this->slotManager->resolveSlotContent( $component, $definitions[1], $providedSlots ); expect($bodyContent)->toContain('
'); }); }); describe('XSS Protection', function () { it('escapes HTML in scoped context values', function () { $component = new ModalComponent( id: ComponentId::generate(), state: ComponentState::fromArray([]) ); // Try to inject script via slot content $providedSlots = [ SlotContent::named('content', '

{context.modalId}

'), ]; $definitions = $component->getSlotDefinitions(); $content = $this->slotManager->resolveSlotContent( $component, $definitions[1], // content (scoped) $providedSlots ); // modalId is a ComponentId, which gets htmlspecialchars treatment // Check that HTML entities are properly escaped expect($content)->not->toContain('