getMessage()}\n"; echo " File: {$e->getFile()}:{$e->getLine()}\n"; $testsFailed++; } } // Test 1: Constructor accepts only Value Objects test('Constructor: Only accepts ComponentId and ComponentData', function () { $id = ComponentId::create('counter', 'test-1'); $data = ComponentData::fromArray(['count' => 5]); $component = new CounterComponent($id, $data); assert($component->getId() instanceof ComponentId); assert($component->getData() instanceof ComponentData); assert($component->getId()->equals($id)); assert($component->getData()->get('count') === 5); }); // Test 2: Constructor with empty data test('Constructor: Works with null initialData', function () { $id = ComponentId::create('counter', 'test-2'); $component = new CounterComponent($id); assert($component->getData()->isEmpty()); }); // Test 3: Increment returns ComponentData test('increment(): Returns ComponentData', function () { $id = ComponentId::create('counter', 'test-3'); $data = ComponentData::fromArray(['count' => 0]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $result = $component->increment($params, $dispatcher); assert($result instanceof ComponentData); assert($result->get('count') === 1); assert($result->has('last_update')); // Verify event was dispatched assert($dispatcher->hasEvents()); $events = $dispatcher->getEvents(); assert(count($events) === 1); assert($events[0]->name === 'counter:changed'); }); // Test 4: Decrement returns ComponentData test('decrement(): Returns ComponentData', function () { $id = ComponentId::create('counter', 'test-4'); $data = ComponentData::fromArray(['count' => 5]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $result = $component->decrement($params, $dispatcher); assert($result instanceof ComponentData); assert($result->get('count') === 4); }); // Test 5: Reset returns ComponentData test('reset(): Returns ComponentData', function () { $id = ComponentId::create('counter', 'test-5'); $data = ComponentData::fromArray(['count' => 42]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $result = $component->reset($params, $dispatcher); assert($result instanceof ComponentData); assert($result->get('count') === 0); // Verify reset event was dispatched $events = $dispatcher->getEvents(); assert(count($events) === 1); assert($events[0]->name === 'counter:reset'); }); // Test 6: addAmount with ActionParameters test('addAmount(): Uses ActionParameters correctly', function () { $id = ComponentId::create('counter', 'test-6'); $data = ComponentData::fromArray(['count' => 10]); $component = new CounterComponent($id, $data); $params = ActionParameters::fromArray(['amount' => 5]); $dispatcher = new ComponentEventDispatcher(); $result = $component->addAmount($params, $dispatcher); assert($result instanceof ComponentData); assert($result->get('count') === 15); }); // Test 7: Milestone event at count 10 test('Milestone: Dispatches event at multiples of 10', function () { $id = ComponentId::create('counter', 'test-7'); $data = ComponentData::fromArray(['count' => 9]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $component->increment($params, $dispatcher); // Should have both counter:changed AND counter:milestone $events = $dispatcher->getEvents(); assert(count($events) === 2); assert($events[0]->name === 'counter:changed'); assert($events[1]->name === 'counter:milestone'); assert($events[1]->payload->getInt('milestone') === 10); }); // Test 8: Poll returns ComponentData test('poll(): Returns ComponentData', function () { $id = ComponentId::create('counter', 'test-8'); $data = ComponentData::fromArray(['count' => 5]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $result = $component->poll($params); assert($result instanceof ComponentData); assert($result->has('count')); assert($result->has('last_update')); assert($result->has('server_time')); }); // Test 9: Immutability - original data unchanged test('Immutability: Actions do not modify original data', function () { $id = ComponentId::create('counter', 'test-9'); $originalData = ComponentData::fromArray(['count' => 5]); $component = new CounterComponent($id, $originalData); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $newData = $component->increment($params, $dispatcher); // Original data unchanged assert($originalData->get('count') === 5); // New data has updated count assert($newData->get('count') === 6); // Component's data is still original assert($component->getData()->get('count') === 5); }); // Test 10: Event payloads are EventPayload objects test('Events: Use EventPayload Value Objects', function () { $id = ComponentId::create('counter', 'test-10'); $data = ComponentData::fromArray(['count' => 0]); $component = new CounterComponent($id, $data); $params = ActionParameters::empty(); $dispatcher = new ComponentEventDispatcher(); $component->increment($params, $dispatcher); $events = $dispatcher->getEvents(); $event = $events[0]; // Event payload should be EventPayload assert($event->payload instanceof \App\Framework\LiveComponents\ValueObjects\EventPayload); assert($event->payload->getString('component_id') === 'counter:test-10'); assert($event->payload->getInt('old_value') === 0); assert($event->payload->getInt('new_value') === 1); }); echo "\n"; echo "==================================================\n"; echo "Tests passed: {$testsPassed}\n"; echo "Tests failed: {$testsFailed}\n"; echo "==================================================\n"; if ($testsFailed > 0) { exit(1); } echo "\nāœ… All CounterComponent strict typing tests passed!\n"; echo "āœ… ComponentData return types work correctly!\n";