TenantAtlas/apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php
ahmido 9a564d6bf2 feat: environment dashboard operator guidance consolidation (spec 352) (#423)
Implemented the consolidated operator guidance panel for the environment dashboard. Updated EnvironmentDashboardSummaryBuilder to prioritize and select guidance based on the operator guidance contract. Added comprehensive unit, feature, and browser tests to verify the guidance selection logic and UI rendering.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #423
2026-06-04 12:56:02 +00:00

275 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\EnvironmentDashboard;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Widgets\Dashboard\EnvironmentDashboardOverview;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
use App\Support\EnvironmentReviewStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use function Pest\Laravel\mock;
uses(RefreshDatabase::class);
function mockSpec352FeatureDashboardPermissions(array $overview = []): void
{
mock(ManagedEnvironmentRequiredPermissionsViewModelBuilder::class, function ($mock) use ($overview): void {
$mock->shouldReceive('build')->andReturn([
'overview' => array_replace_recursive([
'overall' => 'ready',
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
],
'freshness' => [
'is_stale' => false,
'last_refreshed_at' => now()->toIso8601String(),
],
], $overview),
]);
});
}
/**
* @return array{
* review:\App\Models\EnvironmentReview,
* successor:\App\Models\EnvironmentReview|null,
* reviewPack:ReviewPack,
* }
*/
function spec352FeatureSeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array
{
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now()->subHour(),
'published_by_user_id' => (int) $user->getKey(),
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
'publish_blockers' => ['Operator approval note is still missing.'],
]),
])->save();
$reviewPack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => 'review-packs/spec352-feature.zip',
'file_disk' => 'exports',
'generated_at' => now()->subMinutes(10),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
])->save();
$successor = null;
if ($withSuccessorDraft) {
$successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot);
}
return [
'review' => $review->fresh(),
'successor' => $successor?->fresh(),
'reviewPack' => $reviewPack->fresh(),
];
}
/**
* @return list<string>
*/
function spec352DashboardHrefs(string $html, string $xpathExpression): array
{
$dom = new DOMDocument;
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$nodes = $xpath->query($xpathExpression);
if ($nodes === false) {
return [];
}
return collect(iterator_to_array($nodes))
->map(static fn (DOMNode $node): string => (string) $node->nodeValue)
->filter()
->values()
->all();
}
function spec352DashboardNodeCount(string $html, string $xpathExpression): int
{
$dom = new DOMDocument;
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$nodes = $xpath->query($xpathExpression);
if ($nodes === false) {
return 0;
}
return count(iterator_to_array($nodes));
}
it('renders review output guidance as one dominant top case while preserving proof surfaces', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminPanelContext($environment);
$component = Livewire::test(EnvironmentDashboardOverview::class)
->assertSee('Draft review exists')
->assertSee('Open draft review')
->assertSee('Additional follow-ups')
->assertSee('Readiness dimensions')
->assertSee('Readiness proof')
->assertSee('Supporting signals')
->assertSee('Recommended next action');
$html = $component->html();
expect(substr_count($html, 'data-testid="tenant-dashboard-operator-guidance-title"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-primary-next-action"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-operator-guidance-secondary-actions"'))->toBe(1)
->and(substr_count($html, 'data-recommended-actions-style="compact"'))->toBe(1)
->and($html)->not->toContain('No single repo-real follow-up is currently available.')
->and(spec352DashboardNodeCount($html, "//*[@data-testid='tenant-dashboard-recommended-action']//*[contains(@class, 'fi-btn')]"))->toBe(0)
->and(substr_count($html, 'data-testid="tenant-dashboard-readiness-proof-panel"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-supporting-signals"'))->toBe(1);
});
it('keeps dashboard guidance links scoped to the current environment and avoids mutation CTAs in the top block', function (): void {
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec352 Active Environment']);
$otherEnvironment = ManagedEnvironment::factory()->create(['name' => 'Spec352 Other Environment']);
[$user, $environment] = createUserWithTenant(
tenant: $environment,
role: 'owner',
workspaceRole: 'manager',
);
createUserWithTenant(
tenant: $otherEnvironment,
user: $user,
role: 'owner',
workspaceRole: 'manager',
);
mockSpec352FeatureDashboardPermissions();
$activeState = spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$otherState = spec352FeatureSeedBlockedReviewOutput($otherEnvironment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminPanelContext($environment);
$component = Livewire::test(EnvironmentDashboardOverview::class)
->assertSee('Draft review exists')
->assertSee('Open draft review');
$html = $component->html();
$primaryHref = spec352DashboardHrefs(
$html,
"//*[@data-testid='tenant-dashboard-primary-next-action']//a/@href",
);
$secondaryHrefs = spec352DashboardHrefs(
$html,
"//*[@data-testid='tenant-dashboard-operator-guidance-secondary-action']/@href",
);
expect($primaryHref)->not->toBe([])
->and(collect($primaryHref)->contains(EnvironmentReviewResource::environmentScopedUrl(
'view',
['record' => $activeState['successor']],
$environment,
)))->toBeTrue()
->and($secondaryHrefs)->not->toBe([])
->and(collect($secondaryHrefs)->contains(
fn (string $href): bool => str_contains($href, (string) $environment->getRouteKey())
|| str_contains($href, (string) $environment->workspace->slug)
))->toBeTrue()
->and(collect($secondaryHrefs)->contains(
fn (string $href): bool => str_contains($href, (string) $otherEnvironment->getRouteKey())
|| str_contains($href, (string) $otherEnvironment->workspace->slug)
|| str_contains($href, '/environment-reviews/'.($otherState['successor']?->getKey()))
))->toBeFalse()
->and($html)->not->toContain('createNextReview')
->and($html)->not->toContain('publishReview')
->and($html)->not->toContain('refreshReviewInputs');
});
it('keeps only one clickable primary CTA on the page when review-output guidance is dominant', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminEnvironmentContext($environment);
$content = $this->get(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->assertSuccessful()
->assertSee('Draft review exists')
->assertSee('Open draft review')
->assertDontSee('No single repo-real follow-up is currently available.')
->getContent();
expect(spec352DashboardNodeCount(
$content,
"//a[contains(normalize-space(.), 'Open draft review')]",
))->toBe(1);
});
it('renders the no-urgent-action state without Graph or outbound HTTP during dashboard page render', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
bindFailHardGraphClient();
workspaceOverviewSeedQuietTenantTruth($environment);
$backupSet = workspaceOverviewSeedHealthyBackup($environment);
workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed');
setAdminEnvironmentContext($environment);
assertNoOutboundHttp(function () use ($user, $environment): void {
$this->actingAs($user)
->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $environment->workspace_id => (int) $environment->getKey(),
],
])
->get(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->assertSuccessful()
->assertSeeText('No urgent operator action')
->assertSeeText('Review environment')
->assertSeeText('Readiness dimensions')
->assertSeeText('Readiness proof');
});
});