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
136 lines
4.8 KiB
JavaScript
136 lines
4.8 KiB
JavaScript
/**
|
|
* 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 }
|
|
);
|
|
});
|
|
});
|