actingAs($user); Filament::setTenant($tenant, true); $firstRun = null; foreach (range(0, 3) as $offset) { $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'queued', 'outcome' => 'pending', 'created_at' => now()->subMinutes($offset), ]); if ($offset === 0) { $firstRun = $run; } } $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = $component->html(); expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(3) ->and($html)->toContain('Active operations') ->and(substr_count($html, 'Review operations'))->toBe(1) ->and(substr_count($html, 'View operation'))->toBe(0) ->and($html)->toContain('data-testid="ops-ux-activity-feedback-actions"') ->and($html)->toContain('data-testid="ops-ux-activity-feedback-primary-action"') ->and($html)->toContain('Show all operations') ->and($html)->toContain('Hide activity') ->and($html)->not->toContain('Dismiss') ->and($html)->not->toContain('Acknowledge') ->and($html)->toContain(OperationRunUrl::index($tenant)) ->and($html)->not->toContain($firstRun instanceof OperationRun ? OperationRunUrl::view($firstRun, $tenant) : '') ->and($html)->not->toContain('Open operation'); })->group('ops-ux'); it('renders a single visible operation with a detail primary action', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'queued', 'outcome' => 'pending', 'created_at' => now()->subSeconds(10), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = $component->html(); expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(1) ->and($html)->toContain('Active operations') ->and(substr_count($html, 'View operation'))->toBe(1) ->and($html)->not->toContain('Review operations') ->and($html)->toContain(OperationRunUrl::view($run, $tenant)) ->and($html)->toContain(OperationRunUrl::index($tenant)) ->and($html)->toContain('Hide activity') ->and($html)->not->toContain('Dismiss') ->and($html)->not->toContain('Acknowledge'); })->group('ops-ux'); it('renders a recent terminal success state with dismiss instead of hide activity', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'completed', 'outcome' => 'succeeded', 'started_at' => now()->subMinutes(2), 'completed_at' => now()->subSeconds(8), 'summary_counts' => [ 'total' => 10, 'processed' => 10, ], ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $pageText = preg_replace('/\s+/', ' ', strip_tags($html)); expect($component->get('hasActiveRuns'))->toBeFalse() ->and($html)->toContain('Operation updates') ->and($pageText)->toContain('Completed successfully') ->and($pageText)->toContain('No action needed.') ->and($html)->toContain('View operation') ->and($html)->not->toContain('Review operations') ->and($html)->toContain('Dismiss') ->and($html)->not->toContain('Hide activity') ->and($html)->not->toContain('data-testid="ops-ux-activity-feedback-indeterminate"') ->and($html)->not->toContain('role="progressbar"'); })->group('ops-ux'); it('renders terminal follow-up states with dismiss instead of hide activity', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'completed', 'outcome' => 'failed', 'started_at' => now()->subMinutes(3), 'completed_at' => now()->subSeconds(6), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); expect($component->get('hasActiveRuns'))->toBeFalse() ->and($html)->toContain('Operation updates') ->and($html)->toContain('View operation') ->and($html)->not->toContain('Review operations') ->and($html)->toContain('Dismiss') ->and($html)->not->toContain('Acknowledge') ->and($html)->not->toContain('data-testid="ops-ux-activity-feedback-indeterminate"') ->and($html)->not->toContain('role="progressbar"') ->and($html)->not->toContain('Hide activity'); })->group('ops-ux'); it('uses a collective primary action when active and terminal follow-up items are both visible', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); $runningRun = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'running', 'outcome' => 'pending', 'started_at' => now()->subMinute(), ]); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'completed', 'outcome' => 'failed', 'started_at' => now()->subMinutes(3), 'completed_at' => now()->subSeconds(6), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $pageText = preg_replace('/\s+/', ' ', strip_tags($html)); expect($html)->toContain('Review operations') ->and($html)->toContain('Operation updates') ->and($html)->not->toContain('View operation') ->and($html)->toContain('Hide activity') ->and($html)->toContain(OperationRunUrl::index($tenant)) ->and($html)->not->toContain(OperationRunUrl::view($runningRun, $tenant)) ->and($pageText)->toContain('Execution failed'); })->group('ops-ux'); it('renders an indeterminate running indicator when processed totals are not trustworthy', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'processed' => 4, ], 'started_at' => now()->subMinute(), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $pageText = preg_replace('/\s+/', ' ', strip_tags($html)); expect($html)->toContain('data-testid="ops-ux-activity-feedback-indeterminate"') ->and($html)->toContain('Active operations') ->and($pageText)->toMatch('/Running · .* · Progress details pending\./') ->and($html)->not->toContain('role="progressbar"') ->and($html)->toContain('View operation') ->and($html)->not->toContain('Review operations') ->and($html)->toContain('Hide activity'); })->group('ops-ux'); it('shows determinate progress with truthful processed totals and percent when summary counts are valid', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'total' => 10, 'processed' => 4, ], 'started_at' => now()->subWeeks(2), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $pageText = preg_replace('/\s+/', ' ', strip_tags($html)); expect($html)->toContain('role="progressbar"') ->and($html)->toContain('aria-valuenow="40"') ->and($html)->toContain('Active operations') ->and($pageText)->toMatch('/Running · .* · 4 \/ 10 processed \(40%\)/') ->and($html)->toContain('View operation') ->and($html)->not->toContain('Review operations') ->and($html)->not->toContain('data-testid="ops-ux-activity-feedback-indeterminate"') ->and($html)->not->toContain('Likely stale'); })->group('ops-ux'); it('renders an indeterminate queued indicator without fake determinate progress', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'queued', 'outcome' => 'pending', 'summary_counts' => [ 'processed' => 4, ], 'created_at' => now()->subSeconds(15), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5); $pageText = preg_replace('/\s+/', ' ', strip_tags($html)); expect($html)->toContain('data-testid="ops-ux-activity-feedback-indeterminate"') ->and($html)->toContain('Active operations') ->and($pageText)->toContain('Queued · now · Waiting for worker.') ->and($html)->not->toContain('aria-valuenow=') ->and($html)->toContain('View operation') ->and($html)->not->toContain('Review operations') ->and($html)->toContain('Hide activity') ->and(strip_tags($html))->not->toContain('processed ('); })->group('ops-ux'); it('keeps the queued status pill on one line for the compact banner layout', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'queued', 'outcome' => 'pending', 'created_at' => now()->subSeconds(20), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $html = $component->html(); preg_match('/class="([^"]+)"[^>]*data-testid="ops-ux-activity-feedback-status-pill"/m', $html, $matches); expect($matches[1] ?? '')->toContain('whitespace-nowrap') ->and($html)->toContain('Queued for execution'); })->group('ops-ux'); it('renders the activity banner inside the tenant shell instead of before the application chrome', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'running', 'outcome' => 'pending', 'started_at' => now()->subMinute(), ]); $response = $this->actingAs($user) ->get(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant)); $response->assertOk(); $content = $response->getContent(); expect(strpos($content, 'Tenant-Dashboard'))->toBeLessThan(strpos($content, 'Active operations')) ->and($content)->not->toContain('fixed bottom-4 right-4 z-[999999] w-96 space-y-2'); })->group('ops-ux'); it('registers browser-session collapse affordances and run-enqueued reopen wiring for active hints', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => 'running', 'outcome' => 'pending', 'started_at' => now()->subMinute(), ]); $component = Livewire::actingAs($user) ->test(BulkOperationProgress::class) ->call('refreshRuns'); $script = file_get_contents(public_path('js/tenantpilot/ops-ux-progress-widget-poller.js')); expect($component->html())->toContain('data-testid="ops-ux-activity-feedback-toggle"') ->and($component->html())->toContain('data-testid="ops-ux-activity-feedback-expand"') ->and($script)->toContain('sessionStorage') ->and($script)->toContain('ops-ux:run-enqueued'); })->group('ops-ux');