browser()->timeout(30_000); uses(RefreshDatabase::class); function spec354BrowserScreenshot(string $name): string { return 'spec354-'.$name; } function spec354CopyBrowserScreenshot(string $name): void { $filename = spec354BrowserScreenshot($name).'.png'; $source = base_path('tests/Browser/Screenshots/'.$filename); $targetDirectory = repo_path('specs/354-finding-exceptions-accepted-risk-resolution-guidance-v1/artifacts/screenshots'); if (! is_dir($targetDirectory)) { @mkdir($targetDirectory, 0755, true); } if (! is_file($source)) { $source = \Pest\Browser\Support\Screenshot::path($filename); } for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) { usleep(100_000); clearstatcache(true, $source); } if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) { @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$filename); } } function spec354AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $tenant): void { $workspaceId = (int) $tenant->workspace_id; $test->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspaceId => (int) $tenant->getKey(), ], ]); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenant->getKey(), ]); setAdminPanelContext($tenant); } function spec354BrowserException( ManagedEnvironment $tenant, User $user, array $findingAttributes = [], array $exceptionAttributes = [], ?string $decisionType = FindingExceptionDecision::TYPE_APPROVED, ): FindingException { $decisionMetadata = is_array($exceptionAttributes['decision_metadata'] ?? null) ? $exceptionAttributes['decision_metadata'] : []; unset($exceptionAttributes['decision_metadata']); $finding = Finding::factory() ->for($tenant) ->riskAccepted() ->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, ], $findingAttributes)); $exception = FindingException::query()->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'approved_by_user_id' => (int) $user->getKey(), 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'request_reason' => 'Spec354 browser accepted-risk guidance', 'approval_reason' => 'Spec354 browser approval', 'requested_at' => now()->subDays(5), 'approved_at' => now()->subDays(4), 'effective_from' => now()->subDays(4), 'review_due_at' => now()->addDays(10), 'expires_at' => now()->addDays(30), 'evidence_summary' => ['reference_count' => 0], ], $exceptionAttributes)); if ($decisionType !== null) { $decision = $exception->decisions()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'actor_user_id' => (int) $user->getKey(), 'decision_type' => $decisionType, 'reason' => 'Spec354 browser decision', 'metadata' => $decisionMetadata, 'decided_at' => now()->subDays(4), ]); $exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save(); } return $exception->fresh(['finding', 'tenant', 'owner', 'currentDecision', 'decisions.actor', 'evidenceReferences']); } it('smokes expiring queue guidance', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $expiring = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'review_due_at' => now()->addDay(), 'expires_at' => now()->addDays(2), ]); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'exception' => (int) $expiring->getKey(), ])) ->resize(1440, 1100) ->waitForText(__('localization.accepted_risk_guidance.title_expiring')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.review_focus_label')) ->assertSee(__('localization.accepted_risk_guidance.next_step_expiring')) ->assertSee(__('localization.accepted_risk_guidance.action_open_exception')) ->screenshot(true, spec354BrowserScreenshot('ui-026-finding-exceptions-queue-guidance')); spec354CopyBrowserScreenshot('ui-026-finding-exceptions-queue-guidance'); }); it('smokes expired queue guidance', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $expired = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'review_due_at' => now()->subDays(2), 'expires_at' => now()->subDay(), ]); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'exception' => (int) $expired->getKey(), ])) ->waitForText(__('localization.accepted_risk_guidance.title_expired')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.impact_expired')); }); it('smokes pending-renewal queue guidance while governance remains non-lapsed', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $pendingRenewalValid = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, 'decision_metadata' => [ 'previous_review_due_at' => now()->addDays(10)->toIso8601String(), 'previous_expires_at' => now()->addDays(30)->toIso8601String(), ], ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'exception' => (int) $pendingRenewalValid->getKey(), ])) ->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.next_step_pending_renewal')); }); it('smokes pending-renewal queue guidance when expired governance stays dominant', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $pendingRenewalExpired = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, 'decision_metadata' => [ 'previous_review_due_at' => now()->subDays(2)->toIso8601String(), 'previous_expires_at' => now()->subDay()->toIso8601String(), ], ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'exception' => (int) $pendingRenewalExpired->getKey(), ])) ->waitForText(__('localization.accepted_risk_guidance.title_expired')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertDontSee(__('localization.accepted_risk_guidance.title_pending_renewal')); }); it('smokes german queue localization without fake remediation copy', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $expiring = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'review_due_at' => now()->addDay(), 'expires_at' => now()->addDays(2), ]); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'exception' => (int) $expiring->getKey(), 'locale' => 'de', ])) ->waitForText('Was zu prüfen ist') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee('Das aktuelle Accepted-Risk-Governance-Fenster ist noch aktiv, läuft aber bald ab und muss geprüft werden.') ->assertDontSee('The current accepted-risk governance window is still active, but it is nearing expiry and needs review.') ->assertDontSee('Fix accepted risk'); }); it('smokes detail guidance hierarchy and pending-renewal queue continuity', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $incomplete = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'owner_user_id' => null, 'request_reason' => '', 'review_due_at' => null, ]); $pendingRenewalValid = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, 'decision_metadata' => [ 'previous_review_due_at' => now()->addDays(10)->toIso8601String(), 'previous_expires_at' => now()->addDays(30)->toIso8601String(), ], ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionResource::getUrl('view', ['record' => $incomplete], tenant: $tenant)) ->resize(1440, 1100) ->waitForText(__('localization.accepted_risk_guidance.title_incomplete_governance')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.detail_missing_fields_label')) ->assertSee('Renew exception') ->screenshot(true, spec354BrowserScreenshot('ui-036-exception-detail-guidance')); spec354CopyBrowserScreenshot('ui-036-exception-detail-guidance'); visit(FindingExceptionResource::getUrl('view', ['record' => $pendingRenewalValid], tenant: $tenant)) ->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.primary_action_label')) ->assertSee(__('localization.accepted_risk_guidance.next_step_pending_renewal')) ->click(__('localization.accepted_risk_guidance.next_step_pending_renewal')) ->waitForText(__('localization.accepted_risk_guidance.title_pending_renewal')) ->assertSee($tenant->name); }); it('smokes ready and missing-support detail semantics without fake remediation', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $ready = spec354BrowserException($tenant, $user); $missingSupport = spec354BrowserException($tenant, $user, exceptionAttributes: [ 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, ]); spec354AuthenticateBrowser($this, $user, $tenant); visit(FindingExceptionResource::getUrl('view', ['record' => $ready], tenant: $tenant)) ->resize(1440, 1100) ->waitForText(__('localization.accepted_risk_guidance.title_ready')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.review_focus_label')) ->assertSee(__('localization.accepted_risk_guidance.impact_ready')) ->assertDontSee(__('localization.accepted_risk_guidance.primary_action_label')) ->assertDontSee(__('localization.accepted_risk_guidance.title_expired')); visit(FindingExceptionResource::getUrl('view', ['record' => $missingSupport], tenant: $tenant)) ->waitForText(__('localization.accepted_risk_guidance.title_missing_support')) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() ->assertSee(__('localization.accepted_risk_guidance.review_focus_label')) ->assertSee(__('localization.accepted_risk_guidance.next_step_missing_support')) ->assertDontSee(__('localization.accepted_risk_guidance.primary_action_label')) ->assertDontSee('Fix accepted risk') ->assertDontSee('Resolve risk'); });