import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { InventoryStatusCounts } from './InventoryStatusCounts'; import { buildVehicleModalRequest, deriveInventoryView, groupInventoryByRegion, type VehicleModalSelection } from '../../model'; import { selectInventoryGroup, selectInventoryRow } from './InventoryDesktopTable'; import type { RegionalInventoryStats } from '../../types'; import { inventoryBrandGroups, inventoryGroups, inventoryHierarchy, inventoryMobileDrilldown, inventoryMobileGroups, inventoryMobileRows, inventoryModelTags, inventoryPage, inventoryRegionRows, inventorySelection, inventorySubtotalSelection } from './inventory-presentation'; import { inventoryKey } from '../../../../shared/assets/inventory'; import { fetchVehicleList } from '../../api'; const filters = { region: '华东', city: '上海', brand: '品牌甲', type: '18T', model: '批次一' }; test('abnormal inventory drill preserves exact row keys and intersects the abnormal category and company', () => { const rows = [{ ...filters, batch: '', quantity: 10, abnormal: 1 }]; const selection = inventorySubtotalSelection(rows, rows[0].model, 'Abnormal'); const request = buildVehicleModalRequest(selection, '公司甲'); assert.equal(request.kind, 'vehicles'); if (request.kind !== 'vehicles') throw new Error('wrong endpoint'); assert.deepEqual(request.params.inventoryKeys, rows.map(inventoryKey)); assert.equal(request.params.category, 'Abnormal'); assert.equal(request.params.subject, '公司甲'); assert.match(selection.scopeLabel!, /异动 1 台$/); assert.ok(selection.scopeLabel!.includes('批次一')); }); test('mobile region navigation shows only cities then types, preserving totals and exact leaf scope', () => { const rows = [ { ...filters, batch: '', quantity: 9 }, { ...filters, brand: '品牌乙', batch: '', quantity: 8 }, { ...filters, type: '49T', batch: '', quantity: 3 }, { ...filters, city: '苏州', batch: '', quantity: 2 }, ]; const cities = inventoryMobileDrilldown(rows, 'region', []); assert.equal(cities.dimension, 'city'); assert.deepEqual(cities.groups.map(group => [group.label, group.quantity]), [['上海', 20], ['苏州', 2]]); const types = inventoryMobileDrilldown(rows, 'region', ['上海']); assert.equal(types.dimension, 'type'); assert.deepEqual(types.groups.map(group => [group.label, group.quantity]), [['18T', 17], ['49T', 3]]); const variants = inventoryMobileDrilldown(rows, 'region', ['上海', '18T']); assert.equal(variants.dimension, 'variant'); assert.deepEqual(variants.groups.map(group => group.quantity), [9, 8]); assert.deepEqual(variants.groups.flatMap(group => inventorySubtotalSelection(group.rows, group.label).inventoryKeys), rows.slice(0, 2).map(inventoryKey)); assert.deepEqual(inventoryMobileDrilldown(rows, 'region', ['上海']).groups, types.groups, 'back navigation restores the whole parent'); }); test('mobile model navigation isolates same-named cities by region and recovers invalid paths', () => { const rows = [ { ...filters, batch: '', quantity: 4 }, { ...filters, region: '华南', batch: '', quantity: 7 }, { ...filters, region: '华南', city: '', batch: '', quantity: 1 }, ]; assert.equal(inventoryMobileDrilldown(rows, 'model', []).dimension, 'region'); const cities = inventoryMobileDrilldown(rows, 'model', ['华南']); assert.deepEqual(cities.groups.map(group => [group.label, group.quantity]), [['上海', 7], ['未分配城市', 1]]); assert.deepEqual(inventoryMobileDrilldown(rows, 'model', ['华南', '上海']).rows, [rows[1]]); assert.deepEqual(inventoryMobileDrilldown(rows, 'model', ['华南', '']).rows, [rows[2]]); assert.equal(inventoryMobileDrilldown(rows, 'model', ['华南', '已移除城市']).dimension, 'city'); assert.equal(inventoryMobileDrilldown(rows, 'model', ['已移除区域']).dimension, 'region'); assert.deepEqual(inventoryMobileDrilldown([], 'model', []).groups, []); }); test('mobile type totals combine brands and other types without merging distinct variants', () => { const rows = [ { ...filters, type: '挂车', brand: '帕力安牌', batch: '一批', quantity: 3 }, { ...filters, type: '其他', brand: '帕力安', batch: '二批', quantity: 4 }, ]; assert.deepEqual(inventoryMobileGroups(rows, 'type').map(group => [group.label, group.quantity]), [['其他', 7]]); const view = inventoryMobileDrilldown(rows, 'region', ['上海', '其他']); assert.equal(view.groups.length, 2); assert.deepEqual(view.groups.flatMap(group => group.rows), rows); }); test('subtotal drilldown sends the exact filtered records, including merged brand aliases and vehicle types', async (t) => { const rows = [ { ...filters, brand: '帕力安牌', type: '挂车', batch: '', quantity: 2 }, { ...filters, brand: '帕力安', type: '其他', model: '其他型号', batch: '', quantity: 1 }, ]; const selection = inventorySubtotalSelection(rows, '其他'); const request = buildVehicleModalRequest(selection, '公司甲'); assert.equal(request.kind, 'vehicles'); if (request.kind !== 'vehicles') throw new Error('wrong endpoint'); assert.deepEqual(request.params.inventoryKeys, rows.map(inventoryKey)); assert.equal(request.params.subject, '公司甲'); assert.equal(request.params.category, 'Inventory'); assert.equal(request.params.brand, undefined, 'a merged alias must not restrict the scope to one spelling'); t.mock.method(globalThis, 'fetch', async (url: string, init?: RequestInit) => { assert.equal(init?.method, 'POST'); assert.deepEqual(JSON.parse(String(init?.body)).inventoryKeys, rows.map(inventoryKey)); assert.equal(new URL(url, 'http://localhost').searchParams.get('subject'), '公司甲'); return new Response('[]', { status: 200 }); }); await fetchVehicleList(request.params); }); test('hierarchy subtotals stay inside their parent and include records beyond the current page', () => { const base = { ...filters, batch: '', quantity: 9 }; const rows = [base, { ...base, model: '批次二', quantity: 8 }, { ...base, type: '49T', quantity: 5 }, { ...base, brand: '品牌乙', quantity: 3 }, { ...base, city: '苏州', quantity: 2 }]; assert.deepEqual(inventoryHierarchy(base, rows, 'region').map(level => level.quantity), [25, 22, 17]); const modelRows = rows.filter(row => row.type === '18T'); assert.deepEqual(inventoryHierarchy(base, modelRows, 'model').map(level => level.quantity), [19, 11, 11]); assert.deepEqual(inventoryHierarchy(base, [base], 'region').map(level => level.quantity), [9, 9, 9]); }); test('region merging brings separated cities together without losing their quantities or scope', () => { const rows = [ { ...filters, region: '华南', city: '广州', batch: '', quantity: 25 }, { ...filters, batch: '', quantity: 9 }, { ...filters, region: '华南', city: '佛山', batch: '', quantity: 1 }, ]; const merged = inventoryRegionRows(rows); assert.deepEqual(merged.map(({ row, regionSpan }) => [row.city, row.quantity, regionSpan]), [['广州', 25, 2], ['佛山', 1, 0], ['上海', 9, 1]]); assert.deepEqual(rows.map(row => row.city), ['广州', '上海', '佛山']); assert.equal(merged[1].row, rows[2]); }); test('mobile pages cover every record once and clamp after a filter reduces the rows', () => { const rows = Array.from({ length: 19 }, (_, index) => ({ ...filters, model: `型号${index}`, batch: '', quantity: index + 1 })); const group = inventoryGroups(groupInventoryByRegion(rows), 'default')[0]; const ordered = inventoryMobileRows(group, 'region'); const pages = Array.from({ length: 4 }, (_, index) => inventoryPage(ordered, index)); assert.deepEqual(pages.map(page => page.rows.length), [6, 6, 6, 1]); assert.deepEqual(pages.flatMap(page => page.rows), rows); assert.equal(inventoryPage(rows.slice(0, 2), 3).page, 0); assert.deepEqual(inventoryPage([], 1), { page: 0, pageCount: 1, start: 0, rows: [] }); }); test('inventory status hints show nonzero subset counts and hide zeros', () => { const row = { ...filters, batch: '', quantity: 9, pending: 2, abnormal: 1 }; const html = renderToStaticMarkup(createElement(InventoryStatusCounts, { onSelect: () => {}, rows: [row] })); assert.match(html, /待交车 2<\/strong> 台/); assert.match(html, /异动 1<\/strong> 台/); assert.equal(renderToStaticMarkup(createElement(InventoryStatusCounts, { onSelect: () => {}, rows: [{ ...row, pending: 0, abnormal: 0 }] })), ''); }); test('filtered inventory totals retain all filters in the vehicle request', () => { const request = buildVehicleModalRequest(inventorySelection(filters, '筛选结果'), '公司甲'); assert.deepEqual(request, { kind: 'vehicles', params: { brand: '品牌甲', vehicleType: '18T', model: '批次一', location: '上海', source: 'asset', category: 'Inventory', subject: '公司甲', } }); }); test('inventory subsets retain scope while changing category', () => { for (const category of ['Pending', 'Abnormal'] as const) { const request = buildVehicleModalRequest(inventorySelection({ ...filters, city: '' }, '子集', { category }), null); assert.equal(request.kind, 'vehicles'); if (request.kind !== 'vehicles') throw new Error('wrong request type'); assert.equal(request.params.location, '华东'); assert.equal(request.params.category, category); assert.equal(request.params.vehicleType, '18T'); assert.equal(request.params.brand, '品牌甲'); } }); test('quantity sorting preserves source order and counts inventory subsets only once', () => { const rows: RegionalInventoryStats[] = [ { ...filters, batch: '', quantity: 8, pending: 2, abnormal: 1 }, { ...filters, city: '苏州', batch: '', quantity: 3 }, { ...filters, region: '华南', city: '广州', batch: '', quantity: 20 }, ]; const source = groupInventoryByRegion(rows); assert.deepEqual(inventoryGroups(source, 'quantity').map(group => [group.name, group.quantity]), [['华南', 20], ['华东', 11]]); assert.deepEqual(inventoryGroups(source, 'default').map(group => group.name), ['华东', '华南']); assert.deepEqual(inventoryGroups({}, 'quantity'), []); }); test('model tags separate type and qualifiers without inventing a batch', () => { assert.deepEqual(inventoryModelTags({ type: '4.5T冷链', model: '现代4.5T冷链(交投)', batch: '现代4.5T冷链(交投)' }), [ { kind: 'type', label: '车型', value: '4.5T冷链' }, { kind: 'detail', label: '型号说明', value: '交投' }, ]); assert.deepEqual(inventoryModelTags({ type: '18T', model: '飞驰18T(白车)', batch: '2026-01' }).map(tag => tag.value), ['18T', '白车', '2026-01']); assert.deepEqual(inventoryModelTags({ type: '挂车', model: '挂车', batch: '挂车' }).map(tag => tag.value), ['其他', '挂车']); assert.deepEqual(inventoryModelTags({ type: '', model: '未知型号(未闭合', batch: '' }).map(tag => tag.value), ['未知型号(未闭合']); }); test('brand removal preserves body style, color and qualifiers in model tags', () => { assert.deepEqual(inventoryModelTags({ brand: '帕力安牌', type: '18T', model: '现代18T双飞翼(羚牛)', batch: '' }).map(tag => tag.value), ['18T', '双飞翼', '羚牛']); assert.deepEqual(inventoryModelTags({ brand: '苏龙', type: '18T', model: '苏龙18T飞翼(白色)', batch: '' }).map(tag => tag.value), ['18T', '飞翼', '白色']); assert.deepEqual(inventoryModelTags({ brand: '飞驰', type: '49T', model: '49T飞驰(红车)', batch: '' }).map(tag => tag.value), ['49T', '红车']); }); test('brand ordering stays inside city blocks and does not mutate source rows', () => { const brands = ['其他', '飞驰', '跃进', '帕力安牌', '宇通', '现代', '苏龙', '帕力安', '明威']; const records = brands.map(brand => ({ ...filters, brand, batch: '', quantity: 1 })); const source = { 华东: { 上海: records, 苏州: [{ ...records[0], city: '苏州' }] } }; const result = inventoryGroups(source, 'default'); assert.deepEqual(result[0].sections.map(([name]) => name), ['上海', '苏州']); assert.deepEqual(result[0].sections[0][1].map(row => row.brand), ['现代', '帕力安牌', '帕力安', '宇通', '飞驰', '跃进', '苏龙', '明威', '其他']); assert.deepEqual(records.map(row => row.brand), brands); assert.equal(result[0].quantity, 10); }); test('type view sorts complete model blocks by brand while preserving model grouping', () => { const row = { ...filters, batch: '', quantity: 1 }; const source = { '18T': { 飞驰白车: [{ ...row, brand: '飞驰' }, { ...row, brand: '飞驰', city: '苏州' }], 现代双飞翼: [{ ...row, brand: '帕力安牌' }], 现代普货: [{ ...row, brand: '现代' }], } }; const result = inventoryGroups(source, 'default', 'model'); assert.deepEqual(result[0].sections.map(([name]) => name), ['现代普货', '现代双飞翼', '飞驰白车']); assert.equal(result[0].sections[2][1].length, 2); }); test('brand and type groups combine nonadjacent variants while retaining all detail records', () => { const rows: RegionalInventoryStats[] = [ { ...filters, brand: '飞驰', type: '18T', model: '飞驰18T(白车)', batch: '', quantity: 9 }, { ...filters, brand: '现代', type: '18T', model: '现代18T双飞翼(羚牛)', batch: '', quantity: 8 }, { ...filters, brand: '飞驰', type: '49T', model: '49T飞驰', batch: '', quantity: 16 }, { ...filters, brand: '飞驰', type: '18T', model: '飞驰18T(红车)', batch: '', quantity: 7 }, ]; const groups = inventoryBrandGroups(rows, 'type'); assert.deepEqual(groups.map(group => group.brand), ['现代', '飞驰']); assert.deepEqual(groups[1].variants.map(variant => variant.map(row => row.quantity)), [[9, 7], [16]]); assert.equal(groups[1].variants[0][0], rows[0]); assert.equal(groups[1].variants[0][1], rows[3]); assert.equal(groups.flatMap(group => group.rows).reduce((sum, row) => sum + row.quantity, 0), 40); assert.deepEqual(rows.map(row => row.quantity), [9, 8, 16, 7]); assert.deepEqual(groups[1].variants[0].map(row => inventoryModelTags(row).filter(tag => tag.kind !== 'type').map(tag => tag.value)), [['白车'], ['红车']]); }); test('model grouping merges locations but keeps different brands, colors and batches separate', () => { const base = { ...filters, brand: '飞驰', type: '18T', model: '飞驰18T(白车)', batch: '一期', quantity: 1 }; const rows = [base, { ...base, city: '苏州' }, { ...base, batch: '二期' }, { ...base, model: '飞驰18T(红车)' }, { ...base, brand: '现代' }]; const groups = inventoryBrandGroups(rows, 'model'); assert.deepEqual(groups.map(group => group.brand), ['现代', '飞驰']); assert.deepEqual(groups[1].variants.map(variant => variant.length), [2, 1, 1]); assert.deepEqual(groups[1].variants[0].map(row => row.city), ['上海', '苏州']); for (const row of groups.flatMap(group => group.rows)) { assert.equal(inventorySelection(row, '明细').model, row.model); assert.equal(inventorySelection(row, '明细').brand, row.brand); assert.equal(inventorySelection(row, '明细').location, row.city); } }); test('inventory other includes trailers in groups, filters and drilldown while retaining exact row types', () => { const trailers = { ...filters, brand: '明威', type: '挂车', model: '挂车', batch: '挂车', quantity: 3, pending: 1 }; const other = { ...filters, brand: '其他', type: '其他', model: '氢能叉车', batch: '', quantity: 2, abnormal: 1 }; const standard = { ...filters, type: '18T', batch: '', quantity: 10 }; const filter = { region: '', city: '', brand: '', type: '其他', model: '' }; const view = deriveInventoryView([trailers, other, standard], filter, '其他'); assert.deepEqual(view.filtered, [trailers, other]); assert.deepEqual(view.types, ['18T', '其他']); assert.deepEqual(view.modelsForType, ['挂车', '氢能叉车']); assert.deepEqual(Object.keys(view.byModel), ['其他']); const group = inventoryGroups(view.byModel, 'default', 'model')[0]; assert.equal(group.quantity, 5); assert.equal(group.rows.reduce((sum, row) => sum + (row.pending || 0), 0), 1); assert.equal(group.rows.reduce((sum, row) => sum + (row.abnormal || 0), 0), 1); let selected: VehicleModalSelection | null = null; const select = (value: VehicleModalSelection | null | ((previous: VehicleModalSelection | null) => VehicleModalSelection | null)) => { selected = typeof value === 'function' ? value(selected) : value; }; selectInventoryGroup(group, { tab: 'model', filters: filter, select }); assert.equal((selected as VehicleModalSelection | null)?.vehicleType, '库存其他'); assert.equal(inventorySelection(filter, '筛选后库存').vehicleType, '库存其他'); selectInventoryRow(trailers, select); assert.equal((selected as VehicleModalSelection | null)?.vehicleType, '挂车'); assert.equal((selected as VehicleModalSelection | null)?.model, '挂车'); selectInventoryRow(other, select); assert.equal((selected as VehicleModalSelection | null)?.vehicleType, '其他'); });