Add collapsible sections to Ivanti Queue page
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
This commit is contained in:
135
frontend/src/__tests__/queue-grouping.property.test.js
Normal file
135
frontend/src/__tests__/queue-grouping.property.test.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Property-Based Test: Grouping Correctness
|
||||
*
|
||||
* Feature: queue-collapsible-sections, Property 1: Grouping Correctness
|
||||
* **Validates: Requirements 1.1, 1.2, 1.5**
|
||||
*
|
||||
* For any array of visible queue items, every item with workflow_type CARD,
|
||||
* GRANITE, or DECOM appears in the Inventory section; every FP/Archer item
|
||||
* appears in the vendor section matching its vendor field (or "Unknown" if
|
||||
* null/empty); no item appears in more than one section; and total items
|
||||
* across all sections equals input length.
|
||||
*/
|
||||
import fc from 'fast-check';
|
||||
import { groupQueueItems } from '../utils/queueGrouping';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Arbitraries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const INVENTORY_TYPES = ['CARD', 'GRANITE', 'DECOM'];
|
||||
const VENDOR_TYPES = ['FP', 'Archer'];
|
||||
const ALL_WORKFLOW_TYPES = [...INVENTORY_TYPES, ...VENDOR_TYPES];
|
||||
|
||||
// Generate vendor strings including edge cases: null, undefined, empty, whitespace-only, normal strings
|
||||
const vendorArbitrary = fc.oneof(
|
||||
fc.constant(null),
|
||||
fc.constant(undefined),
|
||||
fc.constant(''),
|
||||
fc.constant(' '),
|
||||
fc.stringMatching(/^[A-Za-z][A-Za-z0-9 ]{0,19}$/)
|
||||
);
|
||||
|
||||
// Generate a single queue item with a random workflow_type and vendor
|
||||
const queueItemArbitrary = fc.record({
|
||||
id: fc.integer({ min: 1, max: 100000 }),
|
||||
workflow_type: fc.constantFrom(...ALL_WORKFLOW_TYPES),
|
||||
vendor: vendorArbitrary,
|
||||
status: fc.constant('pending'),
|
||||
hostname: fc.string({ minLength: 1, maxLength: 20 }),
|
||||
});
|
||||
|
||||
// Generate arrays of queue items (0 to 50 items)
|
||||
const queueItemsArbitrary = fc.array(queueItemArbitrary, { minLength: 0, maxLength: 50 });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property Test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Queue Grouping — Property 1: Grouping Correctness', () => {
|
||||
it('every CARD/GRANITE/DECOM item appears in Inventory section', () => {
|
||||
fc.assert(
|
||||
fc.property(queueItemsArbitrary, (items) => {
|
||||
const sections = groupQueueItems(items);
|
||||
const inventorySection = sections.find(s => s.key === 'inventory');
|
||||
const inventoryItems = inventorySection ? inventorySection.items : [];
|
||||
|
||||
const expectedInventoryItems = items.filter(item =>
|
||||
INVENTORY_TYPES.includes(item.workflow_type)
|
||||
);
|
||||
|
||||
// Every inventory-type item must be in the inventory section
|
||||
for (const item of expectedInventoryItems) {
|
||||
expect(inventoryItems).toContain(item);
|
||||
}
|
||||
|
||||
// Inventory section should contain exactly the inventory-type items
|
||||
expect(inventoryItems.length).toBe(expectedInventoryItems.length);
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
|
||||
it('every FP/Archer item appears in a vendor section matching its vendor field (or "Unknown")', () => {
|
||||
fc.assert(
|
||||
fc.property(queueItemsArbitrary, (items) => {
|
||||
const sections = groupQueueItems(items);
|
||||
|
||||
const vendorSections = sections.filter(s => s.type === 'vendor');
|
||||
const allVendorItems = vendorSections.flatMap(s => s.items);
|
||||
|
||||
const expectedVendorItems = items.filter(item =>
|
||||
VENDOR_TYPES.includes(item.workflow_type)
|
||||
);
|
||||
|
||||
// Every vendor-type item must appear in a vendor section
|
||||
for (const item of expectedVendorItems) {
|
||||
expect(allVendorItems).toContain(item);
|
||||
|
||||
// Determine expected vendor key
|
||||
const expectedVendor = item.vendor?.trim() || 'Unknown';
|
||||
const expectedKey = `vendor:${expectedVendor}`;
|
||||
const matchingSection = sections.find(s => s.key === expectedKey);
|
||||
|
||||
expect(matchingSection).toBeDefined();
|
||||
expect(matchingSection.items).toContain(item);
|
||||
}
|
||||
|
||||
// Vendor sections should contain exactly the vendor-type items
|
||||
expect(allVendorItems.length).toBe(expectedVendorItems.length);
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
|
||||
it('no item appears in more than one section', () => {
|
||||
fc.assert(
|
||||
fc.property(queueItemsArbitrary, (items) => {
|
||||
const sections = groupQueueItems(items);
|
||||
|
||||
// Collect all items across all sections
|
||||
const allSectionItems = sections.flatMap(s => s.items);
|
||||
|
||||
// Use a Set to check for duplicates (by reference)
|
||||
const seen = new Set();
|
||||
for (const item of allSectionItems) {
|
||||
expect(seen.has(item)).toBe(false);
|
||||
seen.add(item);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
|
||||
it('total items across all sections equals input length', () => {
|
||||
fc.assert(
|
||||
fc.property(queueItemsArbitrary, (items) => {
|
||||
const sections = groupQueueItems(items);
|
||||
const totalInSections = sections.reduce((sum, s) => sum + s.items.length, 0);
|
||||
|
||||
expect(totalInSections).toBe(items.length);
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user