Group queue items into a hybrid layout: Inventory section (CARD/GRANITE/DECOM) at top, then vendor-grouped sections for FP/Archer items sorted alphabetically. Each section header is clickable to collapse/expand with chevron indicators. - Extract grouping logic into reusable utility (queueGrouping.js) - Add collapse state management (all sections expanded by default) - Preserve cross-section multi-select, floating action bar, ticket badges - Add 5 property-based tests covering grouping correctness, ordering, empty section omission, count accuracy, and selection independence
214 lines
7.4 KiB
JavaScript
214 lines
7.4 KiB
JavaScript
/**
|
|
* 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 }
|
|
);
|
|
});
|
|
});
|