- 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.
282 lines
8.9 KiB
PHP
282 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
|
|
|
use App\Framework\LiveComponents\ComponentEventDispatcher;
|
|
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentEvent;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\LiveComponents\ValueObjects\EventPayload;
|
|
|
|
echo "Testing LiveComponents Integration with Value Objects\n";
|
|
echo "=====================================================\n\n";
|
|
|
|
$testsPassed = 0;
|
|
$testsFailed = 0;
|
|
|
|
function test(string $name, callable $fn): void
|
|
{
|
|
global $testsPassed, $testsFailed;
|
|
|
|
try {
|
|
$fn();
|
|
echo "✅ {$name}\n";
|
|
$testsPassed++;
|
|
} catch (Throwable $e) {
|
|
echo "❌ {$name}\n";
|
|
echo " Error: {$e->getMessage()}\n";
|
|
echo " File: {$e->getFile()}:{$e->getLine()}\n";
|
|
$testsFailed++;
|
|
}
|
|
}
|
|
|
|
// Test 1: ComponentId creation and usage
|
|
test('ComponentId: Create and parse', function () {
|
|
$id = ComponentId::create('chart', 'user-123');
|
|
assert($id->name === 'chart');
|
|
assert($id->instanceId === 'user-123');
|
|
assert($id->toString() === 'chart:user-123');
|
|
|
|
$parsed = ComponentId::fromString('chart:user-123');
|
|
assert($parsed->equals($id));
|
|
});
|
|
|
|
// Test 2: ComponentData immutability
|
|
test('ComponentData: Immutable updates', function () {
|
|
$data1 = ComponentData::fromArray(['count' => 0]);
|
|
$data2 = $data1->with('count', 5);
|
|
|
|
assert($data1->get('count') === 0); // Original unchanged
|
|
assert($data2->get('count') === 5); // New instance updated
|
|
});
|
|
|
|
// Test 3: ActionParameters type coercion
|
|
test('ActionParameters: Type coercion', function () {
|
|
$params = ActionParameters::fromArray([
|
|
'count' => '42',
|
|
'price' => '19.99',
|
|
'active' => '1',
|
|
]);
|
|
|
|
assert($params->getInt('count') === 42);
|
|
assert($params->getFloat('price') === 19.99);
|
|
assert($params->getBool('active') === true);
|
|
});
|
|
|
|
// Test 4: EventPayload creation and access
|
|
test('EventPayload: Create and access', function () {
|
|
$payload = EventPayload::fromArray([
|
|
'user_id' => 123,
|
|
'action' => 'clicked',
|
|
'timestamp' => time(),
|
|
]);
|
|
|
|
assert($payload->getInt('user_id') === 123);
|
|
assert($payload->getString('action') === 'clicked');
|
|
assert($payload->has('timestamp'));
|
|
});
|
|
|
|
// Test 5: ComponentEvent broadcast
|
|
test('ComponentEvent: Broadcast event', function () {
|
|
$payload = EventPayload::fromArray(['value' => 42]);
|
|
$event = ComponentEvent::broadcast('counter:updated', $payload);
|
|
|
|
assert($event->name === 'counter:updated');
|
|
assert($event->isBroadcast());
|
|
assert(! $event->isTargeted());
|
|
assert($event->payload->getInt('value') === 42);
|
|
});
|
|
|
|
// Test 6: ComponentEvent targeted
|
|
test('ComponentEvent: Targeted event', function () {
|
|
$payload = EventPayload::fromArray(['message' => 'Hello']);
|
|
$event = ComponentEvent::target('notification:show', 'notification:instance-1', $payload);
|
|
|
|
assert($event->name === 'notification:show');
|
|
assert($event->isTargeted());
|
|
assert(! $event->isBroadcast());
|
|
assert($event->targetsComponent('notification:instance-1'));
|
|
assert($event->payload->getString('message') === 'Hello');
|
|
});
|
|
|
|
// Test 7: ComponentEventDispatcher
|
|
test('ComponentEventDispatcher: Dispatch events', function () {
|
|
$dispatcher = new ComponentEventDispatcher();
|
|
|
|
$payload1 = EventPayload::fromArray(['count' => 1]);
|
|
$dispatcher->dispatch('event1', $payload1);
|
|
|
|
$payload2 = EventPayload::fromArray(['count' => 2]);
|
|
$dispatcher->dispatchTo('event2', 'target:123', $payload2);
|
|
|
|
assert($dispatcher->hasEvents());
|
|
$events = $dispatcher->getEvents();
|
|
assert(count($events) === 2);
|
|
|
|
assert($events[0]->name === 'event1');
|
|
assert($events[0]->isBroadcast());
|
|
|
|
assert($events[1]->name === 'event2');
|
|
assert($events[1]->isTargeted());
|
|
});
|
|
|
|
// Test 8: Full integration scenario
|
|
test('Integration: Complete component lifecycle', function () {
|
|
// 1. Create component identity
|
|
$componentId = ComponentId::create('shopping-cart', 'session-abc');
|
|
|
|
// 2. Initialize component data
|
|
$initialData = ComponentData::fromArray([
|
|
'items' => [],
|
|
'total' => 0,
|
|
]);
|
|
|
|
// 3. Simulate action with parameters
|
|
$actionParams = ActionParameters::fromArray([
|
|
'product_id' => 'prod-123',
|
|
'quantity' => 2,
|
|
'price' => 29.99,
|
|
]);
|
|
|
|
// 4. Update component data
|
|
$items = $initialData->get('items', []);
|
|
$items[] = [
|
|
'product_id' => $actionParams->getString('product_id'),
|
|
'quantity' => $actionParams->getInt('quantity'),
|
|
'price' => $actionParams->getFloat('price'),
|
|
];
|
|
|
|
$newData = $initialData
|
|
->with('items', $items)
|
|
->with('total', $actionParams->getFloat('price') * $actionParams->getInt('quantity'));
|
|
|
|
// 5. Dispatch event
|
|
$dispatcher = new ComponentEventDispatcher();
|
|
$eventPayload = EventPayload::fromArray([
|
|
'item_count' => count($items),
|
|
'total' => $newData->get('total'),
|
|
]);
|
|
$dispatcher->dispatch('cart:updated', $eventPayload);
|
|
|
|
// Assertions
|
|
assert($componentId->toString() === 'shopping-cart:session-abc');
|
|
assert(count($newData->get('items')) === 1);
|
|
assert($newData->get('total') === 59.98);
|
|
assert($dispatcher->hasEvents());
|
|
|
|
$events = $dispatcher->getEvents();
|
|
assert($events[0]->name === 'cart:updated');
|
|
assert($events[0]->payload->getInt('item_count') === 1);
|
|
});
|
|
|
|
// Test 9: Empty payload handling
|
|
test('EventPayload: Empty payload', function () {
|
|
$event = ComponentEvent::broadcast('notification:clear');
|
|
assert($event->payload->isEmpty());
|
|
assert($event->payload->size() === 0);
|
|
});
|
|
|
|
// Test 10: ComponentData merge
|
|
test('ComponentData: Merge operations', function () {
|
|
$data1 = ComponentData::fromArray(['a' => 1, 'b' => 2]);
|
|
$data2 = ComponentData::fromArray(['c' => 3, 'd' => 4]);
|
|
$merged = $data1->merge($data2);
|
|
|
|
assert($merged->get('a') === 1);
|
|
assert($merged->get('b') === 2);
|
|
assert($merged->get('c') === 3);
|
|
assert($merged->get('d') === 4);
|
|
});
|
|
|
|
// Test 11: ActionParameters validation
|
|
test('ActionParameters: Required parameters', function () {
|
|
$params = ActionParameters::fromArray(['name' => 'John']);
|
|
|
|
try {
|
|
$params->requireString('email');
|
|
assert(false, 'Should have thrown exception');
|
|
} catch (InvalidArgumentException $e) {
|
|
assert(str_contains($e->getMessage(), 'missing'));
|
|
}
|
|
});
|
|
|
|
// Test 12: ComponentEvent serialization
|
|
test('ComponentEvent: Array serialization', function () {
|
|
$payload = EventPayload::fromArray(['key' => 'value']);
|
|
$event = ComponentEvent::broadcast('test:event', $payload);
|
|
|
|
$array = $event->toArray();
|
|
assert($array['name'] === 'test:event');
|
|
assert($array['payload']['key'] === 'value');
|
|
assert($array['target'] === null);
|
|
});
|
|
|
|
// Test 13: ComponentData filtering
|
|
test('ComponentData: Only/Except filtering', function () {
|
|
$data = ComponentData::fromArray(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]);
|
|
|
|
$only = $data->only(['a', 'c']);
|
|
assert($only->size() === 2);
|
|
assert($only->has('a') && $only->has('c'));
|
|
assert(! $only->has('b') && ! $only->has('d'));
|
|
|
|
$except = $data->except(['b', 'd']);
|
|
assert($except->size() === 2);
|
|
assert($except->has('a') && $except->has('c'));
|
|
assert(! $except->has('b') && ! $except->has('d'));
|
|
});
|
|
|
|
// Test 14: Type safety enforcement
|
|
test('Type Safety: No primitive arrays allowed', function () {
|
|
// ComponentEvent only accepts EventPayload, not arrays
|
|
$payload = EventPayload::fromArray(['test' => 'data']);
|
|
$event = ComponentEvent::broadcast('test', $payload);
|
|
|
|
assert($event->payload instanceof EventPayload);
|
|
});
|
|
|
|
// Test 15: Complex nested data
|
|
test('Complex: Nested component state', function () {
|
|
$componentId = ComponentId::generate('data-table');
|
|
|
|
$data = ComponentData::fromArray([
|
|
'columns' => [
|
|
['name' => 'id', 'sortable' => true],
|
|
['name' => 'name', 'sortable' => true],
|
|
['name' => 'email', 'sortable' => false],
|
|
],
|
|
'rows' => [
|
|
['id' => 1, 'name' => 'John', 'email' => 'john@example.com'],
|
|
['id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'],
|
|
],
|
|
'pagination' => [
|
|
'page' => 1,
|
|
'per_page' => 10,
|
|
'total' => 2,
|
|
],
|
|
]);
|
|
|
|
$columns = $data->getArray('columns');
|
|
assert(count($columns) === 3);
|
|
|
|
$pagination = $data->get('pagination');
|
|
assert($pagination['page'] === 1);
|
|
assert($pagination['total'] === 2);
|
|
});
|
|
|
|
echo "\n";
|
|
echo "=====================================================\n";
|
|
echo "Tests passed: {$testsPassed}\n";
|
|
echo "Tests failed: {$testsFailed}\n";
|
|
echo "=====================================================\n";
|
|
|
|
if ($testsFailed > 0) {
|
|
exit(1);
|
|
}
|
|
|
|
echo "\n✅ All LiveComponents integration tests passed!\n";
|
|
echo "✅ Type safety refactoring complete!\n";
|