Files
order_site/apps/frontend/tests/consignment-manual-match-mount.test.ts
T
yml2213 893388b37f feat(consignment): 寄售仓库在库列表增加通过审核入库时间并在手动匹配中补全时间展示
- 寄售仓库 (ConsignmentInventoryPanel):表格中增加「通过审核入库时间」列并格式化展示,详情弹窗规范化展示入库时间、出库时间与提报时间
- 手动匹配弹窗 (ConsignmentManualMatchModal):候选订单列表展示工单创建时间,顶部横幅与已选概要展示现货入库时间及订单创建时间;从订单匹配现货时展示现货入库时间
- 内部匹配接口与工单拦截:后端 listInventoryMatchableOrders 返回工单创建时间及平台单号;发布拦截弹窗中增加展示现货入库时间
- 验证方式:运行前后端单元测试全部通过,前端 build 校验通过
2026-10-08 15:41:23 +08:00

159 lines
7.4 KiB
TypeScript

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(/<Modal[\s>]/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)'), '数据过期时要清空已选')
// 统一内部现货直接履约:调用 fulfillWorkOrderWithConsignment,免外派佣金与二次押金
assert.ok(
source.includes('fulfillWorkOrderWithConsignment'),
'手动匹配必须调用内部现货直接履约接口',
)
assert.ok(
!source.includes('assignWorkOrderWithConsignmentWorker'),
'手动匹配不得再调用旧的指派打手接口',
)
// 凭证核销反馈与关键字搜索:用共享出口 / 透传后端,不内联复制
assert.ok(source.includes("from '@/utils/voucher-consume'"), '核销反馈必须用共享出口')
assert.ok(source.includes('keyword: keyword.trim() || undefined'), '库存侧关键字要透传后端')
})
test('发布拦截弹窗:只走内部现货履约,文案停在待加好友而不是冷静期', () => {
const source = readFileSync(
new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname,
'utf8',
)
assert.ok(source.includes('fulfillWorkOrderWithConsignment'), '拦截弹窗必须调用内部现货履约')
assert.ok(
!source.includes('assignWorkOrderWithConsignmentWorker'),
'拦截弹窗不得再调用旧的指派打手接口',
)
assert.ok(source.includes('进入待加好友'), '拦截弹窗必须写明工单进入待加好友')
assert.ok(!source.includes('自动进冷静期'), '拦截弹窗不得再写自动进冷静期')
assert.ok(!source.includes('进入赠送冷静期'), '成功提示不得再写成进入赠送冷静期')
assert.ok(source.includes('入库时间:'), '拦截弹窗现货展示必须包含入库时间')
})
test('寄售仓库库存面板:必须包含「通过审核入库时间」列,且详情弹窗正确展示时间', () => {
const source = readFileSync(
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
'utf8',
)
assert.ok(source.includes("title: '通过审核入库时间'"), '库存表格必须有「通过审核入库时间」列')
assert.ok(
source.includes('formatDateTime(record.inboundAt || record.createdAt)'),
'表格必须格式化入库时间',
)
assert.ok(source.includes('label="通过审核入库时间"'), '详情弹窗必须包含「通过审核入库时间」项')
assert.ok(
source.includes('inboundAt: manualMatchItem.inboundAt || manualMatchItem.createdAt'),
'打开手动匹配弹窗时必须透传现货的入库时间',
)
})
test('手动匹配弹窗:候选订单必须展示创建时间,现货展示审核入库时间', () => {
const source = readFileSync(
new URL('../src/components/admin/ConsignmentManualMatchModal.tsx', import.meta.url).pathname,
'utf8',
)
assert.ok(
source.includes('创建时间 {formatDateTime(order.createdAt)}'),
'候选订单卡片必须展示工单创建时间',
)
assert.ok(
source.includes('审核入库:${formatDateTime(inventoryInboundAt)}'),
'弹窗现货提示栏必须展示审核入库时间',
)
assert.ok(
source.includes('审核入库于 {formatDateTime(selected.inventoryInboundAt)}'),
'已选现货概要必须展示审核入库时间',
)
assert.ok(
source.includes('创建于 {formatDateTime(selected.workOrderCreatedAt)}'),
'已选订单概要必须展示创建时间',
)
})