/** * Property-Based Test: Selection Independence from Collapse State * * Feature: queue-collapsible-sections, Property 5: Selection Independence from Collapse State * **Validates: Requirements 4.2, 4.4, 4.5** * * For any combination of selected items and collapse state, the set of selected * item IDs remains unchanged when sections are collapsed or expanded. Select All * always covers all visible items regardless of which sections are collapsed. */ import fc from 'fast-check'; // --- Pure logic under test (mirrors IvantiTodoQueuePage behavior) --- /** * Toggle a section's collapse state. Only modifies collapsedSections, * never touches selectedIds. */ function toggleSection(collapsedSections, sectionKey) { return { ...collapsedSections, [sectionKey]: !collapsedSections[sectionKey] }; } /** * Select All — always selects all visible items regardless of collapse state. * The visibleItems array contains ALL pending items, not just those in expanded sections. */ function selectAll(visibleItems) { return new Set(visibleItems.map(item => item.id)); } // --- Generators --- const WORKFLOW_TYPES = ['CARD', 'GRANITE', 'DECOM', 'FP', 'Archer']; const VENDORS = ['Microsoft', 'Adobe', 'Cisco', 'Unknown', 'Oracle', 'VMware']; const queueItemArbitrary = fc.record({ id: fc.integer({ min: 1, max: 100000 }), workflow_type: fc.constantFrom(...WORKFLOW_TYPES), vendor: fc.constantFrom(...VENDORS), status: fc.constant('pending'), }); // Generate arrays of queue items with unique IDs const queueItemsArbitrary = fc .array(queueItemArbitrary, { minLength: 1, maxLength: 50 }) .map(items => { // Ensure unique IDs const seen = new Set(); return items.filter(item => { if (seen.has(item.id)) return false; seen.add(item.id); return true; }); }) .filter(items => items.length > 0); // Generate a subset of item IDs as the selected set const selectedIdsArbitrary = (items) => fc.subarray(items.map(i => i.id), { minLength: 0 }).map(ids => new Set(ids)); // Generate section keys that would exist for a given set of items const sectionKeysForItems = (items) => { const keys = new Set(); const INVENTORY_TYPES = new Set(['CARD', 'GRANITE', 'DECOM']); let hasInventory = false; for (const item of items) { if (INVENTORY_TYPES.has(item.workflow_type)) { hasInventory = true; } else { keys.add(`vendor:${item.vendor || 'Unknown'}`); } } if (hasInventory) keys.add('inventory'); return [...keys]; }; // Generate arbitrary collapse state for given section keys const collapsedSectionsArbitrary = (sectionKeys) => fc.record( Object.fromEntries(sectionKeys.map(key => [key, fc.boolean()])), { withDeletedKeys: true } ); // --- Tests --- describe('Property 5: Selection Independence from Collapse State', () => { it('toggling collapse state does not alter the set of selected item IDs', () => { fc.assert( fc.property( queueItemsArbitrary.chain(items => fc.tuple( fc.constant(items), selectedIdsArbitrary(items), fc.constant(sectionKeysForItems(items)) ) ).chain(([items, selectedIds, sectionKeys]) => fc.tuple( fc.constant(items), fc.constant(selectedIds), sectionKeys.length > 0 ? collapsedSectionsArbitrary(sectionKeys) : fc.constant({}), sectionKeys.length > 0 ? fc.array(fc.constantFrom(...sectionKeys), { minLength: 1, maxLength: 10 }) : fc.constant([]) ) ), ([items, selectedIds, initialCollapsed, toggleSequence]) => { // Record the original selected IDs const originalSelectedIds = new Set(selectedIds); // Apply a sequence of toggleSection calls let currentCollapsed = { ...initialCollapsed }; for (const sectionKey of toggleSequence) { currentCollapsed = toggleSection(currentCollapsed, sectionKey); } // The selectedIds set must remain completely unchanged // (toggleSection only modifies collapsedSections, never selectedIds) expect(selectedIds).toEqual(originalSelectedIds); expect(selectedIds.size).toBe(originalSelectedIds.size); for (const id of originalSelectedIds) { expect(selectedIds.has(id)).toBe(true); } } ), { numRuns: 200 } ); }); it('Select All always covers all visible items regardless of collapse state', () => { fc.assert( fc.property( queueItemsArbitrary.chain(items => fc.tuple( fc.constant(items), fc.constant(sectionKeysForItems(items)) ) ).chain(([items, sectionKeys]) => fc.tuple( fc.constant(items), sectionKeys.length > 0 ? collapsedSectionsArbitrary(sectionKeys) : fc.constant({}) ) ), ([visibleItems, collapsedSections]) => { // Regardless of which sections are collapsed, selectAll operates // on the full visibleItems array (all pending items) const allSelectedIds = selectAll(visibleItems); // Every visible item must be in the selected set for (const item of visibleItems) { expect(allSelectedIds.has(item.id)).toBe(true); } // The selected set size must equal the number of visible items expect(allSelectedIds.size).toBe(visibleItems.length); // Verify this holds even after toggling all sections to collapsed let fullyCollapsed = { ...collapsedSections }; const sectionKeys = Object.keys(collapsedSections); for (const key of sectionKeys) { fullyCollapsed = toggleSection(fullyCollapsed, key); } // selectAll still returns all visible items — collapse state is irrelevant const afterCollapseSelectAll = selectAll(visibleItems); expect(afterCollapseSelectAll).toEqual(allSelectedIds); expect(afterCollapseSelectAll.size).toBe(visibleItems.length); } ), { numRuns: 200 } ); }); it('selection count always equals total selected items across all sections regardless of collapse', () => { fc.assert( fc.property( queueItemsArbitrary.chain(items => fc.tuple( fc.constant(items), selectedIdsArbitrary(items), fc.constant(sectionKeysForItems(items)) ) ).chain(([items, selectedIds, sectionKeys]) => fc.tuple( fc.constant(items), fc.constant(selectedIds), sectionKeys.length > 0 ? collapsedSectionsArbitrary(sectionKeys) : fc.constant({}) ) ), ([_items, selectedIds, collapsedSections]) => { // The selection count (selectedIds.size) is independent of collapse state. // Changing collapse state should never change the count. const countBefore = selectedIds.size; // Toggle every section let currentCollapsed = { ...collapsedSections }; const sectionKeys = Object.keys(collapsedSections); for (const key of sectionKeys) { currentCollapsed = toggleSection(currentCollapsed, key); // After each toggle, selection count must remain the same expect(selectedIds.size).toBe(countBefore); } } ), { numRuns: 200 } ); }); });