Files
michaelschiemer/tests/Feature/Framework/LiveComponents/ContractComplianceTest.php
2025-11-24 21:28:25 +01:00

276 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Framework\LiveComponents;
use App\Application\LiveComponents\Counter\CounterComponent;
use App\Application\LiveComponents\Dashboard\FailedJobsListComponent;
use App\Application\LiveComponents\Dashboard\PerformanceMetricsComponent;
use App\Application\LiveComponents\UserStatsComponent;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\LiveComponents\Contracts\Cacheable;
use App\Framework\LiveComponents\Contracts\LifecycleAware;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Contracts\Pollable;
use App\Framework\LiveComponents\Contracts\SupportsFileUpload;
use App\Framework\LiveComponents\Contracts\SupportsSlots;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentState;
use ReflectionClass;
use ReflectionMethod;
describe('Contract Compliance Tests', function () {
describe('Pollable Interface', function () {
it('verifies Pollable components implement required methods', function () {
$component = new PerformanceMetricsComponent(
ComponentId::create('performance-metrics', 'test'),
ComponentState::empty()
);
expect($component)->toBeInstanceOf(Pollable::class);
expect($component)->toBeInstanceOf(LiveComponentContract::class);
// Check required methods exist
$reflection = new ReflectionClass($component);
expect($reflection->hasMethod('poll'))->toBeTrue();
expect($reflection->hasMethod('getPollInterval'))->toBeTrue();
// Check method signatures
$pollMethod = $reflection->getMethod('poll');
expect($pollMethod->isPublic())->toBeTrue();
expect($pollMethod->getReturnType()?->getName())->toBe('App\Application\LiveComponents\LiveComponentState');
$intervalMethod = $reflection->getMethod('getPollInterval');
expect($intervalMethod->isPublic())->toBeTrue();
expect($intervalMethod->getReturnType()?->getName())->toBe('int');
});
it('verifies poll() returns LiveComponentState', function () {
$component = new PerformanceMetricsComponent(
ComponentId::create('performance-metrics', 'test'),
ComponentState::empty()
);
$result = $component->poll();
expect($result)->toBeInstanceOf(\App\Application\LiveComponents\LiveComponentState::class);
});
it('verifies getPollInterval() returns positive integer', function () {
$component = new PerformanceMetricsComponent(
ComponentId::create('performance-metrics', 'test'),
ComponentState::empty()
);
$interval = $component->getPollInterval();
expect($interval)->toBeInt();
expect($interval)->toBeGreaterThan(0);
});
});
describe('Cacheable Interface', function () {
it('verifies Cacheable components implement required methods', function () {
$component = new UserStatsComponent(
ComponentId::create('user-stats', 'test'),
ComponentState::empty()
);
expect($component)->toBeInstanceOf(Cacheable::class);
expect($component)->toBeInstanceOf(LiveComponentContract::class);
$reflection = new ReflectionClass($component);
// Check required methods
expect($reflection->hasMethod('getCacheKey'))->toBeTrue();
expect($reflection->hasMethod('getCacheTTL'))->toBeTrue();
expect($reflection->hasMethod('shouldCache'))->toBeTrue();
expect($reflection->hasMethod('getCacheTags'))->toBeTrue();
expect($reflection->hasMethod('getVaryBy'))->toBeTrue();
expect($reflection->hasMethod('getStaleWhileRevalidate'))->toBeTrue();
// Check return types
$ttlMethod = $reflection->getMethod('getCacheTTL');
expect($ttlMethod->getReturnType()?->getName())->toBe(Duration::class);
$varyByMethod = $reflection->getMethod('getVaryBy');
$varyByReturnType = $varyByMethod->getReturnType();
expect($varyByReturnType?->allowsNull())->toBeTrue();
});
it('verifies getCacheKey() returns string', function () {
$component = new UserStatsComponent(
ComponentId::create('user-stats', 'test'),
ComponentState::empty()
);
$key = $component->getCacheKey();
expect($key)->toBeString();
expect($key)->not->toBeEmpty();
});
it('verifies getCacheTTL() returns Duration', function () {
$component = new UserStatsComponent(
ComponentId::create('user-stats', 'test'),
ComponentState::empty()
);
$ttl = $component->getCacheTTL();
expect($ttl)->toBeInstanceOf(Duration::class);
});
it('verifies getCacheTags() returns array', function () {
$component = new UserStatsComponent(
ComponentId::create('user-stats', 'test'),
ComponentState::empty()
);
$tags = $component->getCacheTags();
expect($tags)->toBeArray();
});
});
describe('LifecycleAware Interface', function () {
it('verifies LifecycleAware components implement required methods', function () {
// Find a component that implements LifecycleAware
$componentClass = CounterComponent::class;
$reflection = new ReflectionClass($componentClass);
if ($reflection->implementsInterface(LifecycleAware::class)) {
$component = new $componentClass(
ComponentId::create('counter', 'test'),
\App\Application\LiveComponents\Counter\CounterState::empty()
);
expect($component)->toBeInstanceOf(LifecycleAware::class);
// Check required methods
expect($reflection->hasMethod('onMount'))->toBeTrue();
expect($reflection->hasMethod('onUpdate'))->toBeTrue();
expect($reflection->hasMethod('onDestroy'))->toBeTrue();
// Check method signatures (all should return void)
$onMountMethod = $reflection->getMethod('onMount');
expect($onMountMethod->getReturnType()?->getName())->toBe('void');
} else {
// Skip if no component implements LifecycleAware in test scope
$this->markTestSkipped('No LifecycleAware component found for testing');
}
});
});
describe('SupportsFileUpload Interface', function () {
it('verifies SupportsFileUpload components implement required methods', function () {
// Find a component that implements SupportsFileUpload
$componentClass = \App\Application\LiveComponents\ImageUploader\ImageUploaderComponent::class;
if (!class_exists($componentClass)) {
$this->markTestSkipped('ImageUploaderComponent not available');
return;
}
$reflection = new ReflectionClass($componentClass);
if ($reflection->implementsInterface(SupportsFileUpload::class)) {
expect($reflection->hasMethod('handleUpload'))->toBeTrue();
expect($reflection->hasMethod('validateUpload'))->toBeTrue();
expect($reflection->hasMethod('getAllowedMimeTypes'))->toBeTrue();
expect($reflection->hasMethod('getMaxFileSize'))->toBeTrue();
// Check handleUpload signature
$handleUploadMethod = $reflection->getMethod('handleUpload');
$params = $handleUploadMethod->getParameters();
expect(count($params))->toBeGreaterThanOrEqual(1);
expect($params[0]->getType()?->getName())->toBe(\App\Framework\Http\UploadedFile::class);
} else {
$this->markTestSkipped('No SupportsFileUpload component found for testing');
}
});
});
describe('SupportsSlots Interface', function () {
it('verifies SupportsSlots components implement required methods', function () {
// Find a component that implements SupportsSlots
$componentClass = \App\Application\LiveComponents\LayoutComponent::class;
if (!class_exists($componentClass)) {
$this->markTestSkipped('LayoutComponent not available');
return;
}
$reflection = new ReflectionClass($componentClass);
if ($reflection->implementsInterface(SupportsSlots::class)) {
expect($reflection->hasMethod('getSlotDefinitions'))->toBeTrue();
expect($reflection->hasMethod('processSlotContent'))->toBeTrue();
expect($reflection->hasMethod('getSlotContext'))->toBeTrue();
} else {
$this->markTestSkipped('No SupportsSlots component found for testing');
}
});
});
describe('LiveComponentContract Interface', function () {
it('verifies all components implement LiveComponentContract', function () {
$components = [
CounterComponent::class,
PerformanceMetricsComponent::class,
UserStatsComponent::class,
FailedJobsListComponent::class,
];
foreach ($components as $componentClass) {
$reflection = new ReflectionClass($componentClass);
expect($reflection->implementsInterface(LiveComponentContract::class))->toBeTrue(
"Component {$componentClass} must implement LiveComponentContract"
);
// Check required properties
expect($reflection->hasProperty('id'))->toBeTrue();
expect($reflection->hasProperty('state'))->toBeTrue();
// Check required methods
expect($reflection->hasMethod('getRenderData'))->toBeTrue();
$getRenderDataMethod = $reflection->getMethod('getRenderData');
expect($getRenderDataMethod->isPublic())->toBeTrue();
expect($getRenderDataMethod->getReturnType()?->getName())->toBe(
\App\Framework\LiveComponents\ValueObjects\ComponentRenderData::class
);
}
});
});
describe('Interface Method Signatures', function () {
it('verifies Pollable::poll() signature', function () {
$reflection = new ReflectionClass(Pollable::class);
$pollMethod = $reflection->getMethod('poll');
expect($pollMethod->getReturnType()?->getName())->toBe(\App\Application\LiveComponents\LiveComponentState::class);
expect($pollMethod->getParameters())->toBeEmpty();
});
it('verifies Cacheable::getCacheTTL() signature', function () {
$reflection = new ReflectionClass(Cacheable::class);
$ttlMethod = $reflection->getMethod('getCacheTTL');
expect($ttlMethod->getReturnType()?->getName())->toBe(Duration::class);
expect($ttlMethod->getParameters())->toBeEmpty();
});
it('verifies SupportsFileUpload::handleUpload() signature', function () {
$reflection = new ReflectionClass(SupportsFileUpload::class);
$handleUploadMethod = $reflection->getMethod('handleUpload');
$params = $handleUploadMethod->getParameters();
expect(count($params))->toBeGreaterThanOrEqual(1);
expect($params[0]->getType()?->getName())->toBe(\App\Framework\Http\UploadedFile::class);
expect($handleUploadMethod->getReturnType()?->getName())->toBe(\App\Application\LiveComponents\LiveComponentState::class);
});
});
});