import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' /** * 手动匹配弹窗曾经被插到「编辑寄售物品」弹窗**内部**,导致外层弹窗没打开时它根本没挂载: * 用户点「手动匹配订单」什么都不会发生。这类"点了没反应"的问题在浏览器里才看得出来, * 这里用静态检查把它钉住:组件必须挂在顶层(前面不能有未闭合的 Modal)。 */ function assertTopLevelMount(filePath: string, componentName: string) { const source = readFileSync(filePath, 'utf8') const index = source.indexOf(`<${componentName}`) assert.ok(index > 0, `${filePath} 里找不到 ${componentName} 的挂载点`) const before = source.slice(0, index) const opened = (before.match(/]/g) || []).length const closed = (before.match(/<\/Modal>/g) || []).length assert.equal( opened - closed, 0, `${componentName} 必须挂在顶层:不能嵌在别的 Modal 里,否则外层弹窗未打开时它不会渲染`, ) } test('库存面板:手动匹配订单弹窗挂在顶层(按钮点了必须能弹出来)', () => { assertTopLevelMount( new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname, 'ConsignmentManualMatchModal', ) }) test('订单面板:手动指定现货弹窗同样挂在顶层', () => { assertTopLevelMount( new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname, 'ConsignmentManualMatchModal', ) }) test('按钮点击要真的把状态设上,弹窗的 open 由该状态驱动', () => { const source = readFileSync( new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname, 'utf8', ) assert.ok(source.includes('setManualMatchItem(record)'), '行内按钮必须设置待匹配的现货') assert.ok(source.includes('open={Boolean(manualMatchItem)}'), '弹窗 open 必须由该状态驱动') }) test('手动匹配入口必须按 consignment.fulfill 门控(提交的是履约动作,不能只挂 manage)', () => { const inventoryPanel = readFileSync( new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname, 'utf8', ) // 库存面板:手动匹配订单按钮要与编辑/作废拆开 —— 前者 fulfill(提交打到 // /fulfill、/assign-worker,后端要求 consignment.fulfill),后者 manage assert.ok( inventoryPanel.includes("resolveAdminPermission(sessionPermissions, 'consignment.fulfill')"), '库存面板必须解析 consignment.fulfill 权限', ) const buttonIndex = inventoryPanel.indexOf('手动匹配订单') assert.ok(buttonIndex > 0, '找不到「手动匹配订单」按钮') assert.ok( inventoryPanel.slice(0, buttonIndex).lastIndexOf('canFulfillConsignment ?') > 0, '「手动匹配订单」必须挂在 canFulfillConsignment 门下', ) const ordersPanel = readFileSync( new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname, 'utf8', ) // 拦截弹窗 footer 的入口同样要门控,无 fulfill 权限的人不该看到能直达 403 的按钮 const footerButtonIndex = ordersPanel.indexOf('手动指定现货匹配') assert.ok(footerButtonIndex > 0, '找不到「手动指定现货匹配」按钮') const beforeFooterButton = ordersPanel.slice(0, footerButtonIndex) assert.ok( beforeFooterButton.lastIndexOf('canFulfillConsignment ?') > beforeFooterButton.lastIndexOf('footer={['), '拦截弹窗 footer 的「手动指定现货匹配」必须按 canFulfillConsignment 门控', ) }) test('手动匹配弹窗:数据过期的 409 要清选择并刷新,无打手的现货禁止走指派', () => { const source = readFileSync( new URL('../src/components/admin/ConsignmentManualMatchModal.tsx', import.meta.url).pathname, 'utf8', ) // 并发占用/规格在打开弹窗后被改 → 清空选择并重新拉列表,不能让运营循环撞同一个错 assert.ok(source.includes('consignment_inventory_race_condition'), '要处理并发占用错误码') assert.ok(source.includes('consignment_spec_mismatch'), '要处理规格变化错误码') assert.ok(source.includes('setSelected(null)'), '数据过期时要清空已选') // 没关联打手的现货走指派必 400 consignment_worker_required:选项禁用 + 提交前再拦一次 assert.ok(source.includes('disabled: !selected.workerId'), '无打手时指派选项必须禁用') assert.ok( source.includes("deliveryKind === 'assign_worker' && !selected.workerId"), '提交前要对无打手的指派再拦一道', ) // 凭证核销反馈与关键字搜索:用共享出口 / 透传后端,不内联复制 assert.ok(source.includes("from '@/utils/voucher-consume'"), '核销反馈必须用共享出口') assert.ok(source.includes('keyword: keyword.trim() || undefined'), '库存侧关键字要透传后端') })