satisfy(ActionSurfaceSlot::ListHeader) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu) ->satisfy(ActionSurfaceSlot::ListBulkMoreGroup) ->satisfy(ActionSurfaceSlot::ListEmptyState) ->satisfy(ActionSurfaceSlot::DetailHeader); } } final class ActionSurfaceValidatorMissingSlotStub { public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->satisfy(ActionSurfaceSlot::ListHeader) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value); } } final class ActionSurfaceValidatorRunLogNoExportStub { public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::RunLog) ->withDefaults(new \App\Support\Ui\ActionSurface\ActionSurfaceDefaults( moreGroupLabel: 'More', exportIsDefaultBulkActionForReadOnly: false, )) ->satisfy(ActionSurfaceSlot::ListHeader) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->satisfy(ActionSurfaceSlot::ListBulkMoreGroup) ->satisfy(ActionSurfaceSlot::ListEmptyState) ->satisfy(ActionSurfaceSlot::DetailHeader); } } final class ActionSurfaceValidatorExemptSlotWithoutReasonStub { public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->setSlot(ActionSurfaceSlot::ListHeader, ActionSurfaceSlotRequirement::exempt()) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu) ->satisfy(ActionSurfaceSlot::ListBulkMoreGroup) ->satisfy(ActionSurfaceSlot::ListEmptyState) ->satisfy(ActionSurfaceSlot::DetailHeader); } } final class ActionSurfaceValidatorNoDeclarationStub {} function actionSurfaceComponent(string $className): ActionSurfaceDiscoveredComponent { return new ActionSurfaceDiscoveredComponent( className: $className, componentType: ActionSurfaceComponentType::Resource, panelScopes: [ActionSurfacePanelScope::Tenant], ); } it('passes when all required slots are declared', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorCompleteStub::class)]); expect($result->hasIssues())->toBeFalse($result->formatForAssertion()); }); it('fails when a required slot is missing', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorMissingSlotStub::class)]); expect($result->hasIssues())->toBeTrue(); expect($result->formatForAssertion())->toContain('Required slot is not declared'); }); it('fails missing declarations when no baseline exemption exists', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorNoDeclarationStub::class)]); expect($result->hasIssues())->toBeTrue(); expect($result->formatForAssertion())->toContain('Missing action-surface declaration'); }); it('accepts missing declarations when explicit baseline exemption exists', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([ ActionSurfaceValidatorNoDeclarationStub::class => 'Retrofit intentionally deferred in validator unit test.', ]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorNoDeclarationStub::class)]); expect($result->hasIssues())->toBeFalse($result->formatForAssertion()); }); it('requires a bulk exemption reason when run-log export default is disabled', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorRunLogNoExportStub::class)]); expect($result->hasIssues())->toBeTrue(); expect($result->formatForAssertion())->toContain('ReadOnly/RunLog profile disables Export default'); }); it('fails when slot exemption reason is missing', function (): void { $validator = new ActionSurfaceValidator( profileDefinition: new ActionSurfaceProfileDefinition, exemptions: new ActionSurfaceExemptions([]), ); $result = $validator->validateComponents([actionSurfaceComponent(ActionSurfaceValidatorExemptSlotWithoutReasonStub::class)]); expect($result->hasIssues())->toBeTrue(); expect($result->formatForAssertion())->toContain('Slot is marked exempt but exemption reason is missing or empty'); });