deadLetterManager = Mockery::mock(DeadLetterManager::class); $this->componentId = ComponentId::create('failed-jobs', 'test'); $this->initialState = FailedJobsState::empty(); }); afterEach(function () { Mockery::close(); }); it('polls and updates state with failed jobs', function () { $failedJob1 = new DeadLetterJob( id: JobId::generate(), queueName: new QueueName('default'), jobType: 'SendEmailJob', payload: ['email' => 'test@example.com'], exception: 'Connection timeout', failedAt: Timestamp::now(), attempts: 3 ); $failedJob2 = new DeadLetterJob( id: JobId::generate(), queueName: new QueueName('high-priority'), jobType: 'ProcessPaymentJob', payload: ['amount' => 100], exception: 'Payment gateway error', failedAt: Timestamp::now(), attempts: 5 ); $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->with(50) ->andReturn([$failedJob1, $failedJob2]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->poll(); expect($newState)->toBeInstanceOf(FailedJobsState::class); expect($newState->totalFailedJobs)->toBe(2); expect($newState->failedJobs)->toHaveCount(2); expect($newState->failedJobs[0]['job_type'])->toBe('SendEmailJob'); expect($newState->failedJobs[0]['queue'])->toBe('default'); expect($newState->failedJobs[0]['attempts'])->toBe(3); }); it('handles retry job action successfully', function () { $jobId = JobId::generate(); $this->deadLetterManager->shouldReceive('retryJob') ->once() ->with($jobId->toString()) ->andReturn(true); // After retry, poll returns updated state $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->with(50) ->andReturn([]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->retryJob($jobId->toString()); expect($newState)->toBeInstanceOf(FailedJobsState::class); expect($newState->totalFailedJobs)->toBe(0); // Job was retried }); it('handles retry job action failure', function () { $jobId = JobId::generate(); $this->deadLetterManager->shouldReceive('retryJob') ->once() ->with($jobId->toString()) ->andReturn(false); // Still poll for current state $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->andReturn([]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->retryJob($jobId->toString()); expect($newState)->toBeInstanceOf(FailedJobsState::class); }); it('handles delete job action successfully', function () { $jobId = JobId::generate(); $this->deadLetterManager->shouldReceive('deleteJob') ->once() ->with($jobId->toString()) ->andReturn(true); // After delete, poll returns updated state $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->with(50) ->andReturn([]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->deleteJob($jobId->toString()); expect($newState)->toBeInstanceOf(FailedJobsState::class); expect($newState->totalFailedJobs)->toBe(0); }); it('dispatches events on retry success', function () { $jobId = JobId::generate(); $eventDispatcher = Mockery::mock(ComponentEventDispatcher::class); $this->deadLetterManager->shouldReceive('retryJob') ->once() ->andReturn(true); $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->andReturn([]); $eventDispatcher->shouldReceive('dispatch') ->once() ->with('failed-jobs:retry-success', ['jobId' => $jobId->toString()]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $component->retryJob($jobId->toString(), $eventDispatcher); }); it('has correct poll interval', function () { $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); expect($component->getPollInterval())->toBe(10000); }); it('returns correct render data', function () { $failedJobs = [ [ 'id' => 'job-1', 'queue' => 'default', 'job_type' => 'EmailJob', 'error' => 'Connection failed', 'failed_at' => '2024-01-15 12:00:00', 'attempts' => 3, ], ]; $state = new FailedJobsState( totalFailedJobs: 1, failedJobs: $failedJobs, statistics: ['total_retries' => 5], lastUpdated: '2024-01-15 12:00:00' ); $component = new FailedJobsListComponent( id: $this->componentId, state: $state, deadLetterManager: $this->deadLetterManager ); $renderData = $component->getRenderData(); expect($renderData->templatePath)->toBe('livecomponent-failed-jobs-list'); expect($renderData->data)->toHaveKey('componentId'); expect($renderData->data)->toHaveKey('pollInterval'); expect($renderData->data['pollInterval'])->toBe(10000); expect($renderData->data['totalFailedJobs'])->toBe(1); expect($renderData->data['failedJobs'])->toHaveCount(1); }); it('handles empty failed jobs list', function () { $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->with(50) ->andReturn([]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->poll(); expect($newState->totalFailedJobs)->toBe(0); expect($newState->failedJobs)->toBe([]); }); it('truncates payload preview to 100 characters', function () { $longPayload = str_repeat('a', 200); $failedJob = new DeadLetterJob( id: JobId::generate(), queueName: new QueueName('default'), jobType: 'LongJob', payload: ['data' => $longPayload], exception: 'Error', failedAt: Timestamp::now(), attempts: 1 ); $this->deadLetterManager->shouldReceive('getFailedJobs') ->once() ->with(50) ->andReturn([$failedJob]); $component = new FailedJobsListComponent( id: $this->componentId, state: $this->initialState, deadLetterManager: $this->deadLetterManager ); $newState = $component->poll(); $payloadPreview = $newState->failedJobs[0]['payload_preview']; expect(strlen($payloadPreview))->toBeLessThanOrEqual(103); // 100 + '...' }); });