TenantAtlas/apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php
ahmido 1e45a29937 feat: finalize restore create ux productization (#403)
## Summary
- finalize the restore create wizard productization across safety, validation, preview, and confirmation steps
- refine the restore presenter output and Blade component rendering for clearer proof, scope, resolver, and execution-readiness states
- add and update feature and browser coverage plus Spec 333 artifacts and screenshots

## Testing
- Not run as part of this commit/PR task

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #403
2026-05-28 22:04:32 +00:00

736 lines
28 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\RestoreRunResource;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\EntraGroup;
use App\Models\ManagedEnvironment;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ProviderConnection;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->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(<<<JS
(() => {
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');
});