139 lines
7.9 KiB
JavaScript
139 lines
7.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { runInNewContext } from 'node:vm'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
import { parse, compileScript } from 'vue/compiler-sfc'
|
|
|
|
const source = readFileSync(new URL('./components/ProcurementDecisionDialog.vue', import.meta.url), 'utf8')
|
|
const { descriptor } = parse(source)
|
|
const code = ts.transpileModule(compileScript(descriptor, { id: 'decision-test' }).content, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText
|
|
const flush = async () => { await vue.nextTick(); await new Promise(resolve => setImmediate(resolve)); await vue.nextTick() }
|
|
let key = 0
|
|
function harness({ decision = 'confirm', api = {} } = {}) {
|
|
const props = vue.reactive({ order: { orderNo: 'PO-1', productName: '商品', purchaseQuantity: 10 }, decision })
|
|
const auth = vue.reactive({ token: 'customer-A', hasPermission: () => true })
|
|
const calls = [], events = [], disposers = [], created = [], revoked = []
|
|
const workflow = { orderNo: 'PO-1', status: 'WAIT_CUSTOMER_CONFIRM', version: 7, receivedQuantity: 8, files: [{ fileId: 'F-1', fileName: 'photo.png' }], receipts: [{ id: 'R-1', quantity: 5, fileIds: ['F-1'], receivedAt: '' }, { id: 'R-2', quantity: 3, fileIds: ['F-2'], receivedAt: '' }] }
|
|
const defaults = {
|
|
getProcurementWorkflow: async () => workflow,
|
|
getProcurementFile: async () => ({ type: 'image/png' }),
|
|
confirmProcurement: async (orderNo, payload) => { calls.push({ orderNo, ...payload }); return { ...workflow, status: 'CONFIRMED' } },
|
|
rejectProcurement: async (orderNo, payload) => { calls.push({ orderNo, ...payload }); return { ...workflow, status: 'RECEIVING' } },
|
|
}
|
|
const context = { exports: {}, Error, URL: { createObjectURL: () => { const url = `blob:${created.length}`; created.push(url); return url }, revokeObjectURL: url => revoked.push(url) }, require(name) {
|
|
if (name === 'vue') return { ...vue, onBeforeUnmount: callback => disposers.push(callback) }
|
|
if (name === 'element-plus') return { ElMessage: { success: () => {} } }
|
|
if (name === '@/api/session') return { accessTokenIdentity: token => token }
|
|
if (name === '@/modules/auth/public') return { useAuthStore: () => auth }
|
|
if (name === '../api/procurement') return { ...defaults, ...api }
|
|
if (name === '../procurementSubmission') return { procurementRequestKey: () => `key-${++key}` }
|
|
if (name === '../procurementPresentation') return { dateTime: value => value }
|
|
throw new Error(`unexpected import ${name}`)
|
|
} }
|
|
runInNewContext(code, context)
|
|
const state = context.exports.default.setup(props, { expose: () => {}, emit: (...args) => events.push(args) })
|
|
return { state, auth, calls, events, created, revoked, workflow, dispose: () => disposers.forEach(callback => callback()) }
|
|
}
|
|
|
|
test('both decisions load receipt photos and batch quantities with preview URL cleanup', async () => {
|
|
for (const decision of ['confirm', 'reject']) {
|
|
const h = harness({ decision })
|
|
try {
|
|
await flush()
|
|
assert.equal(h.state.workflow.value.receivedQuantity, 8)
|
|
assert.equal(h.state.workflow.value.receipts.length, 2)
|
|
assert.equal(Object.keys(h.state.photos.value).length, 2)
|
|
assert.equal(h.state.canSubmit.value, true)
|
|
assert.equal(h.state.fileName('F-1'), 'photo.png')
|
|
assert.match(source, /order.purchaseQuantity/)
|
|
assert.match(source, /receipt.quantity/)
|
|
assert.match(source, /<el-image/)
|
|
assert.match(source, /preview-src-list/)
|
|
} finally { h.dispose() }
|
|
assert.deepEqual(h.revoked, h.created)
|
|
}
|
|
})
|
|
|
|
test('confirmation uses current version, prevents double submits, and signals list refresh', async () => {
|
|
let finish, calls = []
|
|
const h = harness({ api: { confirmProcurement: (orderNo, command) => { calls.push({ orderNo, ...command }); return new Promise(resolve => { finish = resolve }) } } })
|
|
try {
|
|
await flush()
|
|
const pending = h.state.submitDecision(); await h.state.submitDecision()
|
|
assert.equal(calls.length, 1); assert.equal(calls[0].version, 7); assert.equal(calls[0].orderNo, 'PO-1')
|
|
finish({ status: 'CONFIRMED' }); await pending
|
|
assert.deepEqual(h.events, [['changed']])
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('rejection requires a trimmed reason and retries unchanged payload with the same key', async () => {
|
|
const calls = []
|
|
const h = harness({ decision: 'reject', api: { rejectProcurement: async (orderNo, command) => { calls.push({ orderNo, ...command }); throw new Error('网络失败') } } })
|
|
try {
|
|
await flush()
|
|
for (const reason of ['', ' ', 'x'.repeat(1001)]) {
|
|
h.state.reason.value = reason; await h.state.submitDecision()
|
|
assert.match(h.state.error.value, /请填写驳回原因/)
|
|
}
|
|
assert.equal(calls.length, 0)
|
|
h.state.reason.value = ' 照片不清晰 '
|
|
await h.state.submitDecision(); await h.state.submitDecision()
|
|
assert.equal(calls.length, 2); assert.equal(calls[0].reason, '照片不清晰'); assert.equal(calls[0].version, 7)
|
|
assert.equal(calls[0].idempotencyKey, calls[1].idempotencyKey)
|
|
assert.equal(h.state.reason.value, ' 照片不清晰 '); assert.equal(h.events.length, 0)
|
|
h.state.reason.value = '数量不符'; await h.state.submitDecision()
|
|
assert.notEqual(calls[1].idempotencyKey, calls[2].idempotencyKey)
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('confirmation retry preserves the loaded evidence and idempotency key', async () => {
|
|
const calls = []
|
|
const h = harness({ api: { confirmProcurement: async (_, command) => { calls.push(command); throw new Error('版本已更新,请刷新') } } })
|
|
try {
|
|
await flush(); await h.state.submitDecision(); await h.state.submitDecision()
|
|
assert.equal(calls.length, 2); assert.equal(calls[0].idempotencyKey, calls[1].idempotencyKey)
|
|
assert.equal(h.state.workflow.value.version, 7); assert.equal(Object.keys(h.state.photos.value).length, 2)
|
|
assert.equal(h.state.saving.value, false); assert.equal(h.events.length, 0)
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('successful rejection sends the trimmed reason and signals list refresh', async () => {
|
|
const h = harness({ decision: 'reject' })
|
|
try {
|
|
await flush(); h.state.reason.value = ' 数量不符 '; await h.state.submitDecision()
|
|
assert.equal(h.calls[0].reason, '数量不符'); assert.equal(h.calls[0].version, 7)
|
|
assert.deepEqual(h.events, [['changed']])
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('photo loading failures can be retried before a decision is submitted', async () => {
|
|
let fail = true
|
|
const h = harness({ api: { getProcurementFile: async () => { if (fail) throw new Error('failed'); return { type: 'image/png' } } } })
|
|
try {
|
|
await flush(); await h.state.submitDecision()
|
|
assert.equal(h.calls.length, 0); assert.match(h.state.photoError.value, /照片加载失败/)
|
|
fail = false; await h.state.load(); await h.state.submitDecision()
|
|
assert.equal(h.calls.length, 1)
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('stale status, revoked permission and changed account cannot submit decisions', async () => {
|
|
const stale = harness({ api: { getProcurementWorkflow: async () => ({ status: 'CONFIRMED', receipts: [] }) } })
|
|
try { await flush(); await stale.state.submitDecision(); assert.equal(stale.calls.length, 0) } finally { stale.dispose() }
|
|
const h = harness()
|
|
try {
|
|
await flush(); h.auth.hasPermission = () => false; await h.state.submitDecision(); assert.equal(h.calls.length, 0)
|
|
h.auth.hasPermission = () => true; h.auth.token = 'customer-B'; await h.state.submitDecision(); assert.equal(h.calls.length, 0)
|
|
} finally { h.dispose() }
|
|
})
|
|
|
|
test('late photo responses after closing do not allocate preview URLs', async () => {
|
|
const pending = []
|
|
const h = harness({ api: { getProcurementFile: () => new Promise(resolve => pending.push(resolve)) } })
|
|
await flush(); h.dispose()
|
|
pending.forEach(resolve => resolve({ type: 'image/png' })); await flush()
|
|
assert.equal(h.created.length, 0); assert.equal(h.events.length, 0)
|
|
})
|