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
93 lines
3.0 KiB
JavaScript
93 lines
3.0 KiB
JavaScript
/**
|
|
* Property-Based Test: Section Ordering
|
|
*
|
|
* Feature: queue-collapsible-sections, Property 2: Section Ordering
|
|
* **Validates: Requirements 1.3, 1.4**
|
|
*
|
|
* For any array of queue items, the Inventory section (if present) is always
|
|
* the first element, and all vendor sections are sorted alphabetically by label.
|
|
*/
|
|
import fc from 'fast-check';
|
|
|
|
// Inline grouping logic (mirrors the implementation in IvantiTodoQueuePage)
|
|
function groupQueueItems(visibleItems) {
|
|
const INVENTORY_TYPES = new Set(['CARD', 'GRANITE', 'DECOM']);
|
|
const inventoryItems = [];
|
|
const vendorMap = new Map();
|
|
for (const item of visibleItems) {
|
|
if (INVENTORY_TYPES.has(item.workflow_type)) {
|
|
inventoryItems.push(item);
|
|
} else {
|
|
const vendor = item.vendor?.trim() || 'Unknown';
|
|
if (!vendorMap.has(vendor)) vendorMap.set(vendor, []);
|
|
vendorMap.get(vendor).push(item);
|
|
}
|
|
}
|
|
const sections = [];
|
|
if (inventoryItems.length > 0) {
|
|
sections.push({ key: 'inventory', label: 'Inventory', type: 'inventory', items: inventoryItems });
|
|
}
|
|
const sortedVendors = [...vendorMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
for (const [vendor, items] of sortedVendors) {
|
|
sections.push({ key: `vendor:${vendor}`, label: vendor, type: 'vendor', items });
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
// Generator for queue items with random workflow types and vendors
|
|
const workflowTypeArb = fc.constantFrom('CARD', 'GRANITE', 'DECOM', 'FP', 'Archer');
|
|
|
|
const vendorArb = fc.oneof(
|
|
fc.constant(null),
|
|
fc.constant(undefined),
|
|
fc.constant(''),
|
|
fc.constant(' '),
|
|
fc.stringMatching(/^[A-Za-z][A-Za-z0-9 ]{0,14}$/)
|
|
);
|
|
|
|
const queueItemArb = fc.record({
|
|
id: fc.integer({ min: 1, max: 100000 }),
|
|
workflow_type: workflowTypeArb,
|
|
vendor: vendorArb,
|
|
hostname: fc.string({ minLength: 1, maxLength: 20 }),
|
|
status: fc.constant('pending'),
|
|
});
|
|
|
|
const queueItemsArb = fc.array(queueItemArb, { minLength: 0, maxLength: 50 });
|
|
|
|
describe('Queue Grouping — Property 2: Section Ordering', () => {
|
|
it('Inventory section (if present) is always the first element', () => {
|
|
fc.assert(
|
|
fc.property(queueItemsArb, (items) => {
|
|
const sections = groupQueueItems(items);
|
|
|
|
const inventoryIndex = sections.findIndex(s => s.type === 'inventory');
|
|
|
|
// If inventory section exists, it must be at index 0
|
|
if (inventoryIndex !== -1) {
|
|
expect(inventoryIndex).toBe(0);
|
|
}
|
|
}),
|
|
{ numRuns: 500 }
|
|
);
|
|
});
|
|
|
|
it('Vendor sections are sorted alphabetically by label', () => {
|
|
fc.assert(
|
|
fc.property(queueItemsArb, (items) => {
|
|
const sections = groupQueueItems(items);
|
|
|
|
const vendorSections = sections.filter(s => s.type === 'vendor');
|
|
|
|
// Each consecutive pair of vendor sections must be in alphabetical order
|
|
for (let i = 1; i < vendorSections.length; i++) {
|
|
const prev = vendorSections[i - 1].label;
|
|
const curr = vendorSections[i].label;
|
|
expect(prev.localeCompare(curr)).toBeLessThanOrEqual(0);
|
|
}
|
|
}),
|
|
{ numRuns: 500 }
|
|
);
|
|
});
|
|
});
|