756 lines
43 KiB
JavaScript
756 lines
43 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { readFile } from 'node:fs/promises'
|
||
import { webcrypto } from 'node:crypto'
|
||
import { test } from 'node:test'
|
||
import * as Vue from 'vue'
|
||
import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
|
||
|
||
// 执行真实 setup、模板和 API,仅替换网络、浏览器存储及组件宿主边界。
|
||
const read = path => readFile(new URL(`../src/${path}`, import.meta.url), 'utf8')
|
||
const { descriptor } = parse(await read('views/business/usage/index.vue'))
|
||
const script = compileScript(descriptor, { id: 'pc-usage-test', genDefaultAs: '__component' })
|
||
const template = compileTemplate({ source: descriptor.template.content, filename: 'usage/index.vue', id: 'pc-usage-test', compilerOptions: { bindingMetadata: script.bindings } })
|
||
assert.deepEqual(template.errors, [])
|
||
const apiSource = await read('api/business/usage.js')
|
||
const scanUtils = await import(`data:text/javascript,${encodeURIComponent(await read('utils/palletScan.js'))}`)
|
||
const bindImports = source => source.replace(/import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"];?/g, (_, names, module) =>
|
||
`const ${names.startsWith('{') ? names.replace(/\s+as\s+/g, ': ') : `{ default: ${names} }`} = __modules[${JSON.stringify(module)}];`)
|
||
const flush = async () => { await new Promise(resolve => setImmediate(resolve)); await Vue.nextTick() }
|
||
function deferred() {
|
||
let resolve, reject
|
||
const promise = new Promise((yes, no) => { resolve = yes; reject = no })
|
||
return { promise, resolve, reject }
|
||
}
|
||
const tunnels = [{ id: 31, name: '一号隧道', code: 'SD-01' }]
|
||
const lines = [{ id: 41, name: '一号产线', code: 'CX-01' }]
|
||
const pallet = (overrides = {}) => ({ palletId: 11, palletCode: '托盘-001', qrToken: 'QR-001', siteId: 21, strainId: 1, strainName: '菌种A', batchNo: 'B-01', tunnelId: null, currentLineId: null, remainingPackages: 100, flowState: 'AT_SITE_UNUSED', productionDate: null, expiryDate: null, ...overrides })
|
||
const boundPallet = (overrides = {}) => pallet({ tunnelId: 31, tunnelName: '一号隧道', tunnelCode: 'SD-01', traceNo: 'INTERNAL-TRACE', ...overrides })
|
||
const renderer = Vue.createRenderer({ createElement: () => ({}), createText: () => ({}), createComment: () => ({}), insert() {}, remove() {}, setText() {}, setElementText() {}, patchProp() {}, parentNode: () => null, nextSibling: () => null })
|
||
function flatten(vnode) {
|
||
if (Array.isArray(vnode)) return vnode.flatMap(flatten)
|
||
if (!vnode || typeof vnode !== 'object') return []
|
||
const children = vnode.children
|
||
const slots = children && !Array.isArray(children) && typeof children === 'object'
|
||
? Object.entries(children).filter(([key, value]) => !key.startsWith('_') && typeof value === 'function').flatMap(([, slot]) => flatten(slot({ row: {} }))) : []
|
||
return [vnode, ...flatten(children), ...slots]
|
||
}
|
||
function createPage({ scan = () => pallet(), request: override, storage = new Map(), storageWriteError = false, storageRemoveError = false, confirm = () => Promise.resolve(), canOperate = true, userId = 7, batchId = '' } = {}) {
|
||
const requests = [], messages = [], confirmations = [], successMessages = [], hooks = { onActivated: [], onDeactivated: [], onBeforeUnmount: [] }
|
||
const user = Vue.reactive({ id: userId, admin: false }), batch = Vue.reactive({ currentBatchId: batchId, scopeVersion: 0 })
|
||
const request = config => {
|
||
requests.push(structuredClone(config))
|
||
const custom = override?.(config)
|
||
if (custom !== undefined) return custom
|
||
if (config.url.includes('/scan/')) return Promise.resolve({ data: scan(decodeURIComponent(config.url.split('/scan/')[1])) })
|
||
if (config.url.endsWith('/tunnel-options')) return Promise.resolve({ data: tunnels })
|
||
if (config.url.endsWith('/line-options')) return Promise.resolve({ data: lines })
|
||
if (config.url.endsWith('/recommendation')) return Promise.resolve({ data: { recommended: null, unknownArrivalCount: 0, message: '请按实际接收时间先到先用' } })
|
||
if (config.url.endsWith('/list')) return Promise.resolve({ rows: [], total: 0 })
|
||
if (config.url.endsWith('/enter-tunnel')) return Promise.resolve({ data: { palletId: 11, tunnelId: config.data.tunnelId, traceNo: 'INTERNAL-TRACE' } })
|
||
if (config.url === '/business/usage') return Promise.resolve({ data: { usageId: 71, remainingPackages: 83, flowState: 'AT_SITE_PARTIAL' } })
|
||
throw new Error(`未处理请求 ${config.url}`)
|
||
}
|
||
const names = [...apiSource.matchAll(/export function (\w+)/g)].map(match => match[1])
|
||
const api = new Function('__modules', `${bindImports(apiSource).replace(/export function /g, 'function ')}\nreturn { ${names.join(',')} };`)({ '@/utils/request': { default: request } })
|
||
const localStorage = {
|
||
getItem: key => storage.get(key) ?? null,
|
||
setItem(key, value) { if (storageWriteError) throw new Error('存储不可用'); storage.set(key, value) },
|
||
removeItem(key) { if (storageRemoveError) throw new Error('存储清理失败'); storage.delete(key) }
|
||
}
|
||
const proxy = { $auth: { hasPermiAnd: () => canOperate }, $modal: {
|
||
msgSuccess(msg) { successMessages.push(msg); messages.push(msg) },
|
||
msgWarning: msg => messages.push(msg),
|
||
confirm(message) { confirmations.push(message); return confirm(message) }
|
||
}, resetForm() {}, $refs: { usageRef: { validate: callback => callback(true) } } }
|
||
const vue = { ...Vue, getCurrentInstance: () => ({ proxy }), ...Object.fromEntries(Object.keys(hooks).map(name => [name, fn => hooks[name].push(fn)])), resolveComponent: name => ({ name }), resolveDirective: () => ({}) }
|
||
const modules = { vue, '@/components/PalletScanInput': { default: { name: 'PalletScanInput' } }, '@/components/TestBatchAdoptionDialog': { default: { name: 'TestBatchAdoptionDialog' } }, '@/store/modules/user': { default: () => user }, '@/store/modules/testBatch': { default: () => batch }, '@/api/business/usage': api, '@/utils/palletScan': { ...scanUtils, createIdempotencyKey: () => scanUtils.createIdempotencyKey(webcrypto) } }
|
||
// PC 的 Vue API 由 Vite 自动导入;测试以相同名字提供真实响应式 API。
|
||
const globals = ['ref', 'reactive', 'computed', 'toRefs', 'watch', 'getCurrentInstance', 'onActivated', 'onDeactivated', 'onBeforeUnmount']
|
||
const component = new Function('__modules', 'localStorage', ...globals, `${bindImports(script.content)}\nreturn __component;`)(modules, localStorage, ...globals.map(name => vue[name]))
|
||
const scope = Vue.effectScope(), state = scope.run(() => component.setup({}, { expose() {} }))
|
||
const render = new Function('__modules', `${bindImports(template.code).replace('export function render', 'function render')}\nreturn render;`)(modules)
|
||
return {
|
||
state, api, requests, messages, confirmations, successMessages, storage, user, batch,
|
||
post: (url = '/business/usage') => requests.filter(item => item.method === 'post' && item.url === url),
|
||
async scan(value = 'QR-001') { state.handleUsageScan(value); await flush() },
|
||
async act(name) { state[name](); await flush() },
|
||
async lifecycle(name) { hooks[name].forEach(fn => fn()); await flush() },
|
||
nodes() {
|
||
let nodes
|
||
const app = renderer.createApp({ render() { nodes = flatten(render({ parseTime: value => value }, [], {}, Vue.proxyRefs(state), {}, {})); return null } })
|
||
app.mount({}); app.unmount()
|
||
return nodes
|
||
},
|
||
close() { hooks.onBeforeUnmount.forEach(fn => fn()); scope.stop() }
|
||
}
|
||
}
|
||
const text = page => page.nodes().map(node => [typeof node.children === 'string' ? node.children : '', node.props?.title || '', node.props?.label || ''].join(' ')).join(' ')
|
||
const button = (page, label) => page.nodes().find(node => node.type?.name === 'el-button' && flatten(node).some(child => typeof child.children === 'string' && child.children.includes(label)))
|
||
async function retry(page) {
|
||
const entry = button(page, '恢复重试')
|
||
assert.ok(entry, '必须提供无需重新扫码的恢复重试入口')
|
||
entry.props.onClick(); await flush()
|
||
}
|
||
|
||
test('扫码 API 编码路径,FIFO 使用独立 GET 与规范 qrToken', async () => {
|
||
const page = createPage()
|
||
await page.api.scanUsagePallet('托盘/A?#')
|
||
assert.equal(page.requests.at(-1).url, '/business/usage/scan/%E6%89%98%E7%9B%98%2FA%3F%23')
|
||
await page.scan('印刷托盘号')
|
||
assert.deepEqual(page.requests.find(item => item.url.endsWith('/recommendation')), { url: '/business/usage-fifo/recommendation', method: 'get', params: { qrToken: 'QR-001' } })
|
||
page.close()
|
||
})
|
||
|
||
for (const failure of ['reject', 'pending']) test(`可选产线 ${failure} 不丢失或阻塞隧道成功结果`, async () => {
|
||
const slow = deferred()
|
||
const page = createPage({ request: config => config.url.endsWith('/line-options') ? (failure === 'reject' ? Promise.reject(new Error('产线不可用')) : slow.promise) : undefined })
|
||
await page.scan()
|
||
assert.deepEqual(page.state.tunnelOptions.value, tunnels)
|
||
assert.equal(page.state.optionLoading.value, false)
|
||
if (failure === 'reject') assert.match(text(page), /产线.*失败|产线.*不可用/)
|
||
page.state.form.value.tunnelId = 31
|
||
await page.act('enterTunnel')
|
||
page.state.form.value.usedPackages = 10
|
||
await page.act('submitUsage')
|
||
assert.equal(page.post().length, 1)
|
||
assert.equal(page.post()[0].data.lineId ?? null, null)
|
||
page.close(); slow.resolve({ data: [] }); await flush()
|
||
})
|
||
|
||
test('摘要展示历史日期未登记、真实日期及隧道名称编号,普通页无内部溯源输入列', async () => {
|
||
const page = createPage({ scan: () => boundPallet() })
|
||
await page.scan()
|
||
assert.match(text(page), /生产日期.*未登记/)
|
||
assert.match(text(page), /有效期.*未登记/)
|
||
assert.match(text(page), /一号隧道/)
|
||
assert.match(text(page), /SD-01/)
|
||
assert.doesNotMatch(text(page), /INTERNAL-TRACE|溯源号/)
|
||
assert.equal(page.nodes().filter(node => node.props?.prop === 'traceNo').length, 0)
|
||
page.close()
|
||
const dated = createPage({ scan: () => boundPallet({ productionDate: '2026-10-01', expiryDate: '2027-04-01' }) })
|
||
await dated.scan()
|
||
assert.match(text(dated), /2026-10-01/); assert.match(text(dated), /2027-04-01/)
|
||
dated.close()
|
||
})
|
||
|
||
test('新入隧后摘要使用选项的名称与编号', async () => {
|
||
const page = createPage()
|
||
await page.scan(); page.state.form.value.tunnelId = 31
|
||
await page.act('enterTunnel')
|
||
assert.match(text(page), /一号隧道/); assert.match(text(page), /SD-01/)
|
||
page.close()
|
||
})
|
||
|
||
test('已入隧扫码仅返回 ID 时从选项补齐隧道名称编号', async () => {
|
||
const page = createPage({ scan: () => boundPallet({ tunnelName: undefined, tunnelCode: undefined }) })
|
||
await page.scan()
|
||
assert.match(text(page), /一号隧道/); assert.match(text(page), /SD-01/)
|
||
assert.equal(page.state.tunnelLocked.value, true)
|
||
page.close()
|
||
})
|
||
|
||
test('入隧成功保留已填写的可选产线及包数,尚未使用时产线仍可调整', async () => {
|
||
const page = createPage()
|
||
await page.scan()
|
||
page.state.form.value.tunnelId = 31
|
||
page.state.form.value.lineId = 41
|
||
page.state.form.value.usedPackages = 10
|
||
await page.act('enterTunnel')
|
||
assert.equal(page.state.form.value.lineId, 41)
|
||
assert.equal(page.state.form.value.usedPackages, 10)
|
||
assert.equal(page.state.lineLocked.value, false)
|
||
await page.act('submitUsage')
|
||
assert.equal(page.post()[0].data.lineId, 41)
|
||
page.close()
|
||
})
|
||
|
||
for (const kind of ['entry', 'usage']) test(`${kind} 缺少确认结果时保留原凭据,不把空响应当作成功`, async () => {
|
||
const page = createPage({ scan: () => kind === 'usage' ? boundPallet() : pallet(), request: config => config.method === 'post' ? Promise.resolve({ data: {} }) : undefined })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; page.state.form.value.usedPackages = 10
|
||
await page.act(kind === 'usage' ? 'submitUsage' : 'enterTunnel')
|
||
assert.ok(button(page, '恢复重试'))
|
||
assert.deepEqual(page.messages, [])
|
||
assert.equal(page.state.usageResult.value, null)
|
||
page.close()
|
||
})
|
||
|
||
test('FIFO 展示服务端提示及推荐托盘,未知到货时间不能被当作排序依据', async () => {
|
||
const page = createPage({ request: config => config.url.endsWith('/recommendation') ? Promise.resolve({ data: { recommended: { palletId: 9, palletCode: '较早托盘', batchNo: '较早批次', remainingPackages: 25, siteArrivedAt: '2026-10-01 09:00:00' }, unknownArrivalCount: 2, message: '另有到货时间未登记的物料,请人工核对' } }) : undefined })
|
||
await page.scan()
|
||
assert.match(text(page), /另有到货时间未登记的物料,请人工核对/)
|
||
assert.match(text(page), /较早托盘/); assert.match(text(page), /较早批次/)
|
||
page.close()
|
||
})
|
||
|
||
for (const outcome of ['pending', 'reject']) test(`FIFO ${outcome} 不阻止入隧及实际使用`, async () => {
|
||
const slow = deferred()
|
||
const page = createPage({ request: config => config.url.endsWith('/recommendation') ? (outcome === 'reject' ? Promise.reject(new Error('FIFO 失败')) : slow.promise) : undefined })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; await flush()
|
||
if (outcome === 'reject') assert.match(text(page), /先到先用.*人工核对|FIFO.*人工核对/)
|
||
await page.act('enterTunnel'); page.state.form.value.usedPackages = 10
|
||
await page.act('submitUsage')
|
||
assert.equal(page.post().length, 1)
|
||
assert.equal(page.state.usageResult.value.remainingPackages, 83)
|
||
page.close(); slow.resolve({ data: { recommended: null, message: '晚到提示' } }); await flush()
|
||
})
|
||
|
||
for (const outcome of ['resolve', 'reject']) test(`旧 FIFO ${outcome} 不污染重扫及离页后的状态`, async () => {
|
||
const slow = deferred(); let count = 0
|
||
const page = createPage({ request: config => config.url.endsWith('/recommendation') && ++count === 1 ? slow.promise : undefined })
|
||
await page.scan(); await page.act('resetUsageOperation'); await page.scan('NEW')
|
||
assert.match(text(page), /请按实际接收时间先到先用/)
|
||
if (outcome === 'resolve') slow.resolve({ data: { recommended: null, message: 'OLD-FIFO' } })
|
||
else slow.reject(new Error('OLD-FIFO'))
|
||
await flush()
|
||
assert.match(text(page), /请按实际接收时间先到先用/)
|
||
assert.doesNotMatch(text(page), /OLD-FIFO|建议获取失败/)
|
||
page.close()
|
||
})
|
||
|
||
for (const amount of [10, 60, 100]) test(`扣减 ${amount} 包后响应丢失:清空、重扫、重建页面只重放原 payload/key`, async () => {
|
||
const storage = new Map(), applied = new Map(); let remaining = 100, deductions = 0
|
||
const request = config => {
|
||
if (config.url !== '/business/usage') return
|
||
if (applied.has(config.data.idempotencyKey)) return Promise.resolve(applied.get(config.data.idempotencyKey))
|
||
deductions++; remaining -= config.data.usedPackages
|
||
applied.set(config.data.idempotencyKey, { data: { usageId: 71, remainingPackages: remaining, flowState: remaining ? 'AT_SITE_PARTIAL' : 'AT_SITE_EMPTY' } })
|
||
return Promise.reject(new Error('提交成功后响应丢失'))
|
||
}
|
||
const first = createPage({ storage, request, scan: () => boundPallet() })
|
||
await first.scan(); first.state.form.value.usedPackages = amount
|
||
await first.act('submitUsage')
|
||
const original = first.post()[0].data
|
||
await first.act('resetUsageOperation'); await first.scan()
|
||
first.state.form.value.usedPackages = amount + 1
|
||
await first.act('submitUsage')
|
||
assert.equal(first.post().length, 1, '未确认操作不可用新键再次扣减')
|
||
first.close()
|
||
const second = createPage({ storage, request, scan: () => boundPallet({ remainingPackages: remaining }) })
|
||
assert.match(text(second), /托盘-001/)
|
||
await retry(second)
|
||
assert.deepEqual(second.post()[0].data, original)
|
||
assert.equal(deductions, 1)
|
||
assert.equal(second.state.usageResult.value.remainingPackages, 100 - amount)
|
||
assert.equal(button(second, '恢复重试'), undefined)
|
||
second.close()
|
||
})
|
||
|
||
test('未知入隧结果同样持久化,并保持独立于使用的幂等键', async () => {
|
||
const storage = new Map()
|
||
const first = createPage({ storage, request: config => config.url.endsWith('/enter-tunnel') ? Promise.reject(new Error('响应丢失')) : undefined })
|
||
await first.scan(); first.state.form.value.tunnelId = 31
|
||
await first.act('enterTunnel')
|
||
const original = first.post('/business/usage/enter-tunnel')[0].data
|
||
first.state.form.value.tunnelId = 32
|
||
await first.act('enterTunnel')
|
||
assert.equal(first.post('/business/usage/enter-tunnel').length, 1)
|
||
first.close()
|
||
const second = createPage({ storage })
|
||
await retry(second)
|
||
assert.deepEqual(second.post('/business/usage/enter-tunnel')[0].data, original)
|
||
assert.equal(second.state.tunnelLocked.value, true)
|
||
second.state.form.value.usedPackages = 10
|
||
await second.act('submitUsage')
|
||
assert.notEqual(second.post()[0].data.idempotencyKey, original.idempotencyKey)
|
||
second.close()
|
||
})
|
||
|
||
test('恢复记录按账号和 test batch 隔离,切回原范围可恢复', async () => {
|
||
const storage = new Map()
|
||
const first = createPage({ storage, scan: () => boundPallet(), request: config => config.url === '/business/usage' ? Promise.reject(new Error('响应丢失')) : undefined })
|
||
await first.scan(); first.state.form.value.usedPackages = 10; await first.act('submitUsage'); first.close()
|
||
for (const identity of [{ userId: 8, batchId: '' }, { userId: 7, batchId: '99' }]) {
|
||
const other = createPage({ storage, ...identity })
|
||
assert.equal(button(other, '恢复重试'), undefined)
|
||
other.close()
|
||
}
|
||
const page = createPage({ storage })
|
||
assert.ok(button(page, '恢复重试'))
|
||
page.batch.currentBatchId = '99'; await flush()
|
||
assert.equal(button(page, '恢复重试'), undefined)
|
||
page.batch.currentBatchId = ''; await flush()
|
||
await retry(page); assert.equal(page.post().length, 1)
|
||
page.close()
|
||
})
|
||
|
||
for (const outcome of ['resolve', 'reject']) test(`离页后旧写请求 ${outcome} 不能污染重建状态或换掉恢复键`, async () => {
|
||
const slow = deferred(), storage = new Map()
|
||
const page = createPage({ storage, scan: () => boundPallet(), request: config => config.url === '/business/usage' ? slow.promise : undefined })
|
||
await page.scan(); page.state.form.value.usedPackages = 10
|
||
page.state.submitUsage(); await flush()
|
||
await page.lifecycle('onDeactivated'); await page.lifecycle('onActivated')
|
||
if (outcome === 'resolve') slow.resolve({ data: { remainingPackages: 90, flowState: 'AT_SITE_PARTIAL' } })
|
||
else slow.reject(new Error('OLD-ERROR'))
|
||
await flush()
|
||
assert.equal(page.state.usageResult.value, null)
|
||
assert.deepEqual(page.messages, [])
|
||
assert.ok(button(page, '恢复重试'))
|
||
page.close()
|
||
})
|
||
|
||
test('恢复凭据写入失败时不能先扣减', async () => {
|
||
const page = createPage({ storageWriteError: true, scan: () => boundPallet() })
|
||
await page.scan(); page.state.form.value.usedPackages = 10; await page.act('submitUsage')
|
||
assert.equal(page.post().length, 0)
|
||
assert.match(text(page), /存储/)
|
||
page.close()
|
||
})
|
||
|
||
test('恢复记录损坏时提示人工核对且不静默覆盖', async () => {
|
||
const storage = new Map()
|
||
const first = createPage({ storage, scan: () => boundPallet(), request: config => config.url === '/business/usage' ? Promise.reject(new Error('响应丢失')) : undefined })
|
||
await first.scan(); first.state.form.value.usedPackages = 10; await first.act('submitUsage'); first.close()
|
||
const key = [...storage.keys()][0]
|
||
storage.set(key, '损坏记录')
|
||
const second = createPage({ storage })
|
||
await second.scan(); await second.act('submitUsage')
|
||
assert.match(text(second), /损坏.*人工核对/)
|
||
assert.equal(second.post().length, 0)
|
||
assert.equal(storage.get(key), '损坏记录')
|
||
second.close()
|
||
})
|
||
|
||
for (const endpoint of ['/business/usage/scan/QR-001', '/business/usage/tunnel-options', '/business/usage/line-options', '/business/usage-fifo/recommendation', '/business/usage/list']) {
|
||
test(`${endpoint} 离页后晚到的读响应不污染新会话`, async () => {
|
||
const slow = deferred(); let count = 0
|
||
const page = createPage({ request: config => config.url === endpoint && ++count === 1 ? slow.promise : undefined })
|
||
await page.scan(); await page.lifecycle('onDeactivated'); await page.lifecycle('onActivated')
|
||
await page.scan('NEW')
|
||
const before = text(page)
|
||
slow.resolve({ rows: [{ usageId: 'OLD' }], total: 99, data: endpoint.includes('/scan/') ? pallet({ palletCode: 'OLD', palletId: 99 }) : endpoint.endsWith('-options') ? [{ id: 99, name: 'OLD' }] : { message: 'OLD', recommended: null } })
|
||
await flush()
|
||
assert.equal(text(page), before)
|
||
assert.equal(page.state.total.value, 0)
|
||
page.close()
|
||
})
|
||
}
|
||
|
||
test('既有权限、入隧顺序、包数校验与成功后防重复继续生效', async () => {
|
||
const denied = createPage({ canOperate: false })
|
||
await denied.scan(); denied.state.form.value.tunnelId = 31
|
||
await denied.act('enterTunnel'); await denied.act('submitUsage')
|
||
assert.equal(denied.requests.filter(item => item.method === 'post').length, 0)
|
||
denied.close()
|
||
const page = createPage()
|
||
await page.scan(); page.state.form.value.tunnelId = 31; page.state.form.value.usedPackages = 10
|
||
await page.act('submitUsage'); assert.equal(page.post().length, 0)
|
||
await page.act('enterTunnel')
|
||
for (const value of [undefined, 0, -1, 1.5, 101]) {
|
||
page.state.form.value.usedPackages = value; await page.act('submitUsage')
|
||
}
|
||
assert.equal(page.post().length, 0)
|
||
page.state.form.value.usedPackages = 10; await page.act('submitUsage'); await page.act('submitUsage')
|
||
assert.equal(page.post().length, 1)
|
||
assert.equal(page.state.usageResult.value.remainingPackages, 83)
|
||
page.close()
|
||
})
|
||
|
||
test('已绑定产线保持锁定并提交原产线,余量状态完全采用后端结果', async () => {
|
||
const page = createPage({ scan: () => boundPallet({ currentLineId: 41 }) })
|
||
await page.scan()
|
||
assert.equal(page.state.lineLocked.value, true)
|
||
assert.equal(page.state.form.value.lineId, 41)
|
||
const lineSelect = page.nodes().find(node => node.type?.name === 'el-select' && node.props?.modelValue === 41)
|
||
assert.equal(lineSelect.props.disabled, true)
|
||
page.state.form.value.usedPackages = 10
|
||
await page.act('submitUsage')
|
||
assert.equal(page.post()[0].data.lineId, 41)
|
||
assert.deepEqual(page.state.usageResult.value, { remainingPackages: 83, flowState: 'AT_SITE_PARTIAL' })
|
||
page.close()
|
||
})
|
||
|
||
async function seedUnknown(storage, identity = {}, kind = 'usage') {
|
||
const page = createPage({ storage, ...identity, scan: () => kind === 'usage' ? boundPallet() : pallet(), request: config => config.method === 'post' ? Promise.reject(new Error('后端持续拒绝')) : undefined })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; page.state.form.value.usedPackages = 10
|
||
await page.act(kind === 'usage' ? 'submitUsage' : 'enterTunnel')
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 1)
|
||
page.close()
|
||
}
|
||
function releaseButton(page) {
|
||
const entry = button(page, '人工核对后解除待确认')
|
||
assert.ok(entry, '持续拒绝时必须有二次确认的人工核对释放入口')
|
||
return entry
|
||
}
|
||
|
||
for (const cancel of ['cancel', 'close']) test(`人工释放 ${cancel} 不清理记录且不发送业务请求`, async () => {
|
||
const storage = new Map()
|
||
await seedUnknown(storage)
|
||
const before = [...storage.entries()]
|
||
const page = createPage({ storage, confirm: () => Promise.reject(cancel) })
|
||
releaseButton(page).props.onClick(); await flush()
|
||
assert.deepEqual([...storage.entries()], before)
|
||
assert.ok(button(page, '恢复重试'))
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
assert.deepEqual(page.messages, [])
|
||
page.close()
|
||
})
|
||
|
||
for (const kind of ['entry', 'usage']) test(`人工确认只释放 ${kind} 本机待确认记录,不重放或宣称成功`, async () => {
|
||
const storage = new Map(), confirmation = deferred()
|
||
await seedUnknown(storage, {}, kind)
|
||
const before = [...storage.entries()]
|
||
const page = createPage({ storage, confirm: () => confirmation.promise })
|
||
releaseButton(page).props.onClick(); await flush()
|
||
assert.deepEqual([...storage.entries()], before, '弹窗出现时不能提前清理')
|
||
assert.match(page.confirmations[0], /使用记录/)
|
||
assert.match(page.confirmations[0], /库存/)
|
||
assert.match(page.confirmations[0], /重复.*扣减|重复.*提交/)
|
||
assert.match(page.confirmations[0], /本机/)
|
||
assert.match(page.confirmations[0], /托盘-001/)
|
||
assert.equal(button(page, '恢复重试').props.disabled, true)
|
||
await page.act('retryPendingOperation')
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
confirmation.resolve(); await flush()
|
||
assert.equal(storage.size, 0)
|
||
assert.equal(page.state.pendingOperation.value, null)
|
||
assert.equal(page.state.scannedPallet.value, null, '必须重新扫描核对当前库存,不能沿用旧快照')
|
||
assert.equal(page.state.form.value.qrToken, '')
|
||
assert.equal(page.state.usageResult.value, null)
|
||
assert.equal(page.state.recoveryRequired.value, false)
|
||
assert.equal(button(page, '恢复重试'), undefined)
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
assert.deepEqual(page.successMessages, [])
|
||
assert.match(page.messages.join(' '), /仅.*本机/)
|
||
page.close()
|
||
const rebuilt = createPage({ storage })
|
||
assert.equal(button(rebuilt, '恢复重试'), undefined)
|
||
rebuilt.close()
|
||
})
|
||
|
||
test('人工释放只影响当前账号及测试批次,新增权限撤销后仍可清理本机记录', async () => {
|
||
const storage = new Map()
|
||
for (const identity of [{}, { userId: 8 }, { batchId: '99' }]) await seedUnknown(storage, identity)
|
||
const page = createPage({ storage, canOperate: false })
|
||
releaseButton(page).props.onClick(); await flush()
|
||
assert.equal(storage.size, 2)
|
||
assert.equal(button(page, '恢复重试'), undefined)
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
page.close()
|
||
for (const identity of [{ userId: 8 }, { batchId: '99' }]) {
|
||
const other = createPage({ storage, ...identity })
|
||
assert.ok(button(other, '恢复重试'))
|
||
other.close()
|
||
}
|
||
})
|
||
|
||
for (const change of ['user', 'batch', 'page', 'record', 'removed']) test(`人工确认期间 ${change} 变化后旧确认不能删除记录`, async () => {
|
||
const storage = new Map(), confirmation = deferred()
|
||
await seedUnknown(storage)
|
||
const page = createPage({ storage, confirm: () => confirmation.promise })
|
||
releaseButton(page).props.onClick(); await flush()
|
||
if (change === 'user') page.user.id = 8
|
||
if (change === 'batch') page.batch.currentBatchId = '99'
|
||
if (change === 'page') await page.lifecycle('onDeactivated')
|
||
if (change === 'record') {
|
||
const [key, raw] = [...storage.entries()][0]
|
||
const changed = JSON.parse(raw)
|
||
changed.payload.idempotencyKey = 'OTHER-OPERATION-KEY'
|
||
storage.set(key, JSON.stringify(changed))
|
||
}
|
||
if (change === 'removed') storage.delete([...storage.keys()][0])
|
||
const before = [...storage.entries()]
|
||
confirmation.resolve(); await flush()
|
||
assert.deepEqual([...storage.entries()], before)
|
||
if (change === 'record' || change === 'removed') assert.equal(page.state.recoveryRequired.value, true, '记录发生变化时不能把旧确认当作有效释放')
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
assert.deepEqual(page.successMessages, [])
|
||
page.close()
|
||
})
|
||
|
||
test('人工释放遇到本机清理失败时保留凭据并显示错误', async () => {
|
||
const storage = new Map()
|
||
await seedUnknown(storage)
|
||
const before = [...storage.entries()]
|
||
const page = createPage({ storage, storageRemoveError: true })
|
||
releaseButton(page).props.onClick(); await flush()
|
||
assert.deepEqual([...storage.entries()], before)
|
||
assert.ok(button(page, '恢复重试'))
|
||
assert.match(text(page), /清理.*失败|解除.*失败/)
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
assert.deepEqual(page.successMessages, [])
|
||
page.close()
|
||
})
|
||
|
||
const fifoData = (requiresSkipReason = true) => ({ recommended: null, unknownArrivalCount: 0, requiresSkipReason, message: '请按实际到货时间核对' })
|
||
const fifoRejection = () => Object.assign(new Error('服务端拒绝'), { response: { data: { code: 500, data: 'FIFO_SKIP_REASON_REQUIRED', msg: '请填写跳过原因' } } })
|
||
const fifoRequests = page => page.requests.filter(item => item.url.endsWith('/recommendation'))
|
||
|
||
for (const kind of ['entry', 'usage']) {
|
||
const action = kind === 'entry' ? 'enterTunnel' : 'submitUsage'
|
||
const endpoint = kind === 'entry' ? '/business/usage/enter-tunnel' : '/business/usage'
|
||
for (const reason of [' \n ', '字'.repeat(501)]) test(`FIFO ${kind} 空白或超长原因不发送且不持久化(${reason.length})`, async () => {
|
||
const page = createPage({ scan: () => kind === 'entry' ? pallet() : boundPallet(), request: config => config.url.endsWith('/recommendation') ? Promise.resolve({ data: fifoData() }) : undefined })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; await flush()
|
||
page.state.form.value.usedPackages = 10; page.state.form.value.fifoSkipReason = reason
|
||
await page.act(action)
|
||
assert.equal(page.post(endpoint).length, 0)
|
||
assert.equal(page.storage.size, 0)
|
||
assert.match(text(page), /原因|500/)
|
||
page.close()
|
||
})
|
||
|
||
test(`FIFO ${kind} trim 原因持久化,修改原因不能绕过待确认,重建后重放原原因及键`, async () => {
|
||
const storage = new Map()
|
||
const first = createPage({ storage, scan: () => kind === 'entry' ? pallet() : boundPallet(), request: config => config.url === endpoint ? Promise.reject(new Error('网络中断')) : undefined })
|
||
await first.scan(); first.state.form.value.tunnelId = 31; first.state.form.value.usedPackages = 10
|
||
first.state.form.value.fifoSkipReason = ' 先到托盘正在检验 \n'
|
||
await first.act(action)
|
||
const original = first.post(endpoint)[0].data
|
||
assert.equal(original.fifoSkipReason, '先到托盘正在检验')
|
||
assert.equal(JSON.parse([...storage.values()][0]).payload.fifoSkipReason, '先到托盘正在检验')
|
||
first.state.form.value.fifoSkipReason = '换了原因'; await first.act(action)
|
||
assert.equal(first.post(endpoint).length, 1)
|
||
first.close()
|
||
const second = createPage({ storage })
|
||
await retry(second)
|
||
assert.deepEqual(second.post(endpoint)[0].data, original)
|
||
assert.equal(second.state.form.value.fifoSkipReason, '先到托盘正在检验')
|
||
second.close()
|
||
})
|
||
|
||
test(`FIFO ${kind} 明确拒绝解除原凭据并刷新建议,补原因后新键提交`, async () => {
|
||
let rejected = false
|
||
const page = createPage({ scan: () => kind === 'entry' ? pallet() : boundPallet(), request: config => {
|
||
if (config.url === endpoint && !rejected) { rejected = true; return Promise.reject(fifoRejection()) }
|
||
if (config.url.endsWith('/recommendation')) return Promise.resolve({ data: fifoData(rejected) })
|
||
} })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; await flush(); page.state.form.value.usedPackages = 10
|
||
const before = fifoRequests(page).length
|
||
await page.act(action)
|
||
const original = page.post(endpoint)[0].data
|
||
assert.equal(page.storage.size, 0)
|
||
assert.equal(page.state.recoveryRequired.value, false)
|
||
assert.match(text(page), /原因/)
|
||
assert.ok(fifoRequests(page).length > before)
|
||
page.state.form.value.fifoSkipReason = ' 质量待核实 '
|
||
await page.act(action)
|
||
assert.equal(page.post(endpoint)[1].data.fifoSkipReason, '质量待核实')
|
||
assert.notEqual(page.post(endpoint)[1].data.idempotencyKey, original.idempotencyKey)
|
||
page.close()
|
||
})
|
||
}
|
||
|
||
test('FIFO 清理失败保留待确认,恢复重试收到明确拒绝后可编辑', async () => {
|
||
const storage = new Map(); await seedUnknown(storage)
|
||
const before = [...storage.entries()]
|
||
const blocked = createPage({ storage, storageRemoveError: true, request: config => config.method === 'post' ? Promise.reject(fifoRejection()) : undefined })
|
||
await retry(blocked)
|
||
assert.deepEqual([...storage.entries()], before)
|
||
assert.ok(button(blocked, '恢复重试'))
|
||
blocked.close()
|
||
const page = createPage({ storage, request: config => config.method === 'post' ? Promise.reject(fifoRejection()) : undefined })
|
||
await retry(page)
|
||
assert.equal(storage.size, 0)
|
||
assert.equal(page.state.form.value.usedPackages, 10)
|
||
assert.equal(page.state.scannedPallet.value.palletId, 11)
|
||
assert.ok(fifoRequests(page).length)
|
||
page.close()
|
||
})
|
||
|
||
for (const error of [new Error('FIFO_SKIP_REASON_REQUIRED'), { response: { data: { code: 503, data: 'FIFO_SKIP_REASON_REQUIRED' } } }, { response: { data: { code: 500, data: 'OTHER_BUSINESS_ERROR' } } }]) {
|
||
test(`FIFO 非明确拒绝保留原凭据 ${JSON.stringify(error)}`, async () => {
|
||
const page = createPage({ scan: () => boundPallet(), request: config => config.method === 'post' ? Promise.reject(error) : undefined })
|
||
await page.scan(); page.state.form.value.usedPackages = 10; await page.act('submitUsage')
|
||
assert.equal(page.storage.size, 1)
|
||
assert.ok(button(page, '恢复重试'))
|
||
page.close()
|
||
})
|
||
}
|
||
|
||
for (const change of ['user', 'batch', 'page', 'record']) test(`FIFO 明确拒绝的旧回调不能清理 ${change} 变化后的凭据`, async () => {
|
||
const slow = deferred(), storage = new Map()
|
||
const page = createPage({ storage, scan: () => boundPallet(), request: config => config.method === 'post' ? slow.promise : undefined })
|
||
await page.scan(); page.state.form.value.usedPackages = 10; await page.act('submitUsage')
|
||
if (change === 'user') page.user.id = 8
|
||
if (change === 'batch') page.batch.currentBatchId = '99'
|
||
if (change === 'page') await page.lifecycle('onDeactivated')
|
||
if (change === 'record') {
|
||
const [key, raw] = [...storage.entries()][0], record = JSON.parse(raw)
|
||
record.payload.idempotencyKey = 'ANOTHER-KEY'; storage.set(key, JSON.stringify(record))
|
||
}
|
||
const before = [...storage.entries()]
|
||
slow.reject(fifoRejection()); await flush()
|
||
assert.deepEqual([...storage.entries()], before)
|
||
assert.equal(page.state.usageResult.value, null)
|
||
page.close()
|
||
})
|
||
|
||
for (const outcome of ['resolve', 'reject']) test(`FIFO 选择目标隧道重新请求并隔离旧 ${outcome} 响应`, async () => {
|
||
const old = deferred()
|
||
const page = createPage({ request: config => config.url.endsWith('/recommendation')
|
||
? (config.params.tunnelId === 31 ? old.promise : Promise.resolve({ data: fifoData(false) })) : undefined })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; await flush()
|
||
assert.deepEqual(fifoRequests(page).at(-1).params, { qrToken: 'QR-001', tunnelId: 31 })
|
||
page.state.form.value.tunnelId = 32; await flush()
|
||
if (outcome === 'resolve') old.resolve({ data: fifoData(true) }); else old.reject(new Error('旧建议失败'))
|
||
await flush()
|
||
page.state.form.value.fifoSkipReason = ''; await page.act('enterTunnel')
|
||
assert.equal(page.post('/business/usage/enter-tunnel')[0].data.tunnelId, 32)
|
||
assert.equal(page.state.fifoRecommendation.value.requiresSkipReason, false)
|
||
assert.equal(page.state.fifoWarning.value, '')
|
||
page.close()
|
||
})
|
||
|
||
test('FIFO 原因多行输入绑定表单并只读展示列表原因', async () => {
|
||
const page = createPage(); await page.scan()
|
||
const input = page.nodes().find(node => node.type?.name === 'el-input' && node.props?.type === 'textarea')
|
||
assert.ok(input)
|
||
input.props['onUpdate:modelValue']('人工核对原因')
|
||
assert.equal(page.state.form.value.fifoSkipReason, '人工核对原因')
|
||
const column = page.nodes().find(node => node.type?.name === 'el-table-column' && node.props?.prop === 'fifoSkipReason')
|
||
assert.ok(column)
|
||
assert.ok(flatten(column.children.default({ row: { fifoSkipReason: '人工核对原因' } })).some(node => node.children === '人工核对原因'))
|
||
assert.ok(flatten(column.children.default({ row: {} })).some(node => node.children === '—'))
|
||
page.close()
|
||
})
|
||
|
||
test('PC 真实 request 拦截器透传 code500 响应供 FIFO 精确识别', async () => {
|
||
const source = await read('utils/request.js')
|
||
let onResponse
|
||
const service = { interceptors: { request: { use() {} }, response: { use(fn) { onResponse = fn } } } }
|
||
const modules = Object.fromEntries([...source.matchAll(/from\s+['"]([^'"]+)['"]/g)].map(([, name]) => [name, {}]))
|
||
modules.axios = { default: { defaults: { headers: {} }, create: () => service } }
|
||
modules['element-plus'] = { ElMessage() {} }
|
||
modules['@/utils/errorCode'] = { default: {} }
|
||
modules['@/utils/transportCrypto'] = { decryptTransportResponse: async response => response }
|
||
new Function('__modules', bindImports(source).replace(/import\.meta\.env\.VITE_APP_BASE_API/g, "''").replace(/export default service/g, '').replace(/export /g, ''))(modules)
|
||
const response = { data: { code: 500, data: 'FIFO_SKIP_REASON_REQUIRED', msg: '请填写原因' }, request: {} }
|
||
await assert.rejects(onResponse(response), error => error.response === response && error.message === '请填写原因')
|
||
})
|
||
|
||
for (const action of ['enterTunnel', 'submitUsage']) test(`FIFO ${action} 可选原因同样限制500字,trim后500字可提交`, async () => {
|
||
const page = createPage({ scan: () => action === 'enterTunnel' ? pallet() : boundPallet() })
|
||
await page.scan(); page.state.form.value.tunnelId = 31; await flush()
|
||
page.state.form.value.usedPackages = 10; page.state.form.value.fifoSkipReason = '字'.repeat(501)
|
||
await page.act(action)
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 0)
|
||
page.state.form.value.fifoSkipReason = ` ${'字'.repeat(500)} \n`
|
||
await page.act(action)
|
||
assert.equal(page.requests.find(item => item.method === 'post').data.fifoSkipReason, '字'.repeat(500))
|
||
page.close()
|
||
})
|
||
|
||
for (const change of ['user', 'batch']) test(`FIFO ${change} 变化后的旧建议不能更新当前状态`, async () => {
|
||
const old = deferred()
|
||
const page = createPage({ request: config => config.url.endsWith('/recommendation') ? old.promise : undefined })
|
||
await page.scan()
|
||
if (change === 'user') page.user.id = 8
|
||
else page.batch.currentBatchId = '99'
|
||
await flush(); const before = text(page)
|
||
old.resolve({ data: { ...fifoData(), message: 'OLD-FIFO' } }); await flush()
|
||
assert.equal(text(page), before)
|
||
page.close()
|
||
})
|
||
|
||
test('FIFO 旧无原因入隧凭据恢复收到明确拒绝后可补原因重新入隧', async () => {
|
||
const storage = new Map(); await seedUnknown(storage, {}, 'entry')
|
||
const original = JSON.parse([...storage.values()][0]).payload
|
||
assert.equal(original.fifoSkipReason, undefined)
|
||
let rejected = false
|
||
const page = createPage({ storage, request: config => {
|
||
if (config.method === 'post' && !rejected) { rejected = true; return Promise.reject(fifoRejection()) }
|
||
} })
|
||
await retry(page)
|
||
assert.equal(page.state.recoveryRequired.value, false)
|
||
assert.equal(page.state.tunnelLocked.value, false)
|
||
assert.equal(page.state.form.value.tunnelId, 31)
|
||
page.state.form.value.fifoSkipReason = '先到托盘待检验'
|
||
await page.act('enterTunnel')
|
||
const posts = page.post('/business/usage/enter-tunnel')
|
||
assert.deepEqual(posts[0].data, original)
|
||
assert.equal(posts[1].data.fifoSkipReason, '先到托盘待检验')
|
||
assert.notEqual(posts[1].data.idempotencyKey, original.idempotencyKey)
|
||
assert.equal(page.state.tunnelLocked.value, true)
|
||
page.close()
|
||
})
|
||
|
||
for (const kind of ['entry', 'usage']) test(`F2 两页面共享凭据,旧${kind}成功不能删除另一页的新未知操作`, async () => {
|
||
const storage = new Map(), old = deferred()
|
||
await seedUnknown(storage, {}, kind)
|
||
const first = createPage({ storage, request: config => config.method === 'post' ? old.promise : undefined })
|
||
await retry(first)
|
||
let writes = 0
|
||
const second = createPage({ storage, scan: () => boundPallet({ palletId: 12, palletCode: '托盘-B', qrToken: 'QR-B' }), request: config => {
|
||
if (config.method === 'post' && ++writes > 1) return Promise.reject(new Error('新操作B响应丢失'))
|
||
} })
|
||
await retry(second)
|
||
await second.act('resetUsageOperation'); await second.scan('QR-B')
|
||
second.state.form.value.usedPackages = 12; second.state.form.value.fifoSkipReason = 'B原因'
|
||
await second.act('submitUsage')
|
||
const originalB = second.post().at(-1).data
|
||
const before = [...storage.entries()]
|
||
assert.equal(JSON.parse(before[0][1]).payload.idempotencyKey, originalB.idempotencyKey)
|
||
old.resolve({ data: kind === 'entry' ? { palletId: 11, tunnelId: 31, traceNo: 'TRACE-A' } : { usageId: 71, remainingPackages: 90, flowState: 'AT_SITE_PARTIAL' } })
|
||
await flush()
|
||
assert.deepEqual([...storage.entries()], before, '旧成功不能清理同范围后来保存的新记录')
|
||
assert.deepEqual({ ...first.state.pendingOperation.value.payload }, originalB, '旧页面应恢复新待确认记录')
|
||
assert.ok(button(first, '恢复重试'))
|
||
const third = createPage({ storage })
|
||
await third.scan('QR-B'); third.state.form.value.fifoSkipReason = '不同原因'; await third.act('submitUsage')
|
||
assert.equal(third.post().length, 0)
|
||
await retry(third)
|
||
assert.deepEqual(third.post()[0].data, originalB)
|
||
first.close(); second.close(); third.close()
|
||
})
|
||
|
||
for (const kind of ['entry', 'usage']) {
|
||
const action = kind === 'entry' ? 'enterTunnel' : 'submitUsage'
|
||
for (const outcome of ['reject', 'pending', 'incomplete']) test(`F3 恢复${kind}被FIFO拒绝后推荐${outcome}仍保留已知必填`, async () => {
|
||
const storage = new Map(), recommendation = deferred()
|
||
await seedUnknown(storage, {}, kind)
|
||
const page = createPage({ storage, request: config => {
|
||
if (config.method === 'post') return Promise.reject(fifoRejection())
|
||
if (config.url.endsWith('/recommendation')) return outcome === 'reject'
|
||
? Promise.reject(new Error('推荐服务不可用'))
|
||
: outcome === 'incomplete' ? Promise.resolve({ data: { message: '缺少必填标记' } }) : recommendation.promise
|
||
} })
|
||
await retry(page)
|
||
// 同一目标的表单重建不能被视为真实换托盘或换隧道。
|
||
page.state.form.value = { ...page.state.form.value }; await flush()
|
||
assert.equal(page.state.fifoRecommendation.value?.requiresSkipReason, true)
|
||
assert.equal(storage.size, 0)
|
||
page.state.form.value.fifoSkipReason = ' '
|
||
await page.act(action)
|
||
assert.equal(page.requests.filter(item => item.method === 'post').length, 1)
|
||
assert.equal(storage.size, 0, '已知必填时空原因不能重新保存待确认')
|
||
page.close(); recommendation.resolve({ data: fifoData(false) }); await flush()
|
||
})
|
||
}
|
||
|
||
test('F3 恢复拒绝的已知必填仅由有效推荐明确覆盖', async () => {
|
||
const storage = new Map(), recommendation = deferred()
|
||
await seedUnknown(storage, {}, 'entry')
|
||
let rejected = false
|
||
const page = createPage({ storage, request: config => {
|
||
if (config.method === 'post' && !rejected) { rejected = true; return Promise.reject(fifoRejection()) }
|
||
if (config.url.endsWith('/recommendation')) return recommendation.promise
|
||
} })
|
||
await retry(page)
|
||
assert.equal(page.state.fifoRecommendation.value?.requiresSkipReason, true)
|
||
recommendation.resolve({ data: fifoData(false) }); await flush()
|
||
assert.equal(page.state.fifoRecommendation.value.requiresSkipReason, false)
|
||
await page.act('enterTunnel')
|
||
assert.equal(page.post('/business/usage/enter-tunnel').length, 2)
|
||
assert.equal(page.state.tunnelLocked.value, true)
|
||
page.close()
|
||
})
|
||
|
||
test('F3 真正更换隧道可清除原目标必填,旧推荐不能恢复它', async () => {
|
||
const storage = new Map(), old = deferred()
|
||
await seedUnknown(storage, {}, 'entry')
|
||
let rejected = false
|
||
const page = createPage({ storage, request: config => {
|
||
if (config.method === 'post' && !rejected) { rejected = true; return Promise.reject(fifoRejection()) }
|
||
if (config.url.endsWith('/recommendation')) return config.params.tunnelId === 31 ? old.promise : Promise.reject(new Error('新目标建议不可用'))
|
||
} })
|
||
await retry(page)
|
||
assert.equal(page.state.fifoRecommendation.value?.requiresSkipReason, true)
|
||
page.state.form.value.tunnelId = 32; await flush()
|
||
old.resolve({ data: fifoData(true) }); await flush()
|
||
assert.equal(page.state.fifoRecommendation.value, null)
|
||
await page.act('enterTunnel')
|
||
assert.equal(page.post('/business/usage/enter-tunnel').at(-1).data.tunnelId, 32)
|
||
assert.equal(page.state.tunnelLocked.value, true)
|
||
page.close()
|
||
})
|