TenantAtlas/apps/platform/tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php
Ahmed Darrazi 4256e642ed
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m39s
chore: commit all changes (automated)
2026-05-05 12:38:55 +02:00

548 lines
21 KiB
PHP

<?php
use App\Filament\Resources\InventoryItemResource;
use App\Livewire\BulkOperationProgress;
use App\Models\OperationRun;
use App\Support\OpsUx\OperationRunUrl;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('renders three visible run summaries while grouping banner actions in one action area', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->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();
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(3)
->and($html)->toContain('Active operations')
->and($pageText)->toContain('Queued and running work stays here until diagnostics are needed.')
->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();
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(1)
->and($html)->toContain('Active operations')
->and($pageText)->toContain('Queued and running work stays here until diagnostics are needed.')
->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('Recent 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);
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
expect($component->get('hasActiveRuns'))->toBeFalse()
->and($html)->toContain('Operation updates')
->and($pageText)->toContain('Recent operation updates that may need review.')
->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($pageText)->toContain('Active and recent operation updates that may need review.')
->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 phased fallback progress without inventing a counted percentage', 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' => 'baseline_capture',
'status' => 'running',
'outcome' => 'pending',
'summary_counts' => [
'total' => 10,
'processed' => 4,
],
'context' => [
'baseline_capture' => [
'evidence_capture' => [
'requested' => 10,
'succeeded' => 3,
'skipped' => 1,
],
'resume_token' => 'resume-123',
],
],
'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($pageText)->toMatch('/Running · .* · Capturing evidence\./')
->and($html)->not->toContain('role="progressbar"')
->and(strip_tags($html))->not->toContain('processed (');
})->group('ops-ux');
it('renders tenant review composite progress from canonical composite metadata without inventing a counted percentage', 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' => 'tenant.review.compose',
'status' => 'running',
'outcome' => 'pending',
'context' => [
'progress' => [
'composite' => [
'label' => 'Review composition is aggregating 3 operations.',
'operation_count' => 3,
'failed_count' => 0,
'partial_count' => 0,
],
],
],
'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($pageText)->toMatch('/Running · .* · Review composition is aggregating 3 operations\./')
->and($html)->not->toContain('role="progressbar"')
->and(strip_tags($html))->not->toContain('processed (');
})->group('ops-ux');
it('renders composite fallback progress without inventing a counted percentage from aggregate counts', 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' => 'cross_tenant_promotion.execute',
'status' => 'running',
'outcome' => 'pending',
'summary_counts' => [
'total' => 10,
'processed' => 4,
'operation_count' => 3,
],
'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($pageText)->toMatch('/Running · .* · Composite progress pending\./')
->and($html)->not->toContain('role="progressbar"')
->and(strip_tags($html))->not->toContain('processed (');
})->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 outcome counters outcome only at the shell host', 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' => [
'succeeded' => 4,
'failed' => 1,
'skipped' => 2,
],
'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($pageText)->toMatch('/Running · .* · Progress details pending\./')
->and($html)->not->toContain('role="progressbar"')
->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');