TenantAtlas/apps/platform/tests/Browser/OpsUx/OperationActivityFeedbackSmokeTest.php
Ahmed Darrazi c1f4ebaa05
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m31s
chore: commit all changes (automated)
2026-05-05 17:20:17 +02:00

283 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\InventoryItemResource;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->browser()->timeout(20_000);
uses(RefreshDatabase::class);
function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, string $redirect = ''): string
{
return route('admin.local.smoke-login', array_filter([
'email' => $user->email,
'tenant' => $tenant->external_id,
'workspace' => $tenant->workspace->slug,
'redirect' => $redirect,
], static fn (?string $value): bool => filled($value)));
}
it('keeps findings row actions reachable while the activity hint collapses and reopens within the browser session', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
InventoryItem::factory()->count(3)->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'display_name' => 'Browser Inventory Item',
'policy_type' => 'deviceConfiguration',
'platform' => 'windows',
'last_seen_at' => now()->subMinute(),
]);
Finding::factory()->count(5)->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
'status' => Finding::STATUS_NEW,
]);
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()->subMinutes(2),
]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$inventoryPage = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
->resize(1440, 1200)
->assertScript('window.innerWidth >= 1400', true)
->waitForText('Inventory Items')
->waitForText('Browser Inventory Item')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$shellGeometry = $inventoryPage->script(<<<'JS'
(() => {
const banner = document.querySelector('[data-testid="ops-ux-activity-feedback-banner"]');
const topbar = document.querySelector('.fi-topbar');
const table = document.querySelector('.fi-ta');
const tableContainer = table?.closest('.overflow-x-auto') ?? table?.parentElement ?? null;
const contentShell = document.querySelector('.fi-page') ?? document.querySelector('.fi-main') ?? tableContainer?.parentElement ?? null;
const main = document.querySelector('.fi-main');
const header = banner?.querySelector('.tp-ops-activity-header') ?? null;
const actionGroup = document.querySelector('[data-testid="ops-ux-activity-feedback-actions"]');
const layout = banner?.querySelector('.tp-ops-activity-layout') ?? null;
const headerCopy = banner?.querySelector('.tp-ops-activity-header-copy') ?? null;
const title = banner?.querySelector('[data-testid="ops-ux-activity-feedback-title"]') ?? null;
const middleColumn = banner?.querySelector('.tp-ops-activity-summary') ?? null;
const track = banner?.querySelector('[data-testid="ops-ux-activity-feedback-track"]') ?? null;
if (!banner || !topbar || !tableContainer || !contentShell || !actionGroup || !layout || !header || !headerCopy || !title || !middleColumn || !track) {
return null;
}
const bannerRect = banner.getBoundingClientRect();
const topbarRect = topbar.getBoundingClientRect();
const tableRect = tableContainer.getBoundingClientRect();
const contentShellRect = contentShell.getBoundingClientRect();
const headerRect = header.getBoundingClientRect();
const actionGroupRect = actionGroup.getBoundingClientRect();
const headerCopyRect = headerCopy.getBoundingClientRect();
const titleRect = title.getBoundingClientRect();
const middleRect = middleColumn.getBoundingClientRect();
const trackRect = track.getBoundingClientRect();
const layoutStyle = window.getComputedStyle(layout);
const headerStyle = window.getComputedStyle(header);
const titleStyle = window.getComputedStyle(title);
const titleLineHeight = Number.parseFloat(titleStyle.lineHeight || '0');
return {
viewportWidth: window.innerWidth,
itemCount: banner.querySelectorAll('[data-testid="ops-ux-activity-feedback-item"]').length,
stylesheetCount: document.styleSheets.length,
hasThemeStylesheet: Array.from(document.styleSheets).some((sheet) => (sheet.href || '').includes('/build/assets/theme-')),
mainClassName: main?.className ?? '',
mainWidth: main?.getBoundingClientRect().width ?? 0,
layoutClassName: layout.className,
layoutDisplay: layoutStyle.display,
layoutFlexDirection: layoutStyle.flexDirection,
headerFlexDirection: headerStyle.flexDirection,
bannerTop: bannerRect.top,
topbarBottom: topbarRect.bottom,
bannerTopGap: bannerRect.top - topbarRect.bottom,
bannerHeight: bannerRect.height,
bannerWidth: bannerRect.width,
tableWidth: tableRect.width,
contentWidth: contentShellRect.width,
leftDelta: Math.abs(bannerRect.left - contentShellRect.left),
rightDelta: Math.abs(bannerRect.right - contentShellRect.right),
actionGroupRightDelta: Math.abs(bannerRect.right - actionGroupRect.right),
summaryTopDelta: middleRect.top - headerRect.bottom,
trackSpanRatio: middleRect.width > 0 ? trackRect.width / middleRect.width : 0,
titleOverflows: title.scrollWidth > (title.clientWidth + 1),
titleLineCount: titleLineHeight > 0 ? titleRect.height / titleLineHeight : 0,
headerCopyWidth: headerCopyRect.width,
middleHeight: middleRect.height,
actionHeight: actionGroupRect.height,
};
})()
JS);
expect($shellGeometry)->not->toBeNull()
->and($shellGeometry['viewportWidth'] ?? 0)->toBeGreaterThanOrEqual(1400)
->and($shellGeometry['itemCount'] ?? 0)->toBe(1)
->and($shellGeometry['stylesheetCount'] ?? 0)->toBeGreaterThan(0)
->and($shellGeometry['hasThemeStylesheet'] ?? false)->toBeTrue()
->and($shellGeometry['mainClassName'] ?? '')->toContain('fi-width-full')
->and($shellGeometry['mainWidth'] ?? 0)->toBeGreaterThanOrEqual(900)
->and($shellGeometry['layoutClassName'] ?? '')->toContain('tp-ops-activity-layout')
->and($shellGeometry['layoutClassName'] ?? '')->toContain('flex')
->and($shellGeometry['layoutDisplay'] ?? '')->toBe('flex')
->and($shellGeometry['layoutFlexDirection'] ?? '')->toBe('column')
->and($shellGeometry['headerFlexDirection'] ?? '')->toBe('row')
->and($shellGeometry['bannerTop'] ?? null)->toBeGreaterThanOrEqual($shellGeometry['topbarBottom'] ?? PHP_INT_MAX)
->and($shellGeometry['bannerTopGap'] ?? PHP_INT_MIN)->toBeGreaterThanOrEqual(12)
->and($shellGeometry['bannerWidth'] ?? 0)->toBeGreaterThan(0)
->and($shellGeometry['tableWidth'] ?? 0)->toBeGreaterThan(0)
->and($shellGeometry['contentWidth'] ?? 0)->toBeGreaterThan(0)
->and($shellGeometry['summaryTopDelta'] ?? PHP_INT_MIN)->toBeGreaterThanOrEqual(8)
->and($shellGeometry['trackSpanRatio'] ?? 0)->toBeGreaterThanOrEqual(0.72)
->and($shellGeometry['titleOverflows'] ?? true)->toBeFalse()
->and($shellGeometry['titleLineCount'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(1.2)
->and($shellGeometry['headerCopyWidth'] ?? 0)->toBeGreaterThanOrEqual(240)
->and($shellGeometry['leftDelta'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(32)
->and($shellGeometry['rightDelta'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(32)
->and($shellGeometry['actionGroupRightDelta'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(32)
->and($shellGeometry['bannerHeight'] ?? 0)->toBeGreaterThanOrEqual(96)
->and($shellGeometry['bannerHeight'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(145);
$inventoryPage
->assertScript('document.documentElement.scrollWidth <= window.innerWidth', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page = visit(FindingResource::getUrl('index', panel: 'tenant', tenant: $tenant))
->resize(1440, 1200);
$page
->waitForText('Triage all matching')
->waitForText('View operation')
->waitForText('Show all operations')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->click('tbody tr.fi-ta-row:last-of-type [aria-label="More"]')
->waitForText('Triage')
->assertSee('Triage')
->click('[data-testid="ops-ux-activity-feedback-toggle"]')
->waitForText('Show activity')
->refresh()
->waitForText('Show activity');
$page->script(<<<'JS'
window.dispatchEvent(new CustomEvent('ops-ux:run-enqueued', {
detail: {
tenantId: Number(document.querySelector('[data-testid="ops-ux-activity-feedback-root"]')?.dataset.tenantId || 0),
},
}));
JS);
$page
->waitForText('View operation')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->assertDontSee('Show activity');
});
it('keeps terminal follow-up acknowledge local to the browser session and reopens for new work', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$failedRun = 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(8),
]);
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
->waitForText('Dashboard')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page = visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
->resize(1440, 1200)
->waitForText('Inventory Items')
->waitForText('Acknowledge')
->assertSee('Review needed')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page
->click('[data-testid="ops-ux-activity-feedback-toggle"]')
->wait(1)
->assertScript(<<<'JS'
(() => {
const banner = document.querySelector('[data-testid="ops-ux-activity-feedback-banner"]');
return banner !== null && window.getComputedStyle(banner).display === 'none';
})()
JS, true)
->refresh()
->waitForText('Inventory Items')
->assertScript(<<<'JS'
(() => {
const banner = document.querySelector('[data-testid="ops-ux-activity-feedback-banner"]');
return banner !== null && window.getComputedStyle(banner).display === 'none';
})()
JS, true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
expect($failedRun->refresh()->status)->toBe('completed')
->and($failedRun->outcome)->toBe('failed');
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()->subSeconds(10),
]);
$page->script(<<<'JS'
window.dispatchEvent(new CustomEvent('ops-ux:run-enqueued', {
detail: {
tenantId: Number(document.querySelector('[data-testid="ops-ux-activity-feedback-root"]')?.dataset.tenantId || 0),
},
}));
JS);
$page
->waitForText('Review operations')
->waitForText('Acknowledge')
->assertScript(<<<'JS'
(() => {
const banner = document.querySelector('[data-testid="ops-ux-activity-feedback-banner"]');
return banner !== null && window.getComputedStyle(banner).display !== 'none';
})()
JS, true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
});