import { ContextStorePageType, type CommandMenuContextApi } from '@/types'; import { evaluateConditionalAvailabilityExpression } from '../evaluateConditionalAvailabilityExpression'; const buildContext = ( overrides: Partial = {}, ): CommandMenuContextApi => ({ pageType: ContextStorePageType.Index, isInSidePanel: false, isDashboardPageLayoutInEditMode: false, isLayoutCustomizationModeEnabled: false, favoriteRecordIds: [], isSelectAll: false, hasAnySoftDeleteFilterOnView: false, numberOfSelectedRecords: 0, objectPermissions: { objectMetadataId: '', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, canDestroyObjectRecords: false, restrictedFields: {}, rowLevelPermissionPredicates: [], rowLevelPermissionPredicateGroups: [], }, selectedRecords: [], featureFlags: {}, permissionFlags: {}, targetObjectReadPermissions: {}, targetObjectWritePermissions: {}, canImpersonate: false, canAccessFullAdminPanel: false, objectMetadataItem: {}, objectMetadataLabel: '', ...overrides, }); describe('evaluateConditionalAvailabilityExpression', () => { describe('arrayLength for favoriteRecordIds', () => { it('should evaluate arrayLength(favoriteRecordIds) < numberOfSelectedRecords when some selected are not favorites', () => { const expression = 'arrayLength(favoriteRecordIds) < numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView'; const context = buildContext({ favoriteRecordIds: ['id-1'], numberOfSelectedRecords: 2, selectedRecords: [ { id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null }, { id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null }, ], hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(true); }); it('should deny when all selected are already favorites', () => { const expression = 'arrayLength(favoriteRecordIds) < numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView'; const context = buildContext({ favoriteRecordIds: ['id-1', 'id-2'], numberOfSelectedRecords: 2, selectedRecords: [ { id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null }, { id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null }, ], hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(false); }); it('should evaluate arrayLength(favoriteRecordIds) == numberOfSelectedRecords when all selected are favorites', () => { const expression = 'arrayLength(favoriteRecordIds) == numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView'; const context = buildContext({ favoriteRecordIds: ['id-1', 'id-2'], numberOfSelectedRecords: 2, selectedRecords: [ { id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null }, { id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null }, ], hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(true); }); it('should reject favoriteRecordIds.length (expr-eval-fork parses "length" as unary op)', () => { const expression = 'favoriteRecordIds.length < numberOfSelectedRecords'; const context = buildContext({ favoriteRecordIds: ['id-1'], numberOfSelectedRecords: 2, }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(false); }); }); describe('includesSome', () => { it('should return true when some records include the value in the array prop', () => { const expression = 'includesSome(selectedRecords, "statuses", "ACTIVE")'; const context = buildContext({ selectedRecords: [ { id: 'id-1', statuses: ['ACTIVE'] }, { id: 'id-2', statuses: ['DRAFT'] }, ], }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(true); }); it('should return false when no record includes the value in the array prop', () => { const expression = 'includesSome(selectedRecords, "statuses", "ACTIVE")'; const context = buildContext({ selectedRecords: [ { id: 'id-1', statuses: ['DRAFT'] }, { id: 'id-2', statuses: ['DRAFT'] }, ], }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(false); }); }); describe('includesNone', () => { it('should return true when no record includes the value in the array prop', () => { const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")'; const context = buildContext({ selectedRecords: [ { id: 'id-1', statuses: ['DRAFT'] }, { id: 'id-2', statuses: ['DRAFT'] }, ], }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(true); }); it('should return false when any record includes the value in the array prop', () => { const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")'; const context = buildContext({ selectedRecords: [ { id: 'id-1', statuses: ['ACTIVE'] }, { id: 'id-2', statuses: ['DRAFT'] }, ], }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(false); }); it('should return false when all records include the value in the array prop', () => { const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")'; const context = buildContext({ selectedRecords: [ { id: 'id-1', statuses: ['ACTIVE', 'DRAFT'] }, { id: 'id-2', statuses: ['ACTIVE'] }, ], }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(false); }); }); describe('empty array guard against vacuous truth', () => { it('should return false for every() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'every(selectedRecords, "active")', context, ), ).toBe(false); }); it('should return false for everyDefined() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'everyDefined(selectedRecords, "name")', context, ), ).toBe(false); }); it('should return false for none() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'none(selectedRecords, "deletedAt")', context, ), ).toBe(false); }); it('should return false for noneDefined() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'noneDefined(selectedRecords, "deletedAt")', context, ), ).toBe(false); }); it('should return false for everyEquals() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'everyEquals(selectedRecords, "status", "DRAFT")', context, ), ).toBe(false); }); it('should return false for noneEquals() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'noneEquals(selectedRecords, "status", "ACTIVE")', context, ), ).toBe(false); }); it('should return false for includesEvery() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'includesEvery(selectedRecords, "statuses", "ACTIVE")', context, ), ).toBe(false); }); it('should return false for includesNone() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'includesNone(selectedRecords, "statuses", "ACTIVE")', context, ), ).toBe(false); }); it('should return false for some() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'some(selectedRecords, "active")', context, ), ).toBe(false); }); it('should return false for someDefined() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'someDefined(selectedRecords, "name")', context, ), ).toBe(false); }); it('should return false for includesSome() on empty selectedRecords', () => { const context = buildContext({ selectedRecords: [] }); expect( evaluateConditionalAvailabilityExpression( 'includesSome(selectedRecords, "statuses", "ACTIVE")', context, ), ).toBe(false); }); }); describe('isSelectAll bypasses selectedRecords array checks', () => { it('should return true for "isSelectAll or noneDefined(...)" when isSelectAll is true and selectedRecords is empty', () => { const context = buildContext({ isSelectAll: true, selectedRecords: [], }); expect( evaluateConditionalAvailabilityExpression( 'isSelectAll or noneDefined(selectedRecords, "deletedAt")', context, ), ).toBe(true); }); it('should return true for "isSelectAll or everyDefined(...)" when isSelectAll is true and selectedRecords is empty', () => { const context = buildContext({ isSelectAll: true, selectedRecords: [], }); expect( evaluateConditionalAvailabilityExpression( 'isSelectAll or everyDefined(selectedRecords, "deletedAt")', context, ), ).toBe(true); }); it('should evaluate the full deleteRecords expression to true in select-all mode', () => { const expression = 'numberOfSelectedRecords >= 1 and not hasAnySoftDeleteFilterOnView and objectPermissions.canSoftDeleteObjectRecords and (isSelectAll or noneDefined(selectedRecords, "deletedAt"))'; const context = buildContext({ isSelectAll: true, selectedRecords: [], numberOfSelectedRecords: 50, hasAnySoftDeleteFilterOnView: false, objectPermissions: { objectMetadataId: '', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, canDestroyObjectRecords: false, restrictedFields: {}, rowLevelPermissionPredicates: [], rowLevelPermissionPredicateGroups: [], }, }); expect( evaluateConditionalAvailabilityExpression(expression, context), ).toBe(true); }); it('should still respect noneDefined when isSelectAll is false and records are present', () => { const expression = 'isSelectAll or noneDefined(selectedRecords, "deletedAt")'; const contextWithDeletedRecord = buildContext({ isSelectAll: false, selectedRecords: [ { id: 'id-1', createdAt: '', updatedAt: '', deletedAt: '2024-01-01' }, ], }); expect( evaluateConditionalAvailabilityExpression( expression, contextWithDeletedRecord, ), ).toBe(false); }); }); describe('activateWorkflow expression regression', () => { const activateWorkflowExpression = 'everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and (everyEquals(selectedRecords, "currentVersion.status", "DRAFT") or includesNone(selectedRecords, "statuses", "ACTIVE")) and noneDefined(selectedRecords, "deletedAt")'; it('should not show Activate for an already-active workflow', () => { const context = buildContext({ selectedRecords: [ { id: 'wf-1', deletedAt: null, statuses: ['ACTIVE'], currentVersion: { status: 'ACTIVE', trigger: { type: 'MANUAL' }, steps: [{ id: 'step-1' }], }, }, ], }); expect( evaluateConditionalAvailabilityExpression( activateWorkflowExpression, context, ), ).toBe(false); }); it('should show Activate for a draft workflow with no active version', () => { const context = buildContext({ selectedRecords: [ { id: 'wf-1', deletedAt: null, statuses: ['DRAFT'], currentVersion: { status: 'DRAFT', trigger: { type: 'MANUAL' }, steps: [{ id: 'step-1' }], }, }, ], }); expect( evaluateConditionalAvailabilityExpression( activateWorkflowExpression, context, ), ).toBe(true); }); it('should show Activate for a draft version when another version is active', () => { const context = buildContext({ selectedRecords: [ { id: 'wf-1', deletedAt: null, statuses: ['ACTIVE', 'DRAFT'], currentVersion: { status: 'DRAFT', trigger: { type: 'MANUAL' }, steps: [{ id: 'step-1' }], }, }, ], }); expect( evaluateConditionalAvailabilityExpression( activateWorkflowExpression, context, ), ).toBe(true); }); }); describe('permissionFlags gating', () => { it('should hide exportRecords when EXPORT_CSV permission flag is missing', () => { const context = buildContext({ permissionFlags: {} }); expect( evaluateConditionalAvailabilityExpression( 'permissionFlags.EXPORT_CSV', context, ), ).toBe(false); }); it('should show exportRecords when EXPORT_CSV permission flag is present', () => { const context = buildContext({ permissionFlags: { EXPORT_CSV: true }, }); expect( evaluateConditionalAvailabilityExpression( 'permissionFlags.EXPORT_CSV', context, ), ).toBe(true); }); it('should hide importRecords when IMPORT_CSV permission flag is missing even if soft-delete filter is off', () => { const context = buildContext({ hasAnySoftDeleteFilterOnView: false, permissionFlags: {}, }); expect( evaluateConditionalAvailabilityExpression( 'not hasAnySoftDeleteFilterOnView and permissionFlags.IMPORT_CSV', context, ), ).toBe(false); }); it('should show importRecords when IMPORT_CSV permission flag is present and soft-delete filter is off', () => { const context = buildContext({ hasAnySoftDeleteFilterOnView: false, permissionFlags: { IMPORT_CSV: true }, }); expect( evaluateConditionalAvailabilityExpression( 'not hasAnySoftDeleteFilterOnView and permissionFlags.IMPORT_CSV', context, ), ).toBe(true); }); }); describe('pageType gating for index-only commands', () => { it('should hide createNewView on a record page', () => { const context = buildContext({ pageType: ContextStorePageType.Record, hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and not hasAnySoftDeleteFilterOnView', context, ), ).toBe(false); }); it('should show createNewView on the index page', () => { const context = buildContext({ pageType: ContextStorePageType.Index, hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and not hasAnySoftDeleteFilterOnView', context, ), ).toBe(true); }); it('should hide importRecords on a record page even with IMPORT_CSV permission', () => { const context = buildContext({ pageType: ContextStorePageType.Record, hasAnySoftDeleteFilterOnView: false, permissionFlags: { IMPORT_CSV: true }, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and not hasAnySoftDeleteFilterOnView and permissionFlags.IMPORT_CSV', context, ), ).toBe(false); }); it('should hide exportView on a record page even with EXPORT_CSV permission', () => { const context = buildContext({ pageType: ContextStorePageType.Record, permissionFlags: { EXPORT_CSV: true }, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and permissionFlags.EXPORT_CSV', context, ), ).toBe(false); }); it('should hide seeDeletedRecords on a record page', () => { const context = buildContext({ pageType: ContextStorePageType.Record, hasAnySoftDeleteFilterOnView: false, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and not hasAnySoftDeleteFilterOnView', context, ), ).toBe(false); }); it('should hide hideDeletedRecords on a record page', () => { const context = buildContext({ pageType: ContextStorePageType.Record, hasAnySoftDeleteFilterOnView: true, }); expect( evaluateConditionalAvailabilityExpression( 'pageType == "INDEX_PAGE" and hasAnySoftDeleteFilterOnView', context, ), ).toBe(false); }); }); });