feat(approval): add task workflow and waiver decisions
This commit is contained in:
201
web/tests/approval-task-queue-composable.test.mjs
Normal file
201
web/tests/approval-task-queue-composable.test.mjs
Normal file
@@ -0,0 +1,201 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { useApprovalTaskQueue } from '../src/composables/useApprovalTaskQueue.js'
|
||||
|
||||
function queueItem(id, overrides = {}) {
|
||||
return {
|
||||
task: {
|
||||
id,
|
||||
claimId: `claim-${id}`,
|
||||
status: 'pending',
|
||||
version: 1,
|
||||
canAct: true,
|
||||
availableActions: ['approve'],
|
||||
batchEligible: true,
|
||||
claimStatusSnapshot: 'submitted',
|
||||
claimStageSnapshot: '直属领导审批',
|
||||
...overrides
|
||||
},
|
||||
claim: { id: `claim-${id}`, claim_no: `BX-${id}` }
|
||||
}
|
||||
}
|
||||
|
||||
function listResult(items, page = 1) {
|
||||
return {
|
||||
items,
|
||||
total: items.length,
|
||||
page,
|
||||
pageSize: 20,
|
||||
totalPages: items.length ? 1 : 0,
|
||||
generatedAt: '2026-07-16T10:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
return { promise, reject, resolve }
|
||||
}
|
||||
|
||||
test('列表请求代次阻止慢请求覆盖新页结果', async () => {
|
||||
const requests = []
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
fetchApprovalTasks(params) {
|
||||
const request = deferred()
|
||||
requests.push({ params, request })
|
||||
return request.promise
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const firstLoad = queue.loadQueue()
|
||||
await queue.setPage(2, { reload: false })
|
||||
const secondLoad = queue.loadQueue()
|
||||
requests[1].request.resolve(listResult([queueItem('task-new')], 2))
|
||||
await secondLoad
|
||||
requests[0].request.resolve(listResult([queueItem('task-stale')], 1))
|
||||
const staleResult = await firstLoad
|
||||
|
||||
assert.equal(requests[0].params.page, 1)
|
||||
assert.equal(requests[1].params.page, 2)
|
||||
assert.equal(staleResult.ignored, true)
|
||||
assert.equal(queue.items.value[0].task.id, 'task-new')
|
||||
assert.equal(queue.page.value, 2)
|
||||
})
|
||||
|
||||
test('筛选变化清空选择并回到第一页', async () => {
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
async fetchApprovalTasks() {
|
||||
return listResult([queueItem('task-001')])
|
||||
}
|
||||
}
|
||||
})
|
||||
await queue.loadQueue()
|
||||
queue.toggleTask(queue.items.value[0], true)
|
||||
await queue.setPage(3, { reload: false })
|
||||
assert.deepEqual(queue.selectedTaskIds.value, ['task-001'])
|
||||
|
||||
await queue.setFilters({ riskLevel: 'high' }, { reload: false })
|
||||
assert.equal(queue.page.value, 1)
|
||||
assert.equal(queue.filters.value.riskLevel, 'high')
|
||||
assert.deepEqual(queue.selectedTaskIds.value, [])
|
||||
})
|
||||
|
||||
test('批量部分成功保留失败选择并使用逐项任务快照', async () => {
|
||||
const batchCalls = []
|
||||
let listCallCount = 0
|
||||
const rows = [queueItem('task-001'), queueItem('task-002')]
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
async fetchApprovalTasks() {
|
||||
listCallCount += 1
|
||||
return listResult(rows)
|
||||
},
|
||||
async batchApproveApprovalTasks(payload) {
|
||||
batchCalls.push(payload)
|
||||
return {
|
||||
batchRequestId: payload.batchRequestId,
|
||||
status: 'partial',
|
||||
succeededCount: 1,
|
||||
replayedCount: 0,
|
||||
conflictCount: 0,
|
||||
blockedCount: 1,
|
||||
forbiddenCount: 0,
|
||||
failedCount: 0,
|
||||
items: [
|
||||
{ taskId: 'task-001', status: 'succeeded' },
|
||||
{ taskId: 'task-002', status: 'blocked', message: '存在开放高风险' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await queue.loadQueue()
|
||||
queue.toggleCurrentPage(true)
|
||||
const result = await queue.batchApprove({ opinion: '批量核对完成' })
|
||||
|
||||
assert.equal(listCallCount, 2)
|
||||
assert.equal(batchCalls.length, 1)
|
||||
assert.equal(batchCalls[0].items[0].task.id, 'task-001')
|
||||
assert.equal(batchCalls[0].items[0].opinion, '批量核对完成')
|
||||
assert.equal(result.status, 'partial')
|
||||
assert.deepEqual(queue.selectedTaskIds.value, ['task-002'])
|
||||
assert.equal(queue.lastBatchResult.value.items[1].status, 'blocked')
|
||||
assert.equal(queue.pendingBatchRequestId.value, '')
|
||||
})
|
||||
|
||||
test('单项刷新采用服务端最新权限并移除失效选择', async () => {
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
async fetchApprovalTasks() {
|
||||
return listResult([queueItem('task-001')])
|
||||
},
|
||||
async fetchApprovalTaskDetail() {
|
||||
return queueItem('task-001', {
|
||||
version: 2,
|
||||
canAct: false,
|
||||
availableActions: [],
|
||||
batchEligible: false,
|
||||
readOnlyReason: '任务已转交给其他审批人'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await queue.loadQueue()
|
||||
queue.toggleTask(queue.items.value[0], true)
|
||||
await queue.refreshTask('task-001')
|
||||
|
||||
assert.equal(queue.items.value[0].task.version, 2)
|
||||
assert.equal(queue.items.value[0].task.readOnlyReason, '任务已转交给其他审批人')
|
||||
assert.deepEqual(queue.selectedTaskIds.value, [])
|
||||
})
|
||||
|
||||
test('批量选择在前端基础层限制为服务端允许的 20 项', async () => {
|
||||
const rows = Array.from({ length: 21 }, (_, index) => queueItem(`task-${index + 1}`))
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
async fetchApprovalTasks() {
|
||||
return listResult(rows)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await queue.loadQueue()
|
||||
queue.toggleCurrentPage(true)
|
||||
|
||||
assert.equal(queue.selectedTaskIds.value.length, 20)
|
||||
assert.match(queue.errorMessage.value, /最多选择 20 项/)
|
||||
})
|
||||
|
||||
test('批量失败按相同负载复用幂等键,编辑意见后生成新键', async () => {
|
||||
const requestIds = []
|
||||
const queue = useApprovalTaskQueue({
|
||||
services: {
|
||||
async fetchApprovalTasks() {
|
||||
return listResult([queueItem('task-retry')])
|
||||
},
|
||||
async batchApproveApprovalTasks(payload) {
|
||||
requestIds.push(payload.batchRequestId)
|
||||
throw new Error('模拟网络中断')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await queue.loadQueue()
|
||||
queue.toggleCurrentPage(true)
|
||||
await assert.rejects(queue.batchApprove({ opinion: '核对通过' }), /模拟网络中断/)
|
||||
await assert.rejects(queue.batchApprove({ opinion: '核对通过' }), /模拟网络中断/)
|
||||
await assert.rejects(queue.batchApprove({ opinion: '补充核对后通过' }), /模拟网络中断/)
|
||||
|
||||
assert.equal(requestIds[1], requestIds[0])
|
||||
assert.notEqual(requestIds[2], requestIds[0])
|
||||
})
|
||||
Reference in New Issue
Block a user