makePortfolioTriageActor('Anchor Tenant'); $targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant'); $backupSet = $this->seedPortfolioBackupConcern($targetTenant, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($targetTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet); $triageState = $this->portfolioReturnFilters( [TenantBackupHealthAssessment::POSTURE_STALE], [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED], [], TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ); $expectedUrl = TenantResource::crossTenantCompareOpenUrl($targetTenant, $triageState); $this->portfolioTriageRegistryList($user, $anchorTenant, $triageState) ->assertTableActionVisible('compareTenants', $targetTenant) ->assertTableActionHasUrl('compareTenants', $expectedUrl, $targetTenant); $query = crossTenantCompareLaunchQuery($expectedUrl); $backUrl = urldecode((string) data_get($query, 'nav.back_url')); expect($query)->toMatchArray([ 'target_tenant_id' => (string) $targetTenant->getKey(), ]) ->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry') ->and(data_get($query, 'nav.tenant_id'))->toBe((string) $targetTenant->getKey()) ->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry') ->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE) ->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED) ->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); Livewire::withQueryParams($query) ->actingAs($user) ->test(CrossTenantComparePage::class) ->assertSet('sourceTenantId', null) ->assertSet('targetTenantId', (string) $targetTenant->getKey()) ->assertActionVisible('return_to_origin') ->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === 'Back to tenant registry' && $action->getUrl() === TenantResource::getUrl(panel: 'admin', parameters: $triageState)); }); it('launches cross-tenant compare from an exact-two bulk selection with both tenants prefilled', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant'); $targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant'); $anchorBackupSet = $this->seedPortfolioBackupConcern($anchorTenant, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($anchorTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $anchorBackupSet); $backupSet = $this->seedPortfolioBackupConcern($targetTenant, TenantBackupHealthAssessment::POSTURE_STALE); $this->seedPortfolioRecoveryConcern($targetTenant, RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP, $backupSet); $triageState = $this->portfolioReturnFilters( [TenantBackupHealthAssessment::POSTURE_STALE], [TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED], [], TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ); $expectedUrl = TenantResource::crossTenantCompareOpenUrlForSelection( targetTenant: $targetTenant, triageState: $triageState, sourceTenant: $anchorTenant, ); $this->portfolioTriageRegistryList($user, $anchorTenant, $triageState) ->selectTableRecords([$anchorTenant, $targetTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $targetTenant]) ->assertRedirect($expectedUrl); $query = crossTenantCompareLaunchQuery($expectedUrl); $backUrl = urldecode((string) data_get($query, 'nav.back_url')); expect($query)->toMatchArray([ 'source_tenant_id' => (string) $anchorTenant->getKey(), 'target_tenant_id' => (string) $targetTenant->getKey(), ]) ->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry') ->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry') ->and(data_get($query, 'nav.tenant_id'))->toBeNull() ->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE) ->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED) ->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); Livewire::withQueryParams($query) ->actingAs($user) ->test(CrossTenantComparePage::class) ->assertSet('sourceTenantId', (string) $anchorTenant->getKey()) ->assertSet('targetTenantId', (string) $targetTenant->getKey()) ->assertActionVisible('return_to_origin'); }); it('rejects the bulk compare action until exactly two active tenants are selected', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant'); $targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant'); $thirdTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Third Tenant'); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant]) ->assertNotified('Select exactly two tenants to compare.'); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant, $targetTenant, $thirdTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $targetTenant, $thirdTenant]) ->assertNotified('Select exactly two tenants to compare.'); }); it('rejects the bulk compare action when a selected tenant is not active', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant'); $onboardingTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Onboarding Tenant'); $onboardingTenant->forceFill([ 'status' => Tenant::STATUS_ONBOARDING, ])->save(); $this->portfolioTriageRegistryList($user, $anchorTenant) ->selectTableRecords([$anchorTenant, $onboardingTenant]) ->assertTableBulkActionVisible('compareSelected') ->callTableBulkAction('compareSelected', [$anchorTenant, $onboardingTenant]) ->assertNotified('Only active tenants can be compared.'); }); it('hides the compare launch action when workspace baseline view capability is missing', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant'); $targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant'); $resolver = \Mockery::mock(WorkspaceCapabilityResolver::class); $resolver->shouldReceive('isMember')->andReturnTrue(); $resolver->shouldReceive('can')->andReturnFalse(); app()->instance(WorkspaceCapabilityResolver::class, $resolver); $this->portfolioTriageRegistryList($user, $anchorTenant) ->assertTableActionHidden('compareTenants', $targetTenant); }); it('hides the compare launch action when the actor lacks tenant view on the launched tenant', function (): void { [$user, $anchorTenant] = $this->makePortfolioTriageActor('Anchor Tenant'); $targetTenant = $this->makePortfolioTriagePeer($user, $anchorTenant, 'Target Tenant'); $resolver = \Mockery::mock(CapabilityResolver::class); $resolver->shouldReceive('primeMemberships')->andReturnNull(); $resolver->shouldReceive('isMember')->andReturnTrue(); $resolver->shouldReceive('can')->andReturnUsing(function (mixed $actor, mixed $tenant, string $capability) use ($targetTenant): bool { if ($tenant instanceof Tenant && (int) $tenant->getKey() === (int) $targetTenant->getKey() && $capability === Capabilities::TENANT_VIEW) { return false; } return true; }); app()->instance(CapabilityResolver::class, $resolver); $this->portfolioTriageRegistryList($user, $anchorTenant) ->assertTableActionHidden('compareTenants', $targetTenant); });