ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
264 lines
17 KiB
TypeScript
264 lines
17 KiB
TypeScript
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, /待交车 <strong>2<\/strong> 台/);
|
|
assert.match(html, /异动 <strong>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, '其他');
|
|
});
|