stateManager = InMemoryStateManager::for(ShoppingCartState::class); $this->manager = new LiveComponentStateManager($this->stateManager); $this->componentId = ComponentId::create('shopping-cart', 'user-123'); }); it('stores and retrieves component state', function () { $cartState = ShoppingCartState::fromArray([ 'items' => [ ['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 2], ], 'discount_code' => 'SAVE10', 'discount_percentage' => 10 ]); $this->manager->setComponentState($this->componentId, $cartState); $retrieved = $this->manager->getComponentState($this->componentId); expect($retrieved)->toBeInstanceOf(ShoppingCartState::class); expect($retrieved->discountCode)->toBe('SAVE10'); expect($retrieved->discountPercentage)->toBe(10); expect(count($retrieved->items))->toBe(1); }); it('preserves type safety throughout', function () { $cartState = ShoppingCartState::fromArray([ 'items' => [ ['id' => '1', 'name' => 'Product A', 'price' => 19.99, 'quantity' => 1], ], 'discount_code' => 'TEST', 'discount_percentage' => 5 ]); $this->manager->setComponentState($this->componentId, $cartState); $retrieved = $this->manager->getComponentState($this->componentId); // Type is preserved - not mixed or LiveComponentState, but ShoppingCartState expect($retrieved)->toBeInstanceOf(ShoppingCartState::class); expect($retrieved->items)->toBeArray(); expect($retrieved->discountCode)->toBeString(); }); it('checks if component state exists', function () { expect($this->manager->hasComponentState($this->componentId))->toBeFalse(); $cartState = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); $this->manager->setComponentState($this->componentId, $cartState); expect($this->manager->hasComponentState($this->componentId))->toBeTrue(); }); it('removes component state', function () { $cartState = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); $this->manager->setComponentState($this->componentId, $cartState); expect($this->manager->hasComponentState($this->componentId))->toBeTrue(); $this->manager->removeComponentState($this->componentId); expect($this->manager->hasComponentState($this->componentId))->toBeFalse(); expect($this->manager->getComponentState($this->componentId))->toBeNull(); }); it('atomically updates component state', function () { $initialCart = ShoppingCartState::fromArray([ 'items' => [ ['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1], ], 'discount_code' => 'SAVE10', 'discount_percentage' => 10 ]); $this->manager->setComponentState($this->componentId, $initialCart); $updatedCart = $this->manager->updateComponentState( $this->componentId, function ($state) { return $state ? $state->withDiscountCode('SAVE20', 20) : ShoppingCartState::fromArray([]); } ); expect($updatedCart)->toBeInstanceOf(ShoppingCartState::class); expect($updatedCart->discountCode)->toBe('SAVE20'); expect($updatedCart->discountPercentage)->toBe(20); expect(count($updatedCart->items))->toBe(1); }); it('handles null state in atomic update', function () { $newCart = $this->manager->updateComponentState( $this->componentId, function ($state) { return $state ?? ShoppingCartState::fromArray([ 'items' => [ ['id' => '2', 'name' => 'New Product', 'price' => 15.50, 'quantity' => 1], ], 'discount_code' => 'FIRST', 'discount_percentage' => 5 ]); } ); expect($newCart)->toBeInstanceOf(ShoppingCartState::class); expect($newCart->discountCode)->toBe('FIRST'); expect(count($newCart->items))->toBe(1); }); it('respects TTL for state expiration', function () { $cartState = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); // Store with very short TTL $this->manager->setComponentState( $this->componentId, $cartState, Duration::fromSeconds(1) ); expect($this->manager->hasComponentState($this->componentId))->toBeTrue(); // Wait for expiration sleep(2); // State should be expired (for TTL-aware implementations) // Note: InMemoryStateManager doesn't enforce TTL, but the interface supports it }); it('retrieves all component states', function () { $cart1 = ShoppingCartState::fromArray([ 'items' => [ ['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1], ], 'discount_code' => 'SAVE10', 'discount_percentage' => 10 ]); $cart2 = ShoppingCartState::fromArray([ 'items' => [ ['id' => '2', 'name' => 'Product B', 'price' => 15.50, 'quantity' => 2], ], 'discount_code' => 'SAVE20', 'discount_percentage' => 20 ]); $componentId1 = ComponentId::create('shopping-cart', 'user-123'); $componentId2 = ComponentId::create('shopping-cart', 'user-456'); $this->manager->setComponentState($componentId1, $cart1); $this->manager->setComponentState($componentId2, $cart2); $allStates = $this->manager->getAllComponentStates(); expect($allStates)->toBeArray(); expect(count($allStates))->toBeGreaterThanOrEqual(2); }); it('clears all component states', function () { $cart1 = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); $cart2 = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => 'TEST', 'discount_percentage' => 5 ]); $componentId1 = ComponentId::create('shopping-cart', 'user-123'); $componentId2 = ComponentId::create('shopping-cart', 'user-456'); $this->manager->setComponentState($componentId1, $cart1); $this->manager->setComponentState($componentId2, $cart2); $this->manager->clearAllComponentStates(); expect($this->manager->hasComponentState($componentId1))->toBeFalse(); expect($this->manager->hasComponentState($componentId2))->toBeFalse(); }); it('provides statistics about state operations', function () { $cartState = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); $this->manager->setComponentState($this->componentId, $cartState); $this->manager->getComponentState($this->componentId); $this->manager->hasComponentState($this->componentId); $stats = $this->manager->getStatistics(); expect($stats)->toBeInstanceOf(StateManagerStatistics::class); expect($stats->totalKeys)->toBeGreaterThanOrEqual(1); expect($stats->setCount)->toBeGreaterThanOrEqual(1); expect($stats->hitCount)->toBeGreaterThanOrEqual(1); }); it('can be created via factory method', function () { $stateManager = InMemoryStateManager::for(ShoppingCartState::class); $manager = LiveComponentStateManager::from($stateManager); expect($manager)->toBeInstanceOf(LiveComponentStateManager::class); }); it('generates consistent cache keys', function () { $componentId1 = ComponentId::fromString('shopping-cart:user-123'); $componentId2 = ComponentId::fromString('shopping-cart:user-123'); $componentId3 = ComponentId::fromString('shopping-cart:user-456'); $cartState = ShoppingCartState::fromArray([ 'items' => [], 'discount_code' => null, 'discount_percentage' => 0 ]); $this->manager->setComponentState($componentId1, $cartState); // Same component ID should retrieve same state expect($this->manager->hasComponentState($componentId2))->toBeTrue(); // Different instance ID should not have state expect($this->manager->hasComponentState($componentId3))->toBeFalse(); }); it('handles complex state transformations', function () { $initialCart = ShoppingCartState::fromArray([ 'items' => [ ['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1], ], 'discount_code' => null, 'discount_percentage' => 0 ]); $this->manager->setComponentState($this->componentId, $initialCart); // Multiple transformations $updatedCart = $this->manager->updateComponentState( $this->componentId, fn($state) => $state->withDiscountCode('SAVE10', 10) ); $updatedCart = $this->manager->updateComponentState( $this->componentId, function($state) { return ShoppingCartState::fromArray([ 'items' => array_merge($state->items, [ ['id' => '2', 'name' => 'Product B', 'price' => 15.50, 'quantity' => 2] ]), 'discount_code' => $state->discountCode, 'discount_percentage' => $state->discountPercentage ]); } ); expect(count($updatedCart->items))->toBe(2); expect($updatedCart->discountCode)->toBe('SAVE10'); }); });