调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -1076,14 +1076,65 @@ test('重复扫码仅在相同值和时间窗口内成立', async () => {
|
||||
assert.equal(isDuplicateScan('qr-a', 'qr-b', 1200, 1000, 500), false)
|
||||
})
|
||||
|
||||
test('幂等键由 Web Crypto randomUUID 生成且不降级为伪随机值', async () => {
|
||||
test('幂等键优先使用 Web Crypto randomUUID', async () => {
|
||||
const palletScanPath = path.join(frontendRoot, 'src/utils/palletScan.js')
|
||||
const source = await readFile(palletScanPath, 'utf8')
|
||||
const { createIdempotencyKey } = await import('../src/utils/palletScan.js')
|
||||
let getRandomValuesCalled = false
|
||||
const expectedKey = '12345678-1234-4123-8123-123456789abc'
|
||||
|
||||
assert.match(source, /globalThis\.crypto\.randomUUID\(\)/)
|
||||
const actualKey = createIdempotencyKey({
|
||||
randomUUID: () => expectedKey,
|
||||
getRandomValues: () => {
|
||||
getRandomValuesCalled = true
|
||||
throw new Error('存在 randomUUID 时不应调用 getRandomValues')
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(actualKey, expectedKey)
|
||||
assert.equal(getRandomValuesCalled, false)
|
||||
assert.doesNotMatch(source, /Date\.now|Math\.random|new Date/)
|
||||
assert.match(createIdempotencyKey(), /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
|
||||
test('局域网 HTTP 环境缺少 randomUUID 时使用安全随机数生成 UUID v4', async () => {
|
||||
const { createIdempotencyKey } = await import('../src/utils/palletScan.js')
|
||||
const cryptoProvider = {
|
||||
getRandomValues(bytes) {
|
||||
bytes.fill(0)
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(createIdempotencyKey(cryptoProvider), '00000000-0000-4000-8000-000000000000')
|
||||
})
|
||||
|
||||
test('首次入库确认由业务幂等键保护,不被通用短时重复提交规则拦截', async () => {
|
||||
const source = await readFile(path.join(frontendRoot, 'src/api/business/inbound.js'), 'utf8')
|
||||
const confirmInboundSource = extractNamedFunction(source, 'confirmInbound')
|
||||
|
||||
assert.match(confirmInboundSource, /headers\s*:\s*\{\s*repeatSubmit\s*:\s*false\s*\}/)
|
||||
})
|
||||
|
||||
test('首次入库确认重试前清理旧错误,并明确重试会再次请求服务端', async () => {
|
||||
const source = await readFile(path.join(frontendRoot, 'src/views/business/inbound/index.vue'), 'utf8')
|
||||
const confirmSource = extractNamedFunction(source, 'handleConfirm')
|
||||
const confirmCatch = extractPromiseCallback(confirmSource, 'catch')
|
||||
const clearErrorIndex = confirmSource.indexOf("loadError.value = ''")
|
||||
|
||||
assert.ok(clearErrorIndex >= 0, '入库确认缺少旧错误状态清理')
|
||||
assert.ok(
|
||||
clearErrorIndex < confirmSource.indexOf('proxy.$modal.confirm('),
|
||||
'打开确认框前必须清理上一次的失败状态'
|
||||
)
|
||||
assert.match(confirmCatch, /再次确认将重新请求服务端,并复用本次幂等键/)
|
||||
})
|
||||
|
||||
test('首次入库确认失败后取消重试不丢弃原幂等键', async () => {
|
||||
const source = await readFile(path.join(frontendRoot, 'src/views/business/inbound/index.vue'), 'utf8')
|
||||
const confirmSource = extractNamedFunction(source, 'handleConfirm')
|
||||
const confirmCatch = extractPromiseCallback(confirmSource, 'catch')
|
||||
|
||||
assert.doesNotMatch(confirmCatch, /clearConfirmIdempotency\(\)|handleConfirmCancellation\(/)
|
||||
})
|
||||
|
||||
test('17 个 API 文件逐一匹配硬编码请求契约', async (t) => {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const dashboardPath = path.resolve(scriptDir, '../src/views/dashboard/index.vue')
|
||||
const dashboardSource = fs.readFileSync(dashboardPath, 'utf8')
|
||||
|
||||
test('首页包含仓储运营总览的核心模块', () => {
|
||||
for (const label of [
|
||||
'仓储运营总览',
|
||||
'当前库存托盘',
|
||||
'剩余包数',
|
||||
'今日入库单',
|
||||
'今日出库单',
|
||||
'待确认单据',
|
||||
'新增入库',
|
||||
'托盘填充',
|
||||
'发货出库',
|
||||
'托盘使用',
|
||||
'托盘退回',
|
||||
'托盘追踪',
|
||||
'近 7 天入库 / 出库趋势',
|
||||
'托盘流转状态分布',
|
||||
'仓库库存排行',
|
||||
'最近入库单',
|
||||
'最近出库单',
|
||||
'扫码异常',
|
||||
'待确认业务'
|
||||
]) {
|
||||
assert.match(dashboardSource, new RegExp(label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
}
|
||||
})
|
||||
|
||||
test('首页不再包含通用模板示例内容', () => {
|
||||
for (const sample of [
|
||||
'项目数',
|
||||
'团队内排名',
|
||||
'项目访问',
|
||||
'Alipay',
|
||||
'Angular',
|
||||
'Ant Design',
|
||||
'XX 指数',
|
||||
'吴彦祖'
|
||||
]) {
|
||||
assert.doesNotMatch(dashboardSource, new RegExp(sample.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
}
|
||||
})
|
||||
|
||||
test('首页使用现有业务报表接口并保留空状态策略', () => {
|
||||
for (const dependency of [
|
||||
'@/api/business/inbound',
|
||||
'@/api/business/shipment',
|
||||
'@/api/business/return',
|
||||
'@/api/report/palletInventory',
|
||||
'@/api/report/shipment'
|
||||
]) {
|
||||
assert.match(dashboardSource, new RegExp(dependency.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
}
|
||||
assert.match(dashboardSource, /暂无扫码异常记录/)
|
||||
assert.match(dashboardSource, /loadDashboardData/)
|
||||
})
|
||||
|
||||
test('首页快捷入口使用动态菜单注册的页面路径', () => {
|
||||
for (const route of [
|
||||
'/businessOperation/inbound',
|
||||
'/businessOperation/filling',
|
||||
'/businessOperation/shipment',
|
||||
'/businessOperation/usage',
|
||||
'/businessOperation/return',
|
||||
'/businessReport/palletInventory',
|
||||
'/businessReport/palletTrace',
|
||||
'/businessMaster/warehouse'
|
||||
]) {
|
||||
assert.match(dashboardSource, new RegExp(route.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user