## Summary - align the Baseline Compare landing page with the shared Product Process Flow contract introduced by Spec 332 - add the horizontal flow rendering primitive and update the landing view/state presentation for readiness, proof, evidence, and next action - add Spec 336 artifacts, screenshots, focused feature coverage, and browser smoke coverage for the aligned states ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php` ## Notes - Filament v5 / Livewire v4 stack remains unchanged - no panel provider registration changes; `bootstrap/providers.php` is unaffected - no global-search resource behavior changes - no new destructive actions and no asset registration/deployment changes Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #406
234 lines
14 KiB
PHP
234 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\BaselineCompareLanding;
|
|
use App\Models\Finding;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
it('Spec330 smokes environment dashboard and baseline compare decision surfaces', function (): void {
|
|
[$user, $environment] = spec330DecisionSurfaceFixture();
|
|
spec330AuthenticateBrowser($this, $user, $environment);
|
|
|
|
visit(ManagedEnvironmentLinks::viewUrl($environment))
|
|
->waitForText('Is this environment ready, blocked, stale, or requiring review?')
|
|
->assertSee('Readiness proof')
|
|
->assertSee('Readiness dimensions')
|
|
->assertSee('Recommended next actions')
|
|
->assertSee('Supporting signals')
|
|
->assertSee('Additional readiness signals used to explain the current recommendation.')
|
|
->assertSee('Signal')
|
|
->assertSee('State')
|
|
->assertSee('Action')
|
|
->assertSee('Unavailable')
|
|
->assertSee('Not ready')
|
|
->assertSee('Absent')
|
|
->assertSee('No active review')
|
|
->assertSee('No customer-safe output')
|
|
->assertSee('Operations follow-up')
|
|
->assertSee('3 require review')
|
|
->assertSee('Open operations hub')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('Secondary context')
|
|
->assertDontSee('Show operation details')
|
|
->assertDontSee('View operation details')
|
|
->assertDontSee('Ab...')
|
|
->assertDontSee('Unava...')
|
|
->assertDontSee('Not re...')
|
|
->assertDontSee('No customer-saf...')
|
|
->assertDontSee('Baseline assig...')
|
|
->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-secondary-context\"]") === null', true)
|
|
->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-operation-details\"]") === null', true)
|
|
->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-diagnostics\"]")?.open === false', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-readiness-decision\"]").length === 1', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-readiness-card\"]").length === 0', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-supporting-signal\"]").length === 6', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-operations-attention-item\"]").length === 0', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"tenant-dashboard-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true)
|
|
->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-supporting-signals\"]").compareDocumentPosition(document.querySelector("[data-testid=\"tenant-dashboard-diagnostics\"]")) & Node.DOCUMENT_POSITION_FOLLOWING ? true : false', true)
|
|
->assertDontSee('Fully ready')
|
|
->assertDontSee('MANAGED_ENVIRONMENT')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider response should stay hidden')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec330Screenshot('environment-dashboard-readiness-workbench'));
|
|
|
|
visit(ManagedEnvironmentLinks::baselineCompareUrl($environment))
|
|
->waitForText('Which baseline drift requires action?')
|
|
->assertSee('Assigned baseline')
|
|
->assertSee('Compare trust')
|
|
->assertSee('Drift impact')
|
|
->assertSee('Evidence path')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true)
|
|
->assertDontSee('MANAGED_ENVIRONMENT')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('raw payload should stay hidden')
|
|
->assertDontSee('provider response should stay hidden')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec330Screenshot('baseline-compare-decision-workbench'));
|
|
});
|
|
|
|
it('Spec330 smokes no-baseline, invalid environment denial, and static tenant-copy guard', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$environment->forceFill(['name' => 'Spec330 Tenant Display Name'])->save();
|
|
createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']);
|
|
spec330AuthenticateBrowser($this, $user, $environment);
|
|
|
|
visit(ManagedEnvironmentLinks::baselineCompareUrl($environment))
|
|
->waitForText('Which baseline drift requires action?')
|
|
->assertSee('Baseline not assigned')
|
|
->assertSee('Baseline drift cannot be used for governance decisions until a baseline assignment exists.')
|
|
->assertSee('Evidence unavailable')
|
|
->assertSee('Open baseline profiles to assign a baseline to this environment.')
|
|
->assertSee('Compare readiness flow')
|
|
->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.')
|
|
->assertSee('Baseline assigned')
|
|
->assertSee('Missing')
|
|
->assertSee('Baseline snapshot')
|
|
->assertSee('Environment snapshot')
|
|
->assertSee('Environment snapshot evidence is present.')
|
|
->assertSee('Compare run')
|
|
->assertSee('Decision output')
|
|
->assertSee('Available inputs')
|
|
->assertSee('Operation proof')
|
|
->assertSee('Unavailable because no baseline assigned.')
|
|
->assertSee('What this unlocks after assignment')
|
|
->assertSee('Actionable drift categories')
|
|
->assertSee('Evidence-backed compare')
|
|
->assertSee('Governance decision path')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertSee('Spec330 Tenant Display Name')
|
|
->assertDontSee('This environment does not have an assigned baseline yet.')
|
|
->assertDontSee('No coverage warning is currently reported for the latest compare.')
|
|
->assertDontSee('Readiness overview')
|
|
->assertDontSee('Recent baseline activity')
|
|
->assertDontSee('0% Ready')
|
|
->assertDontSee('Drift impact')
|
|
->assertDontSee('MANAGED_ENVIRONMENT')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('Evidence gap details')
|
|
->assertDontSee('tenant filter')
|
|
->assertDontSee('all tenants')
|
|
->assertDontSee('choose tenant')
|
|
->assertDontSee('tenant scope')
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-decision-workbench\"]").length === 1', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"]").length === 0', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-proof-item\"]").length === 0', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-readiness-step\"]").length === 5', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-readiness-connector\"]").length === 4', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-readiness-connector\"]")).every((connector) => connector.innerText.trim() === String.fromCharCode(8594))', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Baseline assigned\"]")?.dataset.stepCurrentBlocker === "true"', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Baseline assigned\"]")?.innerText.includes("Missing") === true', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Environment snapshot\"]")?.innerText.includes("Available") === true', true)
|
|
->assertScript('document.querySelector("[data-step-label=\"Compare run\"]")?.innerText.includes("Unavailable") === true', true)
|
|
->assertScript('(function () { const steps = Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-readiness-step\"]")); if (steps.length !== 5) { return false; } const tops = steps.map((step) => step.getBoundingClientRect().top); return Math.max(...tops) - Math.min(...tops) < 12; })()', true)
|
|
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-available-input\"]").length === 3', true)
|
|
->assertScript('document.querySelector("[data-input-label=\"Environment snapshot\"]")?.innerText.includes("Available") === true', true)
|
|
->assertScript('document.querySelector("[data-input-label=\"Operation proof\"]")?.innerText.includes("Unavailable") === true', true)
|
|
->assertScript('document.querySelector("[data-testid=\"baseline-compare-assignment-unlocks\"]") !== null', true)
|
|
->assertScript('document.body.innerText.includes("No Baseline Assigned") === false', true)
|
|
->assertScript('document.body.innerText.includes("Assigned baseline") === false', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Assigned baseline").length === 0', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Compare trust").length === 0', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Drift impact").length === 0', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-proof-panel\"] .text-xs.font-semibold")).filter((element) => element.innerText.trim() === "Evidence path").length === 1', true)
|
|
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true)
|
|
->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->screenshot(true, spec330Screenshot('baseline-compare-no-baseline'));
|
|
|
|
visit('/admin/baseline-compare-landing?environment_id='.(int) $environment->getKey())
|
|
->assertSee('404')
|
|
->assertNoJavaScriptErrors();
|
|
});
|
|
|
|
/**
|
|
* @return array{0: User, 1: ManagedEnvironment}
|
|
*/
|
|
function spec330DecisionSurfaceFixture(): array
|
|
{
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
|
|
|
$run = seedBaselineCompareRun($environment, $profile, $snapshot, [
|
|
'reason_code' => \App\Support\Baselines\BaselineCompareReasonCode::CoverageUnproven->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration', 'deviceCompliancePolicy'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => ['deviceCompliancePolicy'],
|
|
'proof' => true,
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => [
|
|
'inventory_record_missing' => 1,
|
|
],
|
|
],
|
|
'diagnostics' => [
|
|
'support_only' => 'raw payload should stay hidden',
|
|
'provider_response' => 'provider response should stay hidden',
|
|
],
|
|
], OperationRunStatus::Completed->value, OperationRunOutcome::PartiallySucceeded->value);
|
|
|
|
Finding::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'scope_key' => 'baseline_profile:'.$profile->getKey(),
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
'status' => Finding::STATUS_NEW,
|
|
'source' => OperationRunType::BaselineCompare->value,
|
|
'baseline_operation_run_id' => (int) $run->getKey(),
|
|
]);
|
|
|
|
foreach (range(1, 2) as $index) {
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subHours($index),
|
|
]);
|
|
}
|
|
|
|
return [$user, $environment];
|
|
}
|
|
|
|
function spec330AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
|
|
{
|
|
$workspaceId = (int) $environment->workspace_id;
|
|
|
|
$test->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
|
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
|
(string) $workspaceId => (int) $environment->getKey(),
|
|
]);
|
|
}
|
|
|
|
function spec330Screenshot(string $name): string
|
|
{
|
|
return 'spec330-'.$name;
|
|
}
|