browser()->timeout(60_000); beforeEach(function (): void { Storage::fake('exports'); }); it('Spec351 smokes blocked workspace execution guidance and ready detail execution guidance', function (): void { [$user, $publishedBlockedEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); $publishedBlockedEnvironment->forceFill(['name' => 'Spec351 Browser Published Blocked'])->save(); $mutableBlockedEnvironment = spec351BrowserEnvironmentFor($user, $publishedBlockedEnvironment, 'Spec351 Browser Mutable Blocked'); $readyDraftEnvironment = spec351BrowserEnvironmentFor($user, $publishedBlockedEnvironment, 'Spec351 Browser Ready Draft'); $fallbackEnvironment = spec351BrowserEnvironmentFor($user, $publishedBlockedEnvironment, 'Spec351 Browser Fallback'); [$readonlyUser] = createUserWithTenant( tenant: $fallbackEnvironment, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly', ); [$publishedBlockedReview] = spec351BrowserCreatePublishedReviewWithPack( $publishedBlockedEnvironment, $user, seedPartialEnvironmentReviewEvidence($publishedBlockedEnvironment, findingCount: 0, driftCount: 0), [ 'publish_blockers' => ['Operator approval note is still missing.'], ], [ 'include_pii' => false, 'include_operations' => true, ], 'review-packs/spec351-browser-blocked.zip', ); $mutableBlockedReview = composeEnvironmentReviewForTest( $mutableBlockedEnvironment, $user, seedPartialEnvironmentReviewEvidence($mutableBlockedEnvironment, findingCount: 0, driftCount: 0), ); $mutableBlockedReview->forceFill([ 'status' => EnvironmentReviewStatus::Draft->value, 'published_at' => null, 'published_by_user_id' => null, ])->save(); $readyReview = markEnvironmentReviewCustomerSafeReady(composeEnvironmentReviewForTest( $readyDraftEnvironment, $user, seedEnvironmentReviewEvidence($readyDraftEnvironment, findingCount: 0, driftCount: 0), )); $readyReview->forceFill([ 'status' => EnvironmentReviewStatus::Ready->value, 'published_at' => null, 'published_by_user_id' => null, ])->save(); spec351BrowserCreatePublishedReviewWithPack( $fallbackEnvironment, $user, seedPartialEnvironmentReviewEvidence($fallbackEnvironment, findingCount: 0, driftCount: 0), [ 'publish_blockers' => ['Operator approval note is still missing.'], ], [ 'include_pii' => false, 'include_operations' => true, ], 'review-packs/spec351-browser-fallback.zip', ); spec351AuthenticateBrowser($this, $user, $publishedBlockedEnvironment); $workspacePage = visit(CustomerReviewWorkspace::environmentFilterUrl($publishedBlockedEnvironment)) ->resize(1236, 900) ->waitForText('Output not customer-ready') ->assertSee('Create next review') ->assertSee('Supporting actions') ->assertSee('Package exists') ->assertSee('Customer sharing') ->click('[data-testid="customer-review-primary-action"]') ->waitForText('Create next review?') ->assertSee('Create next review?') ->click('button[wire\\:target="callMountedAction"]') ->waitForText('Refresh review') ->assertSee('Refresh review') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $workspacePage->screenshot(true, spec351BrowserScreenshotName('01-published-blocked')); spec351CopyBrowserScreenshot('01-published-blocked'); $returnWorkspacePage = visit(CustomerReviewWorkspace::environmentFilterUrl($publishedBlockedEnvironment)) ->waitForText('Draft review exists') ->assertSee('Open draft review') ->assertDontSee('No released customer reviews match the active environment filter.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $returnWorkspacePage->screenshot(true, spec351BrowserScreenshotName('01b-workspace-successor')); spec351CopyBrowserScreenshot('01b-workspace-successor'); $mutableBlockedPage = visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $mutableBlockedReview], $mutableBlockedEnvironment)) ->waitForText('Refresh review') ->click('Refresh review') ->waitForText('Refresh review') ->assertSee('Refresh this environment review from the latest eligible evidence basis.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $mutableBlockedPage->screenshot(true, spec351BrowserScreenshotName('02-mutable-blocked')); spec351CopyBrowserScreenshot('02-mutable-blocked'); $detailPage = visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $readyReview], $readyDraftEnvironment)) ->waitForText('Publish review') ->click('Publish review') ->waitForText('Publication reason') ->assertSee('Publication reason') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $detailPage->screenshot(true, spec351BrowserScreenshotName('03-ready-draft')); spec351CopyBrowserScreenshot('03-ready-draft'); spec351AuthenticateBrowser($this, $readonlyUser, $fallbackEnvironment); $fallbackPage = visit(CustomerReviewWorkspace::environmentFilterUrl($fallbackEnvironment)) ->waitForText('Output not customer-ready') ->assertSee('Inspect review blockers') ->assertDontSee('Create next review') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $fallbackPage->screenshot(true, spec351BrowserScreenshotName('04-fallback')); spec351CopyBrowserScreenshot('04-fallback'); expect($publishedBlockedReview)->toBeInstanceOf(EnvironmentReview::class) ->and($mutableBlockedReview)->toBeInstanceOf(EnvironmentReview::class) ->and($readyReview)->toBeInstanceOf(EnvironmentReview::class); }); function spec351BrowserScreenshotName(string $name): string { return 'spec351-review-output-resolve-actions-'.$name; } function spec351CopyBrowserScreenshot(string $name): void { $filename = spec351BrowserScreenshotName($name).'.png'; $primarySource = base_path('tests/Browser/Screenshots/'.$filename); $fallbackSource = \Pest\Browser\Support\Screenshot::path($filename); $targetDirectory = repo_path('specs/351-review-output-resolve-actions-v1/artifacts/screenshots'); if (! is_dir($targetDirectory)) { @mkdir($targetDirectory, 0755, true); } $source = null; for ($attempt = 0; $attempt < 50 && $source === null; $attempt++) { foreach ([$primarySource, $fallbackSource] as $candidate) { if (is_file($candidate)) { $source = $candidate; break; } } if ($source !== null) { break; } usleep(100_000); clearstatcache(true, $primarySource); clearstatcache(true, $fallbackSource); } if (is_string($source) && is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) { @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png'); } } function spec351AuthenticateBrowser(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(), ]); setAdminPanelContext($environment); } function spec351BrowserEnvironmentFor(User $user, ManagedEnvironment $baseEnvironment, string $name): ManagedEnvironment { $environment = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $baseEnvironment->workspace_id, 'name' => $name, ]); createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager'); return $environment; } /** * @param array $summaryOverrides * @param array $packOptions * @return array{0: EnvironmentReview, 1: ReviewPack} */ function spec351BrowserCreatePublishedReviewWithPack( ManagedEnvironment $environment, User $user, EvidenceSnapshot $snapshot, array $summaryOverrides = [], array $packOptions = [], string $filePath = 'review-packs/spec351-browser-review-pack.zip', ): array { $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); $summary = array_replace_recursive( is_array($review->summary) ? $review->summary : [], [ 'control_interpretation' => [ 'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY, 'controls' => [ [ 'control_key' => 'customer-output', 'title' => 'Customer output', 'readiness_bucket' => 'review_recommended', 'readiness_label' => 'Review recommended', 'primary_reason' => 'Evidence basis needs review.', 'recommended_next_action' => 'Create the next review cycle before customer sharing.', ], ], ], ], $summaryOverrides, ); Storage::disk('exports')->put($filePath, 'PK-spec351-browser-test'); $review->forceFill([ 'status' => EnvironmentReviewStatus::Published->value, 'completeness_state' => (string) $review->completeness_state, 'summary' => $summary, 'generated_at' => now()->subMinutes(5), 'published_at' => now()->subMinutes(3), 'published_by_user_id' => (int) $user->getKey(), ])->save(); $pack = 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(), 'options' => array_replace([ 'include_pii' => false, 'include_operations' => true, ], $packOptions), 'file_path' => $filePath, 'file_disk' => 'exports', 'generated_at' => now()->subMinutes(4), ]); $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); return [$review->fresh(['tenant', 'evidenceSnapshot', 'currentExportReviewPack.operationRun', 'operationRun']), $pack]; }