Files
goauto/web/tests/unit/syb-return-confirm.test.cjs
T

113 lines
9.1 KiB
JavaScript

const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const readOptions = (file, dependencies) => new Function(...Object.keys(dependencies), fs.readFileSync(path.join(__dirname, '../../src/views/goauto/syb-products', file), 'utf8').replace(/import[^\n]+\n/g, '').replace('export default', 'return'))(...Object.values(dependencies))
const tick = () => new Promise(resolve => setImmediate(resolve))
function instance(overrides = {}) {
const writes = []; const reads = []; const selected = []; const confirmations = []
const dependencies = {
confirmReturnMatch: async (id, options) => { writes.push({ id, options }); return { code: 200 } },
getReturnMatch: async id => { reads.push(id); return { data: { match: { id, sybProductId: id - 10, status: 'matched' } } } }, ...overrides
}
const options = readOptions('return-confirm.js', dependencies)
const cancel = readOptions('return-cancel.js', { cancelReturnMatch: async () => {}, getReturnMatch: dependencies.getReturnMatch })
const vm = { ...cancel.methods, ...options.methods, ...cancel.data(), ...options.data(), canPurchase: true, loading: false, returnMatchLoading: false, purchaseReadinessLoading: false, matchDetail: {}, loadGeneration: 0,
products: [1, 2, 3].map(id => ({ id, orderCode: `TEST-${id}` })),
purchaseReadiness: { 1: { processStage: 'return_pending' }, 2: { processStage: 'return_pending' }, 3: { processStage: 'return_used' } },
returnMatchByProductId: { 1: { id: 11, status: 'matched' }, 2: { id: 12, status: 'matched' }, 3: { id: 13, status: 'confirmed' } },
purchaseReady(row) { return this.purchaseReadiness[row.id] || {} },
$confirm: async text => { confirmations.push(text) }, $nextTick: async () => {},
$refs: { productTable: { clearSelection: () => selected.splice(0), toggleRowSelection: row => selected.push(row.id) } },
load: async function() { this.loadGeneration++ }
}
vm.selectedProducts = [...vm.products]
Object.entries({ ...cancel.computed, ...options.computed }).forEach(([key, fn]) => Object.defineProperty(vm, key, { get: () => fn.call(vm) }))
return { vm, options, writes, reads, selected, confirmations }
}
test('current-page count, deduplication, mixed rows, original confirm API and one confirmation', async () => {
const { vm, writes, confirmations } = instance()
vm.selectedProducts.push(vm.products[0], { id: 99 })
assert.equal(vm.returnConfirmCandidates.length, 2)
await vm.runBatchConfirmReturns()
assert.deepEqual(writes.map(w => w.id), [11, 12])
assert.ok(writes.every(w => w.options.suppressErrorMessage && w.options.suppressNetworkError))
assert.equal(confirmations.length, 1); assert.match(confirmations[0], /2 条/); assert.match(confirmations[0], /不会自动让 Yeeke 发货/)
assert.equal(vm.returnConfirmResult.successCount, 2); assert.equal(vm.returnConfirmResult.skippedCount, 1)
assert.equal(vm.returnConfirmBatchLoading, false)
})
test('permission, metadata, busy and empty states prohibit writes', async () => {
for (const [key, value] of [['canPurchase', false], ['loading', true], ['returnMatchLoadError', true], ['returnMatchLoading', true], ['purchaseReadinessLoading', true], ['returnTransitionLoading', true], ['returnMatchBatchLoading', true], ['returnCancelBatchLoading', true]]) {
const { vm, writes } = instance(); vm[key] = value; await vm.runBatchConfirmReturns(); assert.equal(writes.length, 0, key)
}
const { vm, writes } = instance(); vm.matchDetail.saving = true; await vm.runBatchConfirmReturns(); assert.equal(writes.length, 0)
vm.matchDetail.saving = false; vm.selectedProducts = []; await vm.runBatchConfirmReturns(); assert.equal(writes.length, 0)
})
test('return dismisses without a write or result; confirm busy blocks cancel and single transitions', async () => {
const { vm, writes } = instance(); vm.$confirm = async () => { throw new Error('cancel') }; await vm.runBatchConfirmReturns()
assert.equal(writes.length, 0); assert.equal(vm.returnConfirmResult.open, false); assert.equal(vm.returnConfirmBatchLoading, false)
vm.returnConfirmBatchLoading = true; let ran = false
assert.match(vm.returnCancelButtonReason, /正在处理/)
await vm.runReturnTransition(async () => { ran = true }); assert.equal(ran, false)
})
test('serial, frozen identities and no duplicate execution while waiting', async () => {
let release; const wait = new Promise(resolve => { release = resolve }); const writes = []
const { vm } = instance({ confirmReturnMatch: async id => { writes.push(id); if (id === 11) await wait } })
const running = vm.runBatchConfirmReturns(); await tick()
vm.returnMatchByProductId[2].id = 99; vm.selectedProducts = []; await vm.runBatchConfirmReturns()
assert.deepEqual(writes, [11]); release(); await running; assert.deepEqual(writes, [11, 12])
})
test('partial business failure keeps only readback-proven current-page failed selection', async () => {
const { vm, selected, reads } = instance({ confirmReturnMatch: async id => { if (id === 12) throw { response: { status: 500 } } } })
await vm.runBatchConfirmReturns(); assert.equal(vm.returnConfirmResult.failedCount, 1); assert.deepEqual(reads, [12]); assert.deepEqual(selected, [2])
})
test('state conflict skips original; neither readback nor replacement write', async () => {
const { vm, reads } = instance({ confirmReturnMatch: async () => { throw { response: { status: 409, data: { code: 'STATE_CONFLICT' } } } } })
await vm.runBatchConfirmReturns(); assert.equal(vm.returnConfirmResult.skippedCount, 3); assert.equal(reads.length, 0)
})
for (const status of ['confirmed', 'cancelled', 'matched']) {
test(`unknown outcome reads original ${status} once, never resends`, async () => {
let writes = 0
const { vm, reads, selected } = instance({ confirmReturnMatch: async () => { writes++; throw new Error('timeout') }, getReturnMatch: async id => { reads.push(id); return { data: { match: { id, sybProductId: id - 10, status } } } } })
await vm.runBatchConfirmReturns(); assert.equal(writes, 2); assert.deepEqual(reads, [11, 12])
assert.equal(vm.returnConfirmResult.successCount, 0)
assert.deepEqual(selected, status === 'matched' ? [1, 2] : [])
assert.equal(vm.returnConfirmResult.skippedCount, status === 'matched' ? 1 : 3)
})
}
test('unreadable/mismatched original never treated as confirmed or retryable', async () => {
for (const getReturnMatch of [async () => { throw new Error('offline') }, async id => ({ data: { match: { id: id + 1, sybProductId: 999, status: 'confirmed' } } })]) {
const { vm, selected } = instance({ confirmReturnMatch: async () => { throw new Error('timeout') }, getReturnMatch })
await vm.runBatchConfirmReturns(); assert.equal(vm.returnConfirmResult.failedCount, 2); assert.deepEqual(selected, [])
}
})
for (const response of [false, new Error('Unauthorized'), { response: { status: 401 } }]) {
test(`auth failure/legacy false stops remaining writes (${String(response)})`, async () => {
let writes = 0
const { vm } = instance({ confirmReturnMatch: async () => { writes++; if (response === false) return false; throw response } })
await vm.runBatchConfirmReturns(); assert.equal(writes, 1); assert.equal(vm.returnConfirmResult.failedCount, 2); assert.equal(vm.returnConfirmBatchLoading, false)
assert.match(vm.returnConfirmResult.items[1].reason, /未继续执行/)
})
}
test('login failure during readback stops remaining POSTs', async () => {
let writes = 0
const { vm } = instance({ confirmReturnMatch: async () => { writes++; throw new Error('timeout') }, getReturnMatch: async () => { throw new Error('Unauthorized') } })
await vm.runBatchConfirmReturns(); assert.equal(writes, 1); assert.equal(vm.returnConfirmResult.failedCount, 2)
})
test('new generation, replaced original and vanished failed row cannot restore old selection', async () => {
for (const load of [async function() { this.loadGeneration += 2 }, async function() { this.loadGeneration++; this.products = [] }, async function() { this.loadGeneration++; this.returnMatchByProductId[1].id = 99; this.returnMatchByProductId[2].id = 98 }]) {
const { vm, selected } = instance({ confirmReturnMatch: async () => { throw new Error('timeout') } }); vm.load = load
await vm.runBatchConfirmReturns(); assert.equal(vm.returnConfirmResult.failedCount, 2); assert.deepEqual(selected, [])
}
})
test('refresh failure keeps completed result and does not write again', async () => {
const { vm, writes } = instance(); vm.load = async () => { throw new Error('offline') }; await vm.runBatchConfirmReturns()
assert.match(vm.returnConfirmResult.refreshError, /刷新失败/); assert.equal(vm.returnConfirmResult.successCount, 2); assert.equal(writes.length, 2)
})
test('unmount stops remaining writes after in-flight response', async () => {
let release; const wait = new Promise(resolve => { release = resolve }); let writes = 0
const { vm, options } = instance({ confirmReturnMatch: async () => { writes++; await wait } })
const running = vm.runBatchConfirmReturns(); await tick(); options.beforeUnmount.call(vm); release(); await running
assert.equal(writes, 1); assert.equal(vm.returnConfirmBatchLoading, false)
})