Files
michaelschiemer/tests/Feature/Framework/LiveComponents/SlotSystemIntegrationTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

332 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Application\LiveComponents\CardComponent;
use App\Application\LiveComponents\ContainerComponent;
use App\Application\LiveComponents\LayoutComponent;
use App\Application\LiveComponents\ModalComponent;
use App\Framework\LiveComponents\SlotManager;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentState;
use App\Framework\LiveComponents\ValueObjects\SlotContent;
describe('Slot System Integration', function () {
beforeEach(function () {
$this->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', '<h2>User Profile</h2>'),
SlotContent::named('body', '<p>User details...</p>'),
SlotContent::named('footer', '<button>Edit</button>'),
];
// 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', '<h2>Header</h2>'),
];
$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', '<p>Body content</p>'),
];
$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', '<h3>Confirm</h3>'),
SlotContent::named('content', '<p>Modal ID: {context.modalId}</p>'),
SlotContent::named('actions', '<button onclick="{context.closeFunction}">Close</button>'),
];
$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', '<h3>Title</h3>'),
];
$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', '<header>App Header</header>'),
SlotContent::named('sidebar', '<nav>Sidebar (width: {context.sidebarWidth})</nav>'),
SlotContent::named('main', '<main>Main content</main>'),
SlotContent::named('footer', '<footer>Footer</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', '<nav>Sidebar</nav>'),
];
$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('<h1>Welcome</h1><p>Content</p>'),
SlotContent::named('title', '<h2>Container</h2>'),
SlotContent::named('actions', '<button>Save</button>'),
];
$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', '<h2>Container</h2>'),
];
$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', '<h2>Header</h2>'),
SlotContent::named('body', '<p>Body</p>'),
];
$definitions = $component->getSlotDefinitions();
// Header gets wrapped in div.card-header
$headerContent = $this->slotManager->resolveSlotContent(
$component,
$definitions[0],
$providedSlots
);
expect($headerContent)->toContain('<div class="card-header">');
expect($headerContent)->toContain('</div>');
// Body gets wrapped in div.card-body
$bodyContent = $this->slotManager->resolveSlotContent(
$component,
$definitions[1],
$providedSlots
);
expect($bodyContent)->toContain('<div class="card-body">');
});
});
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', '<p>{context.modalId}</p>'),
];
$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('<script>');
});
});
describe('Slot Statistics', function () {
it('tracks slot registration statistics', function () {
$componentId1 = ComponentId::generate();
$componentId2 = ComponentId::generate();
$this->slotManager->registerSlotContents($componentId1, [
SlotContent::named('header', '<h1>Header</h1>'),
SlotContent::named('body', '<p>Body</p>'),
]);
$this->slotManager->registerSlotContents($componentId2, [
SlotContent::named('footer', '<footer>Footer</footer>'),
]);
$stats = $this->slotManager->getStats();
expect($stats['total_components_with_slots'])->toBe(2);
expect($stats['total_slot_contents'])->toBe(3);
expect($stats['avg_slots_per_component'])->toBe(1.5);
});
});
});