browser()->timeout(60_000); uses(RefreshDatabase::class); function spec333BrowserScreenshotName(string $name): string { return 'spec333-restore-create-'.$name; } function spec333CopyBrowserScreenshot(string $name): void { $filename = spec333BrowserScreenshotName($name).'.png'; $source = base_path('tests/Browser/Screenshots/'.$filename); $targetDirectory = repo_path('specs/333-restore-create-ux-final-productization/artifacts/screenshots'); if (! is_dir($targetDirectory)) { @mkdir($targetDirectory, 0755, true); } if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) { return; } 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)) { @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png'); } } function spec333BrowserLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string { return route('admin.local.smoke-login', array_filter([ 'email' => $user->email, 'tenant' => $tenant->external_id, 'workspace' => $tenant->workspace->slug, 'redirect' => $redirect, ], static fn (?string $value): bool => filled($value))); } function spec333BrowserTenant(bool $credentialAvailable = true): array { $tenant = ManagedEnvironment::factory()->create([ 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: $credentialAvailable); bindFailHardGraphClient(); return [$user, $tenant]; } function spec333BrowserRedirect(ManagedEnvironment $tenant): string { $redirectBase = RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant); return parse_url($redirectBase, PHP_URL_PATH) ?: '/admin'; } function spec333BrowserSelectBackupSet($page, BackupSet $backupSet): void { $selected = $page->script(<< { const select = document.getElementById('form.backup_set_id'); if (! select) { return false; } select.value = '{$backupSet->getKey()}'; select.dispatchEvent(new Event('input', { bubbles: true })); select.dispatchEvent(new Event('change', { bubbles: true })); return true; })() JS); expect($selected)->toBeTrue(); } function spec333BrowserWizardNext($page): void { $clicked = $page->script(<<<'JS' (() => { const footer = document.querySelector('.fi-sc-wizard-footer'); if (! footer) { return false; } const nextTrigger = footer.querySelector('div[x-on\\:click*="requestNextStep"]'); if (! nextTrigger || nextTrigger.classList.contains('fi-hidden')) { return false; } nextTrigger.scrollIntoView({ block: 'center' }); nextTrigger.click(); return true; })() JS); expect($clicked)->toBeTrue(); } function spec333BrowserUsableBackupFixture(ManagedEnvironment $tenant): BackupSet { $policy = Policy::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'external_id' => 'spec333-browser-policy-usable', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Spec333 Browser Policy', 'platform' => 'windows', ]); PolicyVersion::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'policy_id' => (int) $policy->getKey(), 'version_number' => 1, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'captured_at' => now(), 'snapshot' => [ 'foo' => 'current', ], 'metadata' => [], 'assignments' => [], 'scope_tags' => [], ]); $backupSet = BackupSet::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'name' => 'Spec333 Browser Usable Backup', 'status' => 'completed', 'item_count' => 1, ]); BackupItem::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), 'policy_id' => (int) $policy->getKey(), 'policy_identifier' => $policy->external_id, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'captured_at' => now(), 'payload' => [ 'foo' => 'backup', 'displayName' => 'Spec333 Browser Policy', ], 'assignments' => [], 'metadata' => [ 'displayName' => 'Spec333 Browser Policy', ], ]); return $backupSet; } function spec333BrowserGroupBackupFixture(ManagedEnvironment $tenant): BackupSet { $policy = Policy::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'external_id' => 'spec333-browser-policy-group', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Spec333 Group Mapping Policy', 'platform' => 'windows', ]); $backupSet = BackupSet::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'name' => 'Spec333 Browser Group Backup', 'status' => 'completed', 'item_count' => 1, ]); BackupItem::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), 'policy_id' => (int) $policy->getKey(), 'policy_identifier' => $policy->external_id, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'captured_at' => now(), 'payload' => [ 'foo' => 'backup', 'displayName' => 'Spec333 Group Mapping Policy', ], 'assignments' => [[ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => '11111111-1111-1111-1111-111111111111', 'group_display_name' => 'Spec333 Missing Group', ], ]], 'metadata' => [ 'displayName' => 'Spec333 Group Mapping Policy', ], ]); return $backupSet; } function spec333BrowserOpenGroupPicker($page): void { $opened = $page->script(<<<'JS' (() => { const header = Array.from(document.querySelectorAll('.fi-section-header')).find((element) => element.textContent?.includes('Resolve target mappings') ); header?.click(); const pickerButton = document.querySelector('button[wire\\:click*="select_from_directory_cache_11111111_1111_1111_1111_111111111111"]'); pickerButton?.click(); return Boolean(pickerButton); })() JS); expect($opened)->toBeTrue(); } function spec333BrowserOpenMappingResolver($page): void { $opened = $page->script(<<<'JS' (() => { const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]'); const content = section?.querySelector('.fi-section-content-ctn'); if (content?.getAttribute('aria-expanded') !== 'true') { section?.querySelector('.fi-section-header')?.click(); } return Boolean(section); })() JS); expect($opened)->toBeTrue(); } function spec333BrowserWizardHeaderMetrics($page): array { return $page->script(<<<'JS' (() => { const header = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header'); const steps = Array.from(document.querySelectorAll('.restore-run-create-wizard .fi-sc-wizard-header-step')); const buttons = Array.from(document.querySelectorAll('.restore-run-create-wizard .fi-sc-wizard-header-step-btn')); const activeButton = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header-step.fi-active .fi-sc-wizard-header-step-btn'); const inactiveButton = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header-step:not(.fi-active) .fi-sc-wizard-header-step-btn'); const headerStyles = header ? window.getComputedStyle(header) : null; const firstButtonStyles = buttons[0] ? window.getComputedStyle(buttons[0]) : null; const activeButtonStyles = activeButton ? window.getComputedStyle(activeButton) : null; const inactiveButtonStyles = inactiveButton ? window.getComputedStyle(inactiveButton) : null; const stepWidths = steps.map((step) => Math.round(step.getBoundingClientRect().width)); const buttonHeights = buttons.map((button) => Math.round(button.getBoundingClientRect().height)); const documentStyles = window.getComputedStyle(document.documentElement); return { display: headerStyles?.display ?? null, overflowX: headerStyles?.overflowX ?? null, primary400: documentStyles.getPropertyValue('--primary-400').trim(), primary500: documentStyles.getPropertyValue('--primary-500').trim(), headerBackground: headerStyles?.backgroundColor ?? null, firstButtonBackground: firstButtonStyles?.backgroundColor ?? null, firstButtonBorderColor: firstButtonStyles?.borderColor ?? null, inactiveButtonBorderColor: inactiveButtonStyles?.borderColor ?? null, activeButtonBorderColor: activeButtonStyles?.borderColor ?? null, activeButtonShadow: activeButtonStyles?.boxShadow ?? null, stepCount: steps.length, pageOverflows: document.documentElement.scrollWidth > document.documentElement.clientWidth, headerScrollsInternally: Boolean(header && header.scrollWidth > header.clientWidth), smallestStepWidth: Math.min(...stepWidths), tallestButtonHeight: Math.max(...buttonHeights), }; })() JS); } it('keeps the restore create wizard header compact at enterprise browser widths', function (): void { [$user, $tenant] = spec333BrowserTenant(); $backupSet = spec333BrowserUsableBackupFixture($tenant); $page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant))); $page->resize(900, 1100) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSet); $page->waitForText('A usable source backup is selected.') ->assertSee('Backup quality summary') ->assertSee('Available') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $metrics = spec333BrowserWizardHeaderMetrics($page); expect($metrics) ->display->toBe('flex') ->overflowX->toBe('auto') ->stepCount->toBe(5) ->pageOverflows->toBeFalse() ->headerScrollsInternally->toBeTrue() ->smallestStepWidth->toBeGreaterThanOrEqual(220) ->tallestButtonHeight->toBeLessThanOrEqual(120); expect($metrics['headerBackground'])->not->toBe($metrics['firstButtonBackground']); expect($metrics['primary500'])->not->toBeEmpty(); expect($metrics['activeButtonBorderColor'])->toBe($metrics['primary500']); expect($metrics['activeButtonBorderColor'])->not->toBe($metrics['inactiveButtonBorderColor']); expect($metrics['activeButtonShadow'])->not->toBe('none'); $page->script("document.documentElement.classList.add('dark');"); $darkMetrics = spec333BrowserWizardHeaderMetrics($page); expect($darkMetrics['primary400'])->not->toBeEmpty(); expect($darkMetrics) ->headerBackground->toBe('rgb(17, 24, 39)') ->firstButtonBackground->toBe('rgb(31, 41, 55)') ->activeButtonBorderColor->toBe($darkMetrics['primary400']) ->pageOverflows->toBeFalse() ->smallestStepWidth->toBeGreaterThanOrEqual(220) ->tallestButtonHeight->toBeLessThanOrEqual(120); }); it('captures step 1 and step 2 restore create states', function (): void { [$user, $tenant] = spec333BrowserTenant(); $backupSet = spec333BrowserGroupBackupFixture($tenant); $page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSet); $page->waitForText('Source selected') ->assertSee('Continue to scope and resolve required mappings.') ->assertSee('Safety evidence') ->assertSee('View safety gates and proof') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $stepOneDensity = $page->script(<<<'JS' (() => { const evidence = document.querySelector('[data-testid="restore-run-safety-evidence"]'); const evidenceDetails = evidence?.querySelector('details'); const qualityDetails = document.querySelector('[data-testid="restore-run-backup-quality-summary"] details'); return { evidenceDetailsOpen: Boolean(evidenceDetails?.open), qualityDetailsOpen: Boolean(qualityDetails?.open), evidenceHeight: Math.round(evidence?.getBoundingClientRect().height ?? 0), qualityHeight: Math.round(document.querySelector('[data-testid="restore-run-backup-quality-summary"]')?.getBoundingClientRect().height ?? 0), }; })() JS); expect($stepOneDensity) ->evidenceDetailsOpen->toBeFalse() ->qualityDetailsOpen->toBeFalse() ->evidenceHeight->toBeLessThanOrEqual(180) ->qualityHeight->toBeLessThanOrEqual(260); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('01-step-1-backup-selected')); spec333CopyBrowserScreenshot('01-step-1-backup-selected'); spec333BrowserWizardNext($page); $page->waitForText('Scope summary') ->assertSee('1 mapping required') ->assertSee('Validation blocked') ->assertSee('Resolve target mappings') ->assertSee('Restore evidence') ->assertDontSee('Resolve mappings'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('02-step-2-scope-default')); spec333CopyBrowserScreenshot('02-step-2-scope-default'); spec333BrowserOpenMappingResolver($page); $page->waitForText('Hide mapping details'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('03-step-2-resolver-expanded')); spec333CopyBrowserScreenshot('03-step-2-resolver-expanded'); }); it('captures group picker results and empty states', function (): void { [$userWithCache, $tenantWithCache] = spec333BrowserTenant(); $backupSetWithCache = spec333BrowserGroupBackupFixture($tenantWithCache); EntraGroup::factory()->for($tenantWithCache)->create([ 'entra_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'display_name' => 'Spec333 Cached Target Group', 'last_seen_at' => now(), ]); $page = visit(spec333BrowserLoginUrl($userWithCache, $tenantWithCache, spec333BrowserRedirect($tenantWithCache))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSetWithCache); $page->waitForText('Continue to scope and resolve required mappings.'); spec333BrowserWizardNext($page); $page->waitForText('Resolve target mappings'); spec333BrowserOpenGroupPicker($page); $page->waitForText('Resolve target group mapping') ->assertSee('Spec333 Cached Target Group') ->assertDontSee('No directory group cache available'); $page->screenshot(true, spec333BrowserScreenshotName('04-step-2-group-picker-results')); spec333CopyBrowserScreenshot('04-step-2-group-picker-results'); [$userWithoutCache, $tenantWithoutCache] = spec333BrowserTenant(); $backupSetWithoutCache = spec333BrowserGroupBackupFixture($tenantWithoutCache); $page = visit(spec333BrowserLoginUrl($userWithoutCache, $tenantWithoutCache, spec333BrowserRedirect($tenantWithoutCache))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSetWithoutCache); $page->waitForText('Continue to scope and resolve required mappings.'); spec333BrowserWizardNext($page); $page->waitForText('Resolve target mappings'); spec333BrowserOpenGroupPicker($page); $page->waitForText('No directory group cache available') ->assertSee('Open group sync') ->assertSee('View group sync operations'); $page->screenshot(true, spec333BrowserScreenshotName('05-step-2-group-picker-empty')); spec333CopyBrowserScreenshot('05-step-2-group-picker-empty'); }); it('captures blocked and passed validation states', function (): void { [$blockedUser, $blockedTenant] = spec333BrowserTenant(credentialAvailable: false); $blockedBackupSet = spec333BrowserUsableBackupFixture($blockedTenant); $page = visit(spec333BrowserLoginUrl($blockedUser, $blockedTenant, spec333BrowserRedirect($blockedTenant))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $blockedBackupSet); $page->waitForText('Source selected'); spec333BrowserWizardNext($page); $page->waitForText('Scope summary'); spec333BrowserWizardNext($page); $page->waitForText('Validation blocked') ->assertSee('Validation decision') ->assertSee('Provider access must be repaired before restore checks can run.') ->assertSee('Provider credentials are not available for this environment.') ->assertSee('Review provider connection') ->assertSee('Repair the provider connection before validation can run.') ->assertDontSee('Run checks'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('06-step-3-validation-blocked')); spec333CopyBrowserScreenshot('06-step-3-validation-blocked'); [$passedUser, $passedTenant] = spec333BrowserTenant(); $passedBackupSet = spec333BrowserUsableBackupFixture($passedTenant); $page = visit(spec333BrowserLoginUrl($passedUser, $passedTenant, spec333BrowserRedirect($passedTenant))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $passedBackupSet); $page->waitForText('Source selected'); spec333BrowserWizardNext($page); $page->waitForText('Scope summary'); spec333BrowserWizardNext($page); $page->waitForText('Run checks') ->click('Run checks') ->waitForText('Validation passed') ->assertSee('View 7 safe check details') ->assertSee('Prerequisites available') ->assertSee('Generate a preview for the current scope before confirmation.') ->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.') ->assertSee('Validation evidence') ->assertSee('View validation gates and restore proof') ->assertDontSee('Run checks after defining scope and mapping missing groups.') ->assertDontSee('Safety checks completed'); $page->assertScript('document.querySelector("[data-testid=\"restore-run-safe-checks-details\"]")?.open === false', true); $validationSurfaceMetrics = $page->script(<<<'JS' (() => { const decisionCard = document.querySelector('[data-testid="restore-run-validation-decision-card"]'); const statCard = document.querySelector('[data-testid="restore-run-validation-stat-card"]'); if (! decisionCard || ! statCard) { return null; } const decisionStyle = window.getComputedStyle(decisionCard); const statStyle = window.getComputedStyle(statCard); return { decisionBackground: decisionStyle.backgroundColor, decisionBorder: decisionStyle.borderColor, statBackground: statStyle.backgroundColor, }; })() JS); expect($validationSurfaceMetrics)->not->toBeNull(); expect($validationSurfaceMetrics['decisionBackground'])->not->toBe($validationSurfaceMetrics['statBackground']); $page->script("document.documentElement.classList.add('dark');"); $darkValidationSurfaceMetrics = $page->script(<<<'JS' (() => { const decisionCard = document.querySelector('[data-testid="restore-run-validation-decision-card"]'); const statCard = document.querySelector('[data-testid="restore-run-validation-stat-card"]'); if (! decisionCard || ! statCard) { return null; } const decisionStyle = window.getComputedStyle(decisionCard); const statStyle = window.getComputedStyle(statCard); return { decisionBackground: decisionStyle.backgroundColor, decisionBorder: decisionStyle.borderColor, statBackground: statStyle.backgroundColor, }; })() JS); expect($darkValidationSurfaceMetrics)->not->toBeNull(); expect($darkValidationSurfaceMetrics['decisionBackground'])->not->toBe($validationSurfaceMetrics['decisionBackground']); expect($darkValidationSurfaceMetrics['decisionBackground'])->not->toBe($darkValidationSurfaceMetrics['statBackground']); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('07-step-3-validation-passed-dark')); spec333CopyBrowserScreenshot('07-step-3-validation-passed-dark'); $page->script("document.documentElement.classList.remove('dark');"); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('07-step-3-validation-passed')); spec333CopyBrowserScreenshot('07-step-3-validation-passed'); }); it('captures preview and confirmation states after current evidence exists', function (): void { [$user, $tenant] = spec333BrowserTenant(); $backupSet = spec333BrowserUsableBackupFixture($tenant); $page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSet); $page->waitForText('Source selected'); spec333BrowserWizardNext($page); $page->waitForText('Scope summary'); spec333BrowserWizardNext($page); $page->waitForText('Run checks') ->click('Run checks') ->waitForText('Validation passed'); spec333BrowserWizardNext($page); $page->waitForText('Generate preview') ->click('Generate preview') ->waitForText('Preview details') ->assertSee('Review the preview and continue to confirmation.') ->assertSee('Regenerate preview') ->assertSee('Preview is current for this scope. Regenerate only after scope, mapping, or source changes.') ->assertSee('Preview evidence') ->assertSee('View safety gates and restore proof') ->assertSee('Needs attention') ->assertSee('Changes detected') ->assertSee('Restore action') ->assertSee('Update existing') ->assertSee('Policy diff') ->assertSee('Assignments') ->assertSee('Scope tags') ->assertSee('No changes detected') ->assertSee('All reviewed items') ->assertDontSee('Restore safety status') ->assertDontSee('What the preview proves') ->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence-details\"]")?.open === false', true) ->assertNoJavaScriptErrors() ->assertNoConsoleLogs(); $page->resize(680, 1100); $page->script('document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]")?.scrollIntoView({ block: "center" });'); $page ->assertScript('(() => { const cards = document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]"); const table = document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]"); return Boolean( cards && table && getComputedStyle(cards).display !== "none" && cards.getClientRects().length > 0 && getComputedStyle(table).display === "none" ); })()', true) ->assertSee('Policy diff') ->assertSee('0 added') ->assertSee('0 removed') ->assertSee('1 changed'); $page->resize(1920, 1200); $page->script('document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]")?.scrollIntoView({ block: "center" });'); $page ->assertScript('(() => { const cards = document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]"); const table = document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]"); return Boolean( cards && table && getComputedStyle(cards).display === "none" && getComputedStyle(table).display !== "none" && table.getClientRects().length > 0 ); })()', true); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('08-step-4-preview-generated')); spec333CopyBrowserScreenshot('08-step-4-preview-generated'); spec333BrowserWizardNext($page); $page->waitForText('Confirm & Execute') ->assertSee('Preview-only run ready') ->assertSee('Create preview-only run') ->assertSee('Create a preview-only restore run.') ->assertSee('Operation proof is unavailable before execution.') ->assertSee('Post-run evidence is unavailable before execution.') ->assertSee('Recovery is not verified until post-run evidence exists.'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('09-step-5-confirm-ready')); spec333CopyBrowserScreenshot('09-step-5-confirm-ready'); }); it('captures confirmation review when execution prerequisites become unavailable after preview', function (): void { [$user, $tenant] = spec333BrowserTenant(); $backupSet = spec333BrowserUsableBackupFixture($tenant); $page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant))); $page->resize(1920, 1200) ->waitForText('Select Backup Set'); spec333BrowserSelectBackupSet($page, $backupSet); $page->waitForText('Source selected'); spec333BrowserWizardNext($page); $page->waitForText('Scope summary'); spec333BrowserWizardNext($page); $page->waitForText('Run checks') ->click('Run checks') ->waitForText('Validation passed'); spec333BrowserWizardNext($page); $page->waitForText('Generate preview') ->click('Generate preview') ->waitForText('Preview details'); $connection = ProviderConnection::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('provider', 'microsoft') ->first(); $connection?->credential()->delete(); $page->waitForText('Regenerate preview') ->click('Regenerate preview') ->waitForText('Execution unavailable until prerequisites are resolved') ->assertSee('Confirmation required') ->assertSee('Review preview and continue to confirmation; resolve execution prerequisites before executing.') ->assertDontSee('Execution blocked'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('10-step-4-preview-execution-prerequisites-unavailable')); spec333CopyBrowserScreenshot('10-step-4-preview-execution-prerequisites-unavailable'); spec333BrowserWizardNext($page); $page->waitForText('Confirm & Execute') ->assertSee('Confirmation summary') ->assertSee('Execution prerequisites blocked') ->assertSee('Create preview-only run') ->assertSee('Create a preview-only run, or resolve execution prerequisites before queueing real execution.') ->assertSee('Execution unavailable until prerequisites are resolved') ->assertSee('Operation proof is unavailable before execution.') ->assertSee('Preview only (dry-run)'); $page->script('window.scrollTo(0, 0);'); $page->screenshot(true, spec333BrowserScreenshotName('11-step-5-execution-prerequisites-locked')); spec333CopyBrowserScreenshot('11-step-5-execution-prerequisites-locked'); });