- 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.
200 lines
7.3 KiB
PHP
200 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Contracts\LiveComponent;
|
|
use App\Framework\LiveComponents\Rendering\FragmentRenderer;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\View\TemplateRenderer;
|
|
|
|
describe('FragmentRenderer', function () {
|
|
beforeEach(function () {
|
|
$this->templateRenderer = Mockery::mock(TemplateRenderer::class);
|
|
$this->fragmentRenderer = new FragmentRenderer($this->templateRenderer);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('renders single fragment from template', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5]));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
|
|
|
|
$fullHtml = <<<HTML
|
|
<div id="counter">
|
|
<div data-fragment="counter-display">
|
|
<span>Count: 5</span>
|
|
</div>
|
|
<div data-fragment="counter-controls">
|
|
<button>Increment</button>
|
|
</div>
|
|
</div>
|
|
HTML;
|
|
|
|
$this->templateRenderer
|
|
->shouldReceive('render')
|
|
->once()
|
|
->with('livecomponent-counter', ['count' => 5])
|
|
->andReturn($fullHtml);
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments(
|
|
$component,
|
|
['counter-display']
|
|
);
|
|
|
|
expect($fragments->isEmpty())->toBeFalse();
|
|
expect($fragments->has('counter-display'))->toBeTrue();
|
|
|
|
$displayFragment = $fragments->get('counter-display');
|
|
expect($displayFragment)->toContain('Count: 5');
|
|
expect($displayFragment)->toContain('span');
|
|
});
|
|
|
|
it('renders multiple fragments from template', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 10]));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
|
|
|
|
$fullHtml = <<<HTML
|
|
<div id="counter">
|
|
<div data-fragment="counter-display">
|
|
<span>Count: 10</span>
|
|
</div>
|
|
<div data-fragment="counter-controls">
|
|
<button>Increment</button>
|
|
<button>Decrement</button>
|
|
</div>
|
|
</div>
|
|
HTML;
|
|
|
|
$this->templateRenderer
|
|
->shouldReceive('render')
|
|
->once()
|
|
->andReturn($fullHtml);
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments(
|
|
$component,
|
|
['counter-display', 'counter-controls']
|
|
);
|
|
|
|
expect($fragments->count())->toBe(2);
|
|
expect($fragments->has('counter-display'))->toBeTrue();
|
|
expect($fragments->has('counter-controls'))->toBeTrue();
|
|
|
|
expect($fragments->get('counter-display'))->toContain('Count: 10');
|
|
expect($fragments->get('counter-controls'))->toContain('Increment');
|
|
expect($fragments->get('counter-controls'))->toContain('Decrement');
|
|
});
|
|
|
|
it('returns empty collection when fragments not found', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
|
|
|
|
$fullHtml = '<div id="counter"><span>No fragments</span></div>';
|
|
|
|
$this->templateRenderer
|
|
->shouldReceive('render')
|
|
->once()
|
|
->andReturn($fullHtml);
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments(
|
|
$component,
|
|
['non-existent-fragment']
|
|
);
|
|
|
|
expect($fragments->isEmpty())->toBeTrue();
|
|
});
|
|
|
|
it('handles nested fragments correctly', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('form:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-form');
|
|
|
|
$fullHtml = <<<HTML
|
|
<form data-fragment="form-wrapper">
|
|
<div data-fragment="form-fields">
|
|
<input type="text" name="email" />
|
|
<div data-fragment="form-errors">
|
|
<span class="error">Invalid email</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
HTML;
|
|
|
|
$this->templateRenderer
|
|
->shouldReceive('render')
|
|
->once()
|
|
->andReturn($fullHtml);
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments(
|
|
$component,
|
|
['form-fields', 'form-errors']
|
|
);
|
|
|
|
expect($fragments->count())->toBe(2);
|
|
expect($fragments->has('form-fields'))->toBeTrue();
|
|
expect($fragments->has('form-errors'))->toBeTrue();
|
|
|
|
// Nested fragment should still be extractable
|
|
expect($fragments->get('form-errors'))->toContain('Invalid email');
|
|
});
|
|
|
|
it('preserves HTML structure in fragments', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('card:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['title' => 'Test Card']));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-card');
|
|
|
|
$fullHtml = <<<HTML
|
|
<div class="card">
|
|
<div data-fragment="card-header" class="card-header">
|
|
<h2>Test Card</h2>
|
|
<span class="badge">New</span>
|
|
</div>
|
|
<div data-fragment="card-body">
|
|
<p>Card content</p>
|
|
</div>
|
|
</div>
|
|
HTML;
|
|
|
|
$this->templateRenderer
|
|
->shouldReceive('render')
|
|
->once()
|
|
->andReturn($fullHtml);
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments(
|
|
$component,
|
|
['card-header']
|
|
);
|
|
|
|
$header = $fragments->get('card-header');
|
|
|
|
// Should preserve attributes and nested structure
|
|
expect($header)->toContain('class="card-header"');
|
|
expect($header)->toContain('<h2>Test Card</h2>');
|
|
expect($header)->toContain('class="badge"');
|
|
});
|
|
|
|
it('handles empty fragment list', function () {
|
|
$component = Mockery::mock(LiveComponent::class);
|
|
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('test:demo'));
|
|
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
|
|
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-test');
|
|
|
|
// Should not call renderer if no fragments requested
|
|
$this->templateRenderer->shouldNotReceive('render');
|
|
|
|
$fragments = $this->fragmentRenderer->renderFragments($component, []);
|
|
|
|
expect($fragments->isEmpty())->toBeTrue();
|
|
});
|
|
});
|