browser()->timeout(60_000); beforeEach(function (): void { Storage::fake('exports'); }); it('smokes the customer review workspace handoff from environment review detail', function (): void { $tenantPublished = ManagedEnvironment::factory()->create(['name' => 'Published ManagedEnvironment']); [$user, $tenantPublished] = createUserWithTenant( tenant: $tenantPublished, role: 'owner', workspaceRole: 'manager', ); $user->forceFill(['preferred_locale' => 'de'])->save(); $tenantWithoutPublished = ManagedEnvironment::factory()->create([ 'workspace_id' => (int) $tenantPublished->workspace_id, 'name' => 'No Published ManagedEnvironment', ]); createUserWithTenant( tenant: $tenantWithoutPublished, user: $user, role: 'owner', workspaceRole: 'manager', ); $publishedSnapshot = seedEnvironmentReviewEvidence($tenantPublished, findingCount: 0, driftCount: 0); $noPublishedSnapshot = seedEnvironmentReviewEvidence($tenantWithoutPublished, findingCount: 0, driftCount: 0); $publishedReview = composeEnvironmentReviewForTest($tenantPublished, $user, $publishedSnapshot); $publishedSummary = array_replace_recursive(is_array($publishedReview->summary) ? $publishedReview->summary : [], [ 'control_interpretation' => [ 'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY, 'controls' => [ [ 'control_key' => 'customer-handoff-readiness', 'title' => 'Customer handoff readiness', 'readiness_bucket' => 'evidence_on_record', 'readiness_label' => 'Evidence on record', 'primary_reason' => 'Evidence path is complete.', 'recommended_next_action' => 'Share the current review pack.', ], ], ], 'governance_package' => [ 'decision_summary' => [ 'status' => 'none', 'evidence_state' => EnvironmentReviewCompletenessState::Complete->value, 'decision_data_state' => 'complete', 'total_count' => 0, 'summary' => '', 'next_action' => '', 'entries' => [], ], ], ]); $publishedReview->forceFill([ 'status' => EnvironmentReviewStatus::Published->value, 'summary' => $publishedSummary, 'published_at' => now(), 'published_by_user_id' => (int) $user->getKey(), ])->save(); $publishedReview = markEnvironmentReviewCustomerSafeReady($publishedReview); $internalOnlyReview = composeEnvironmentReviewForTest($tenantWithoutPublished, $user, $noPublishedSnapshot); $internalOnlyReview->forceFill([ 'status' => EnvironmentReviewStatus::Ready->value, 'published_at' => null, 'published_by_user_id' => null, ])->save(); Storage::disk('exports')->put('review-packs/customer-review-workspace-smoke.zip', 'PK-test'); $pack = ReviewPack::factory()->ready()->create([ 'managed_environment_id' => (int) $tenantPublished->getKey(), 'workspace_id' => (int) $tenantPublished->workspace_id, 'environment_review_id' => (int) $publishedReview->getKey(), 'evidence_snapshot_id' => (int) $publishedSnapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'options' => [ 'include_pii' => false, 'include_operations' => true, ], 'summary' => [ 'governance_package' => [ 'executive_summary' => 'Customer steering summary for the rendered browser smoke report.', 'evidence_basis_summary' => 'Evidence basis is complete for the published smoke review.', 'top_findings' => [ [ 'title' => 'Conditional access drift requires review', 'summary' => 'An inherited drift signal should stay visible in the rendered report.', ], ], 'accepted_risks' => [ [ 'title' => 'Temporary exception remains accepted', 'customer_safe_summary' => 'The accepted risk stays visible as stakeholder context.', ], ], 'decision_summary' => [ 'status' => 'attention_required', 'summary' => 'A governance decision still requires stakeholder awareness.', 'next_action' => 'Review the accepted exception before external sharing.', 'entries' => [ [ 'title' => 'Conditional access exception', 'summary' => 'Temporary risk is accepted until remediation finishes.', 'next_action' => 'Confirm the remediation timeline before customer handoff.', ], ], ], ], 'control_interpretation' => [ 'non_certification_disclosure' => 'Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.', ], 'recommended_next_actions' => [ 'Share the rendered report with the customer steering group.', ], 'delivery_bundle' => [ 'executive_entrypoint_file' => 'executive-summary.md', 'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'], ], ], 'file_path' => 'review-packs/customer-review-workspace-smoke.zip', 'file_disk' => 'exports', ]); $publishedReview->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); $limitedEnvironment = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenantPublished->workspace_id, 'name' => 'Limited Report ManagedEnvironment', ]); createUserWithTenant(tenant: $limitedEnvironment, user: $user, role: 'owner', workspaceRole: 'manager'); [$limitedReview, $limitedPack] = spec356BrowserCreateRenderedReportPack( environment: $limitedEnvironment, user: $user, snapshot: seedEnvironmentReviewEvidence($limitedEnvironment, findingCount: 0, driftCount: 0), filePath: 'review-packs/spec356-browser-limited.zip', evidenceOverride: EvidenceCompletenessState::Partial, ); $internalEnvironment = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenantPublished->workspace_id, 'name' => 'Internal PII Report ManagedEnvironment', ]); createUserWithTenant(tenant: $internalEnvironment, user: $user, role: 'owner', workspaceRole: 'manager'); [$internalReview, $internalPack] = spec356BrowserCreateRenderedReportPack( environment: $internalEnvironment, user: $user, snapshot: seedEnvironmentReviewEvidence($internalEnvironment, findingCount: 0, driftCount: 0), packOptions: [ 'include_pii' => true, 'include_operations' => true, ], filePath: 'review-packs/spec356-browser-internal-pii.zip', ); $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantPublished->workspace_id, WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantPublished->workspace_id => (int) $tenantPublished->getKey(), ], ]); $environmentReviewPage = visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview], $tenantPublished)) ->waitForText('Verwandter Kontext') ->assertSee('Kunden-Workspace öffnen') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $environmentReviewPage ->assertScript('document.querySelector(\'a.fi-link[href*="/admin/reviews/workspace?environment_id="]\') instanceof HTMLAnchorElement', true); $workspacePage = visit(CustomerReviewWorkspace::environmentFilterUrl($tenantPublished)) ->waitForText('Kundensichere Review-Pakete') ->assertSee('Filter löschen') ->assertSee('Review öffnen') ->assertSee('Governance-Paket') ->assertSee('Status') ->assertSee('Nachweise') ->assertSee('Prüfen Sie veröffentlichte Governance-Pakete, Evidence-Bereitschaft, akzeptierte Risiken und Übergabestatus über berechtigte Umgebungen hinweg.') ->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.') ->assertSee('Letztes veröffentlichtes Review') ->assertSee('Review-Paket-Index') ->assertSee('Offenlegungsregel') ->assertSee('Eingeklappt') ->assertSee('Kundensicheres Review-Paket herunterladen') ->assertSee('Das aktuelle Review-Paket ist verfügbar, erfüllt den kundensicheren Output-Vertrag und kann aus dem Review-Detail als gerenderter Bericht geöffnet werden.') ->assertSee('In diesem veröffentlichten Review benötigen keine Governance-Entscheidungen Kundenaufmerksamkeit.') ->assertSee('Kundensicheres Review-Paket bereit') ->assertSee('Verfügbar') ->assertDontSee('Customer-safe governance package index') ->assertDontSee('localization.review.customer_safe_review_workspace') ->assertDontSee('Publishable') ->assertDontSee('No mapped controls') ->assertDontSee('Compliance evidence mapping v1') ->assertDontSee('Publish review') ->assertDontSee('Refresh review'); $workspacePage ->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true); visit(CustomerReviewWorkspace::getUrl(panel: 'admin')) ->waitForText('Published ManagedEnvironment') ->assertDontSee('No Published ManagedEnvironment') ->assertDontSee('No published review available yet') ->assertSeeIn('tbody tr.fi-ta-row:first-of-type td:last-child', 'Review öffnen'); $customerContextReviewUrl = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview], $tenantPublished) .'?'.http_build_query([ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (int) $tenantPublished->getKey(), ]); $customerReviewDetailPage = visit($customerContextReviewUrl) ->waitForText('Ergebniszusammenfassung') ->assertSee('Kundensicheren Bericht anzeigen') ->assertSee('Governance-Paket') ->assertSee('Veröffentlichter Governance-Nachweis') ->assertSee('Review-Status') ->assertSee('Primäre Aktion') ->assertSee('Executive-Einstieg') ->assertSee('Strukturierter Auditor-Anhang') ->assertSee('Prüfgrundlage') ->assertDontSee('Released governance record') ->assertDontSee('Control readiness interpretation') ->assertDontSee('Compliance evidence mapping v1') ->assertDontSee('Publish review') ->assertDontSee('Refresh review') ->assertDontSee('Create next review') ->assertDontSee('Export executive pack') ->assertDontSee('Archive review') ->assertScript('document.querySelector(\'a[href*="/admin/review-packs/"][href*="/report"]\') instanceof HTMLAnchorElement', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $renderedReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (int) $tenantPublished->getKey(), 'review_id' => (int) $publishedReview->getKey(), 'interpretation_version' => $publishedReview->controlInterpretationVersion(), ]); $renderedReportPage = visit($renderedReportUrl) ->resize(1280, 1440) ->waitForText('Kundensicherer Bericht bereit') ->assertPathContains('/admin/review-packs/') ->assertPathEndsWith('/report') ->assertSee('Evidence-Basis') ->assertSee('Bericht drucken') ->assertSee('Review-Detail öffnen') ->assertSee('Review-Pack-Detail öffnen') ->assertSee('Conditional access drift requires review') ->assertSee('A governance decision still requires stakeholder awareness.') ->assertSee('Unterstützender Anhang') ->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.') ->assertDontSee('localization.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $renderedReportPage->screenshot(true, spec356BrowserScreenshotName('report-customer-safe-ready')); spec356CopyBrowserScreenshot('report-customer-safe-ready'); $renderedReportPage->script('document.body.classList.add("print-preview-smoke"); window.scrollTo(0, 0);'); $renderedReportPage ->assertScript('window.getComputedStyle(document.querySelector("[data-testid=\"rendered-report-toolbar\"]")).display === "none"', true); $renderedReportPage->screenshot(true, spec356BrowserScreenshotName('report-print-view')); spec356CopyBrowserScreenshot('report-print-view'); $renderedReportPage->script('document.body.classList.remove("print-preview-smoke"); document.querySelector("[data-testid=\"rendered-report-supporting-appendix\"]")?.scrollIntoView({ block: "start" });'); $renderedReportPage->screenshot(false, spec356BrowserScreenshotName('report-appendix')); spec356CopyBrowserScreenshot('report-appendix'); $limitedReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($limitedPack, [ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (int) $limitedEnvironment->getKey(), 'review_id' => (int) $limitedReview->getKey(), 'interpretation_version' => $limitedReview->controlInterpretationVersion(), ]); $limitedReportPage = visit($limitedReportUrl) ->resize(1280, 1440) ->waitForText('Bericht mit Einschränkungen') ->assertSee('Nicht extern weitergeben, bevor der Bericht geprüft wurde.') ->assertSee('Output-Einschränkungen') ->assertSee('Die Evidence-Basis ist unvollständig') ->assertDontSee('Kundensicherer Bericht bereit') ->assertDontSee('localization.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $limitedReportPage->screenshot(true, spec356BrowserScreenshotName('report-with-limitations')); spec356CopyBrowserScreenshot('report-with-limitations'); $internalReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($internalPack, [ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (int) $internalEnvironment->getKey(), 'review_id' => (int) $internalReview->getKey(), 'interpretation_version' => $internalReview->controlInterpretationVersion(), ]); $internalReportPage = visit($internalReportUrl) ->resize(1280, 1440) ->waitForText('Interner Bericht mit Einschränkungen') ->assertSee('Nicht extern weitergeben, bevor der Bericht geprüft wurde.') ->assertSee('interne oder PII-tragende Details') ->assertSee('Internes Review-Paket herunterladen') ->assertDontSee('Kundensicherer Bericht bereit') ->assertDontSee('localization.') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $internalReportPage->screenshot(true, spec356BrowserScreenshotName('report-internal-pii')); spec356CopyBrowserScreenshot('report-internal-pii'); }); /** * @param array $packOptions * @return array{0:\App\Models\EnvironmentReview,1:ReviewPack} */ function spec356BrowserCreateRenderedReportPack( ManagedEnvironment $environment, \App\Models\User $user, EvidenceSnapshot $snapshot, array $packOptions = [], string $filePath = 'review-packs/spec356-browser-report.zip', ?EvidenceCompletenessState $evidenceOverride = null, ): array { $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); $review->forceFill([ 'status' => EnvironmentReviewStatus::Published->value, 'published_at' => now()->subMinutes(5), 'published_by_user_id' => (int) $user->getKey(), ])->save(); $review = markEnvironmentReviewCustomerSafeReady($review); if ($evidenceOverride instanceof EvidenceCompletenessState) { restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, $evidenceOverride); $review = $review->fresh(['sections', 'evidenceSnapshot']); } Storage::disk('exports')->put($filePath, 'PK-spec356-browser-test'); $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), 'summary' => [ 'governance_package' => [ 'executive_summary' => 'Management summary for the Spec 356 rendered browser report.', 'evidence_basis_summary' => 'The report is anchored to the stored review evidence basis.', 'top_findings' => [], 'accepted_risks' => [], 'decision_summary' => [ 'status' => 'none', 'summary' => '', 'next_action' => '', 'entries' => [], ], ], 'control_interpretation' => [ 'non_certification_disclosure' => 'Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.', ], 'recommended_next_actions' => [], 'delivery_bundle' => [ 'executive_entrypoint_file' => 'executive-summary.md', 'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'], ], ], 'file_path' => $filePath, 'file_disk' => 'exports', 'generated_at' => now()->subMinutes(4), ]); $review->forceFill([ 'current_export_review_pack_id' => (int) $pack->getKey(), ])->save(); return [$review->refresh(), $pack->refresh()]; } function spec356BrowserScreenshotName(string $name): string { return 'spec356-review-pack-rendered-report-'.$name; } function spec356CopyBrowserScreenshot(string $name): void { $filename = spec356BrowserScreenshotName($name).'.png'; $source = base_path('tests/Browser/Screenshots/'.$filename); $targetDirectory = repo_path('specs/356-review-pack-pdf-html-renderer-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.$name.'.png'); } }