127 lines
5.6 KiB
TypeScript
127 lines
5.6 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import { fileURLToPath } from 'node:url'
|
||
import test from 'node:test'
|
||
|
||
function readSource(relativePath: string): string {
|
||
return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8')
|
||
}
|
||
|
||
test('寄卖配置:每个规格字段同时提供自动/手动两套匹配方式', () => {
|
||
const source = readSource('../src/pages/admin/panels/ConsignmentConfigPanel.tsx')
|
||
|
||
assert.ok(source.includes('自动匹配方式'), '必须保留自动匹配方式下拉')
|
||
assert.ok(source.includes('手动匹配方式'), '必须新增手动匹配方式下拉')
|
||
assert.ok(source.includes("manualMatchMode: 'exact'"), '新增字段与模板生成的字段默认手动精确相等')
|
||
assert.ok(
|
||
source.includes("value={spec.manualMatchMode ?? 'exact'}"),
|
||
'历史配置没有手动口径时按精确相等展示',
|
||
)
|
||
|
||
const typeSource = readSource('../src/types/consignment.ts')
|
||
assert.ok(typeSource.includes('manualMatchMode?'), '前端类型必须包含 manualMatchMode')
|
||
})
|
||
|
||
test('寄卖配置:两套匹配方式与通配值/进物品名/支持筛选必须在同一行', () => {
|
||
const source = readSource('../src/pages/admin/panels/ConsignmentConfigPanel.tsx')
|
||
const css = readSource('../src/styles/admin.css')
|
||
|
||
assert.ok(
|
||
source.includes('order-service-field-extra is-match-modes'),
|
||
'字段行必须使用 5 列栅格类',
|
||
)
|
||
assert.ok(css.includes('.order-service-field-extra.is-match-modes'))
|
||
assert.ok(
|
||
css.includes('minmax(90px, 0.72fr)'),
|
||
'手动匹配方式必须比自动匹配方式窄,避免把通配值挤没',
|
||
)
|
||
assert.ok(
|
||
css.includes('.order-service-field-extra.is-match-modes,'),
|
||
'窄屏媒体查询必须包含该 5 列布局,保证折行',
|
||
)
|
||
})
|
||
|
||
test('后端:规格字段归一化补 manualMatchMode,历史配置缺省精确相等', () => {
|
||
const source = readSource(
|
||
'../../backend/src/services/worker-platform/consignment-spec-service.ts',
|
||
)
|
||
|
||
assert.ok(source.includes('manualMatchMode?: ConsignmentSpecMatchMode'))
|
||
assert.ok(source.includes('source.manualMatchMode ?? source.manual_match_mode'))
|
||
assert.ok(
|
||
source.includes(
|
||
"return specs.map((spec) => ({ ...spec, matchMode: spec.manualMatchMode ?? 'exact' }))",
|
||
),
|
||
'手动口径必须把 matchMode 替换成 manualMatchMode(缺省 exact)',
|
||
)
|
||
})
|
||
|
||
test('后端:手动候选、手动复检、待匹配看板都使用手动口径', () => {
|
||
const engine = readSource(
|
||
'../../backend/src/services/worker-platform/consignment-match-engine-service.ts',
|
||
)
|
||
const pending = readSource(
|
||
'../../backend/src/services/worker-platform/consignment-pending-match-service.ts',
|
||
)
|
||
|
||
assert.equal(
|
||
(engine.match(/matchScope: 'manual'/g) || []).length,
|
||
2,
|
||
'手动候选与手动复检必须传 manual 口径',
|
||
)
|
||
assert.ok(engine.includes("matchScope: 'auto'"), '自动匹配候选必须显式使用 auto 口径')
|
||
assert.ok(pending.includes("matchScope: 'manual'"), '一键查询待匹配订单必须按手动口径')
|
||
})
|
||
|
||
test('内部库存匹配弹窗:默认展示全部候选,按打分降序,并显示寄卖单号', () => {
|
||
const source = readSource('../src/components/admin/ConsignmentWorkOrderMatchModal.tsx')
|
||
|
||
assert.ok(source.includes('默认按匹配打分从高到低展示全部候选'), '默认展示口径必须在界面上说明')
|
||
assert.ok(
|
||
!source.includes("if (!searchable) return (item.matchType || 'exact') === 'exact'"),
|
||
'不能再默认只过滤精确匹配',
|
||
)
|
||
assert.ok(
|
||
source.includes('computeConsignmentMatchScore(right) - computeConsignmentMatchScore(left)'),
|
||
'列表必须按匹配打分降序排列',
|
||
)
|
||
assert.ok(
|
||
source.includes('buildCandidateHaystack(item).includes(normalizedKeyword)'),
|
||
'关键词必须覆盖全部匹配状态',
|
||
)
|
||
assert.ok(source.includes("title: '寄卖单号'"), '必须新增寄卖单号列')
|
||
assert.ok(
|
||
source.includes('item.consignmentNo || item.inventoryNo'),
|
||
'寄卖单号优先展示提报单号,无提报号回退库存号',
|
||
)
|
||
assert.ok(source.includes("item.consignmentNo || ''"), '搜索必须能命中寄卖单号')
|
||
assert.ok(!source.includes('显示不匹配项'), '「显示不匹配项」勾选框必须删除')
|
||
assert.ok(!source.includes('includeConflicts'), '不再保留 includeConflicts 状态')
|
||
assert.ok(source.includes('initialInventoryIds'), '必须支持外部预选库存')
|
||
assert.ok(source.includes('if (presetInventoryKey)'), '预选库存必须优先于默认自动勾选')
|
||
})
|
||
|
||
test('手动匹配弹窗:状态不允许时仍能搜索,但禁止提交', () => {
|
||
const source = readSource('../src/components/admin/ConsignmentWorkOrderMatchModal.tsx')
|
||
|
||
assert.ok(source.includes('const matchable = data?.matchable !== false'))
|
||
assert.ok(source.includes('当前仍可搜索和核对库存'), '只读状态必须给出明确提示')
|
||
assert.ok(source.includes('disabled={!canSubmit}'))
|
||
assert.ok(
|
||
/matchable &&\s+selectedIds\.length > 0/.test(source),
|
||
'canSubmit 必须受 matchable 约束',
|
||
)
|
||
|
||
const searchIndex = source.indexOf('搜索寄卖单号 / 库存号 / 物品 / 规格 / 打手')
|
||
const optionsBranchIndex = source.indexOf('visibleOptions.length > 0 ? (')
|
||
assert.ok(searchIndex > 0)
|
||
assert.ok(
|
||
searchIndex < optionsBranchIndex,
|
||
'搜索框必须常驻在候选分支之外,空列表 / 只读状态也能使用',
|
||
)
|
||
|
||
const typeSource = readSource('../src/types/consignment.ts')
|
||
assert.ok(typeSource.includes('matchable: boolean'), '候选响应类型必须带 matchable')
|
||
assert.ok(typeSource.includes('blockedReason: string'))
|
||
})
|