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";