browser()->timeout(60_000); beforeEach(function (): void { Storage::fake('exports'); }); it('Spec387 smokes the decision-first publication resolution flow', function (): void { [$user, $environment, $review] = spec387BrowserBlockedReviewFixture(); spec387AuthenticateBrowser($this, $user, $environment); $reviewDetailPage = visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $environment)) ->resize(1236, 900) ->waitForText('Resolve publication blockers') ->assertSee('Resolve publication blockers') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $reviewDetailPage->screenshot(true, spec387BrowserScreenshotName('01-review-detail-blocked-cta')); spec387CopyBrowserScreenshot('01-review-detail-blocked-cta'); $resolutionPage = $reviewDetailPage ->click('Resolve publication blockers') ->waitForText('Review can\'t be published yet') ->assertSee('Publication preparation') ->assertSee('Why publication is blocked') ->assertSee('Next safe action') ->assertSee('Update required reports') ->assertSee('Prepare export') ->assertSee('Return to review') ->assertSee('TenantPilot will update the missing required reports. It will not publish the review.') ->assertSee('Technical proof and operation history') ->assertDontSee('Generate review pack') ->assertDontSee('Return to publication') ->assertDontSee('Report-backed evidence') ->assertDontSee('OperationRun') ->assertDontSee('Artifact proof') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $resolutionPage->screenshot(true, spec387BrowserScreenshotName('02-resolution-decision-desktop')); spec387CopyBrowserScreenshot('02-resolution-decision-desktop'); $modalPage = $resolutionPage ->click('Update required reports') ->waitForText('Update required reports?') ->assertSee('TenantPilot will update the missing required reports. This will not publish the review.') ->assertSee('Update required reports') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $modalPage->screenshot(true, spec387BrowserScreenshotName('03-confirmation-modal')); spec387CopyBrowserScreenshot('03-confirmation-modal'); $expandedProofPage = $modalPage ->click('Cancel') ->click('Technical proof and operation history') ->waitForText('Proof and operation links are supporting evidence only.') ->assertSee('Proof and operation links are supporting evidence only.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $expandedProofPage->screenshot(true, spec387BrowserScreenshotName('04-technical-proof-expanded')); spec387CopyBrowserScreenshot('04-technical-proof-expanded'); $mobilePage = $expandedProofPage ->resize(390, 844) ->waitForText('Publication preparation') ->assertSee('Review can\'t be published yet') ->assertSee('Update required reports') ->assertSee('Return to review') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $mobilePage->screenshot(true, spec387BrowserScreenshotName('05-resolution-decision-mobile')); spec387CopyBrowserScreenshot('05-resolution-decision-mobile'); $customerWorkspace = visit(CustomerReviewWorkspace::environmentFilterUrl($environment)) ->resize(1236, 900) ->waitForText('Customer Review Workspace') ->assertDontSee('Resolution Case') ->assertDontSee('Current step') ->assertDontSee('OperationRun') ->assertDontSee('Artifact proof') ->assertDontSee('complete_required_reports') ->assertDontSee('generate_review_pack') ->assertDontSee('return_to_publication') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $customerWorkspace->screenshot(true, spec387BrowserScreenshotName('06-customer-no-leakage')); spec387CopyBrowserScreenshot('06-customer-no-leakage'); }); it('Spec387 smokes readonly inspection copy on the resolution page', function (): void { [, $environment, $review] = spec387BrowserBlockedReviewFixture(); [$readonly] = createUserWithTenant( tenant: $environment, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly', ); spec387AuthenticateBrowser($this, $readonly, $environment); $page = visit(EnvironmentReviewResource::environmentScopedUrl('resolve-publication', ['record' => $review], $environment)) ->resize(1236, 900) ->waitForText('You can inspect this preparation flow, but you do not have permission to run the next action.') ->assertSee('Review can\'t be published yet') ->assertSee('You can inspect this preparation flow, but you do not have permission to run the next action.') ->assertSee('Update required reports') ->assertDontSee('Generate review pack') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $page->screenshot(true, spec387BrowserScreenshotName('07-readonly-inspection')); spec387CopyBrowserScreenshot('07-readonly-inspection'); }); /** * @return array{0: User, 1: ManagedEnvironment, 2: EnvironmentReview} */ function spec387BrowserBlockedReviewFixture(): array { $environment = ManagedEnvironment::factory()->create(['name' => 'Spec387 Browser Resolution']); [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager'); $snapshot = spec387BrowserPartialEvidence($environment); $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); $review->forceFill([ 'status' => EnvironmentReviewStatus::Draft->value, 'published_at' => null, 'published_by_user_id' => null, ])->save(); app(ReviewPublicationResolutionService::class)->openOrResume($review, $user); return [$user, $environment, $review]; } function spec387BrowserPartialEvidence(ManagedEnvironment $environment): EvidenceSnapshot { $snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0); $snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([ 'state' => EvidenceCompletenessState::Missing->value, 'source_record_id' => null, 'source_fingerprint' => null, ]); spec387BrowserDeleteStoredReport($environment, StoredReport::REPORT_TYPE_PERMISSION_POSTURE); spec387BrowserDeleteStoredReport($environment, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES); return $snapshot->fresh('items'); } function spec387BrowserDeleteStoredReport(ManagedEnvironment $environment, string $reportType): void { StoredReport::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->where('report_type', $reportType) ->delete(); } function spec387BrowserScreenshotName(string $name): string { return 'spec387-review-publication-resolution-'.$name; } function spec387CopyBrowserScreenshot(string $name): void { $filename = spec387BrowserScreenshotName($name).'.png'; $primarySource = base_path('tests/Browser/Screenshots/'.$filename); $fallbackSource = \Pest\Browser\Support\Screenshot::path($filename); $targetDirectory = repo_path('specs/387-review-publication-resolution-decision-ux-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 spec387AuthenticateBrowser(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); }