## 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
736 lines
28 KiB
PHP
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');
|
|
});
|