componentRegistry = Mockery::mock(ComponentRegistry::class); $this->handler = Mockery::mock(LiveComponentHandler::class); $this->fragmentRenderer = Mockery::mock(FragmentRenderer::class); $this->processor = new BatchProcessor( $this->componentRegistry, $this->handler, $this->fragmentRenderer ); }); afterEach(function () { Mockery::close(); }); it('processes single operation successfully', function () { $operation = new BatchOperation('counter:demo', 'increment', ['amount' => 5]); $request = new BatchRequest($operation); $component = Mockery::mock(LiveComponent::class); $component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo')); $component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5])); $this->componentRegistry ->shouldReceive('resolve') ->once() ->andReturn($component); $update = new ComponentUpdate( component: $component, state: ComponentData::fromArray(['count' => 10]), events: [['type' => 'incremented']] ); $this->handler ->shouldReceive('handleAction') ->once() ->andReturn($update); $this->componentRegistry ->shouldReceive('render') ->once() ->with($component) ->andReturn('
Counter: 10
'); $response = $this->processor->process($request); expect($response->totalOperations)->toBe(1); expect($response->successCount)->toBe(1); expect($response->failureCount)->toBe(0); expect($response->results[0]->success)->toBeTrue(); expect($response->results[0]->html)->toBe('
Counter: 10
'); expect($response->results[0]->state)->toBe(['count' => 10]); expect($response->results[0]->events)->toBe([['type' => 'incremented']]); }); it('processes multiple operations successfully', function () { $op1 = new BatchOperation('counter:demo', 'increment'); $op2 = new BatchOperation('stats:user', 'refresh'); $request = new BatchRequest($op1, $op2); $component1 = Mockery::mock(LiveComponent::class); $component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo')); $component1->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 1])); $component2 = Mockery::mock(LiveComponent::class); $component2->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user')); $component2->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100])); $this->componentRegistry ->shouldReceive('resolve') ->twice() ->andReturn($component1, $component2); $update1 = new ComponentUpdate( component: $component1, state: ComponentData::fromArray(['count' => 2]), events: [] ); $update2 = new ComponentUpdate( component: $component2, state: ComponentData::fromArray(['views' => 101]), events: [] ); $this->handler ->shouldReceive('handleAction') ->twice() ->andReturn($update1, $update2); $this->componentRegistry ->shouldReceive('render') ->twice() ->andReturn('
Counter: 2
', '
Views: 101
'); $response = $this->processor->process($request); expect($response->totalOperations)->toBe(2); expect($response->successCount)->toBe(2); expect($response->failureCount)->toBe(0); }); it('handles operation failure with error isolation', function () { $op1 = new BatchOperation('counter:demo', 'increment'); $op2 = new BatchOperation('invalid:component', 'action'); $op3 = new BatchOperation('stats:user', 'refresh'); $request = new BatchRequest($op1, $op2, $op3); $component1 = Mockery::mock(LiveComponent::class); $component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo')); $component1->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 1])); $component3 = Mockery::mock(LiveComponent::class); $component3->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user')); $component3->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100])); $this->componentRegistry ->shouldReceive('resolve') ->times(3) ->andReturnUsing(function ($id) use ($component1, $component3) { if ($id->toString() === 'invalid:component') { throw new InvalidArgumentException('Unknown component: invalid:component'); } return $id->toString() === 'counter:demo' ? $component1 : $component3; }); $update1 = new ComponentUpdate( component: $component1, state: ComponentData::fromArray(['count' => 2]), events: [] ); $update3 = new ComponentUpdate( component: $component3, state: ComponentData::fromArray(['views' => 101]), events: [] ); $this->handler ->shouldReceive('handleAction') ->twice() ->andReturn($update1, $update3); $this->componentRegistry ->shouldReceive('render') ->twice() ->andReturn('
Counter: 2
', '
Views: 101
'); $response = $this->processor->process($request); expect($response->totalOperations)->toBe(3); expect($response->successCount)->toBe(2); expect($response->failureCount)->toBe(1); // First operation succeeds expect($response->results[0]->success)->toBeTrue(); // Second operation fails expect($response->results[1]->success)->toBeFalse(); expect($response->results[1]->error)->toContain('Unknown component'); expect($response->results[1]->errorCode)->toBe('COMPONENT_NOT_FOUND'); // Third operation succeeds (error isolation works) expect($response->results[2]->success)->toBeTrue(); }); it('processes fragment-based operation', function () { $operation = new BatchOperation( componentId: 'counter:demo', method: 'increment', fragments: ['counter-display'] ); $request = new BatchRequest($operation); $component = Mockery::mock(LiveComponent::class); $component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo')); $component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5])); $this->componentRegistry ->shouldReceive('resolve') ->once() ->andReturn($component); $update = new ComponentUpdate( component: $component, state: ComponentData::fromArray(['count' => 10]), events: [] ); $this->handler ->shouldReceive('handleAction') ->once() ->andReturn($update); $fragmentCollection = FragmentCollection::fromArray([ 'counter-display' => '10', ]); $this->fragmentRenderer ->shouldReceive('renderFragments') ->once() ->with($component, ['counter-display']) ->andReturn($fragmentCollection); $response = $this->processor->process($request); expect($response->successCount)->toBe(1); expect($response->results[0]->fragments)->toBe(['counter-display' => '10']); expect($response->results[0]->html)->toBeNull(); }); it('preserves operation ids in results', function () { $op1 = new BatchOperation('counter:demo', 'increment', operationId: 'op-1'); $op2 = new BatchOperation('stats:user', 'refresh', operationId: 'op-2'); $request = new BatchRequest($op1, $op2); $component1 = Mockery::mock(LiveComponent::class); $component1->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo')); $component1->shouldReceive('getData')->andReturn(ComponentData::fromArray([])); $component2 = Mockery::mock(LiveComponent::class); $component2->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user')); $component2->shouldReceive('getData')->andReturn(ComponentData::fromArray([])); $this->componentRegistry ->shouldReceive('resolve') ->twice() ->andReturn($component1, $component2); $this->handler ->shouldReceive('handleAction') ->twice() ->andReturn( new ComponentUpdate($component1, ComponentData::fromArray([]), []), new ComponentUpdate($component2, ComponentData::fromArray([]), []) ); $this->componentRegistry ->shouldReceive('render') ->twice() ->andReturn('
1
', '
2
'); $response = $this->processor->process($request); expect($response->results[0]->operationId)->toBe('op-1'); expect($response->results[1]->operationId)->toBe('op-2'); }); });