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