slotManager = new SlotManager(); }); it('registers and retrieves slot contents', function () { $componentId = ComponentId::generate(); $contents = [ SlotContent::named('header', '

Header

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

Body

'), ]; $this->slotManager->registerSlotContents($componentId, $contents); $retrieved = $this->slotManager->getSlotContents($componentId); expect($retrieved)->toHaveCount(2); expect($retrieved[0]->slotName)->toBe('header'); expect($retrieved[1]->slotName)->toBe('body'); }); it('resolves provided content over default content', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('header', '

Default Header

'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = SlotDefinition::named('header', '

Default Header

'); $providedContent = [ SlotContent::named('header', '

Custom Header

'), ]; $result = $this->slotManager->resolveSlotContent( $component, $definition, $providedContent ); expect($result)->toBe('

Custom Header

'); }); it('uses default content when no content provided', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('header', '

Default Header

'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = SlotDefinition::named('header', '

Default Header

'); $providedContent = []; $result = $this->slotManager->resolveSlotContent( $component, $definition, $providedContent ); expect($result)->toBe('

Default Header

'); }); it('injects scoped context into slot content', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::scoped('content', ['userId', 'userName']), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::create([ 'userId' => 123, 'userName' => 'John Doe', ]); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = SlotDefinition::scoped('content', ['userId', 'userName']); $providedContent = [ SlotContent::named('content', '

User: {context.userName} (ID: {context.userId})

'), ]; $result = $this->slotManager->resolveSlotContent( $component, $definition, $providedContent ); expect($result)->toBe('

User: John Doe (ID: 123)

'); }); it('validates required slots', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('body')->withRequired(true), SlotDefinition::named('footer'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; // No content provided $errors = $this->slotManager->validateSlots($component, []); expect($errors)->toContain("Required slot 'body' is not filled"); }); it('validates slots with content provided', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('body')->withRequired(true), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $providedContent = [ SlotContent::named('body', '

Body content

'), ]; $errors = $this->slotManager->validateSlots($component, $providedContent); expect($errors)->toBeEmpty(); }); it('checks if component has specific slot', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('header'), SlotDefinition::named('body'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; expect($this->slotManager->hasSlot($component, 'header'))->toBeTrue(); expect($this->slotManager->hasSlot($component, 'body'))->toBeTrue(); expect($this->slotManager->hasSlot($component, 'footer'))->toBeFalse(); }); it('gets slot definition by name', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('header', '

Default

'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = $this->slotManager->getSlotDefinition($component, 'header'); expect($definition)->not->toBeNull(); expect($definition->name)->toBe('header'); expect($definition->defaultContent)->toBe('

Default

'); }); it('returns null for unknown slot', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return []; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = $this->slotManager->getSlotDefinition($component, 'unknown'); expect($definition)->toBeNull(); }); it('processes slot content through component hook', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('body'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::empty(); } public function processSlotContent(SlotContent $content): SlotContent { // Wrap content in div return $content->withContent('
' . $content->content . '
'); } public function validateSlots(array $providedSlots): array { return []; } }; $definition = SlotDefinition::named('body'); $providedContent = [ SlotContent::named('body', '

Content

'), ]; $result = $this->slotManager->resolveSlotContent( $component, $definition, $providedContent ); expect($result)->toBe('

Content

'); }); it('escapes HTML in scoped context values', function () { $component = new class () implements SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::scoped('content', ['userInput']), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::create([ 'userInput' => '', ]); } public function processSlotContent(SlotContent $content): SlotContent { return $content; } public function validateSlots(array $providedSlots): array { return []; } }; $definition = SlotDefinition::scoped('content', ['userInput']); $providedContent = [ SlotContent::named('content', '

{context.userInput}

'), ]; $result = $this->slotManager->resolveSlotContent( $component, $definition, $providedContent ); expect($result)->toContain('<script>'); expect($result)->not->toContain('