136 lines
9.7 KiB
JavaScript
136 lines
9.7 KiB
JavaScript
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const source = fs.readFileSync(path.join(__dirname, '../../src/views/goauto/syb-products/quick-link.js'), 'utf8').replace(/import[^\n]+\n/g, '').replace('export default', 'return')
|
|
const row = { id: 1, shopeeProductId: 11 }
|
|
const tick = () => new Promise(resolve => setImmediate(resolve))
|
|
const deferred = () => { let resolve; const promise = new Promise(yes => { resolve = yes }); return { promise, resolve } }
|
|
function instance(overrides = {}) {
|
|
const calls = { link: [], match: [], reads: 0, saved: [], refresh: 0 }
|
|
const dependencies = {
|
|
ElMessage: { warning() {} },
|
|
ElMessageBox: { confirm: async () => {}, close() {} },
|
|
listDevices: async () => ({ data: { items: [{ id: 10, status: 'offline', name: 'A' }, { id: 20, status: 'online', name: 'B' }] } }),
|
|
getShopeeProduct: async id => { calls.reads++; return { data: { product: { id, specContextVersion: 'before', pddProductId: calls.link.length ? 55 : null }, autoMatchTimeoutSeconds: 370 } } },
|
|
linkShopeeProductPdd: async (id, body) => { calls.link.push(body); return { data: { product: { id, specContextVersion: 'after' }, collectionSource: { taskId: 9, pddProductId: 55 } } } },
|
|
autoMatchShopeeSpecMappings: async (id, body, seconds) => { calls.match.push({ id, body, seconds }); return { data: { unmatchedCount: 0 } } },
|
|
previewPurchaseTasks: async () => ({ data: { items: [{ aiMatchEligible: true }] } }),
|
|
createRequestId: () => 'request-id', readQuickLinkDevice: () => null,
|
|
rememberQuickLinkDevice: (user, id) => { calls.saved.push(id); return true }, ...overrides
|
|
}
|
|
const options = new Function(...Object.keys(dependencies), source)(...Object.values(dependencies))
|
|
const vm = { ...options.methods, ...options.data(), canPurchase: true, products: [row], $store: { getters: { userId: 1 } }, purchaseReady: () => ({ processNextAction: 'open_pdd_link' }), loadPurchaseReadiness: async () => { calls.refresh++ } }
|
|
return { vm, calls, options }
|
|
}
|
|
test('first selection continues original row and forwards frozen context / #254 budget', async () => {
|
|
const { vm, calls } = instance()
|
|
await vm.quickStart(row); assert.equal(calls.link.length, 0)
|
|
vm.quickPicker.deviceId = 10; vm.quickSaveDevice(); await tick()
|
|
assert.equal(calls.link.length, 1); assert.equal(calls.link[0].latestCollection.sybProductId, 1)
|
|
assert.equal(calls.link[0].latestCollection.deviceId, 10)
|
|
assert.equal(calls.match[0].body.specContextVersion, 'after'); assert.equal(calls.match[0].seconds, 370)
|
|
assert.equal(vm.quickBusyId, null)
|
|
})
|
|
test('switch saves shared preference only; cancel changes nothing', async () => {
|
|
const { vm, calls } = instance(); vm.quickDeviceId = 10
|
|
await vm.quickOpenPicker(); vm.quickPicker.deviceId = 20; vm.quickCancelPicker()
|
|
assert.equal(vm.quickDeviceId, 10); assert.equal(calls.saved.length, 0)
|
|
await vm.quickOpenPicker(); vm.quickPicker.deviceId = 20; vm.quickSaveDevice()
|
|
assert.equal(vm.quickDeviceId, 20); assert.equal(calls.link.length, 0)
|
|
})
|
|
test('in-flight device is frozen and double click cannot duplicate POST', async () => {
|
|
const wait = deferred(); let links = 0, device
|
|
const { vm } = instance({ getShopeeProduct: () => wait.promise, linkShopeeProductPdd: async (id, body) => { links++; device = body.latestCollection.deviceId; return { data: { product: { specContextVersion: 'after' }, collectionSource: {} } } } })
|
|
vm.quickDeviceId = 10; const running = vm.quickStart(row)
|
|
vm.quickDeviceId = 20; await vm.quickStart(row)
|
|
wait.resolve({ data: { product: { specContextVersion: 'before' }, autoMatchTimeoutSeconds: 130 } }); await running
|
|
assert.equal(links, 1); assert.equal(device, 10)
|
|
})
|
|
test('leaving during prepare prevents later association / matching', async () => {
|
|
const wait = deferred(); const { vm, calls, options } = instance({ getShopeeProduct: () => wait.promise })
|
|
vm.quickDeviceId = 10; const running = vm.quickStart(row); options.beforeUnmount.call(vm)
|
|
wait.resolve({ data: { product: { specContextVersion: 'before' } } }); await running
|
|
assert.equal(calls.link.length, 0); assert.equal(calls.match.length, 0)
|
|
})
|
|
for (const error of ['timeout', 'Network Error', 'AI 匹配服务暂时不可用']) {
|
|
test(`${error}: finite readback, end loading, no automatic write retry`, async () => {
|
|
let matches = 0
|
|
const { vm, calls } = instance({ autoMatchShopeeSpecMappings: async () => { matches++; throw new Error(error) } })
|
|
vm.quickDeviceId = 10; await vm.quickStart(row)
|
|
assert.equal(calls.link.length, 1); assert.equal(matches, 1); assert.equal(calls.reads, 2)
|
|
assert.equal(vm.quickBusyId, null); assert.match(vm.quickMessages[1], /当前已有关联/)
|
|
})
|
|
}
|
|
test('failed recovery reports unknown and clears busy', async () => {
|
|
const { vm } = instance({ getShopeeProduct: async () => { throw new Error('offline') } })
|
|
vm.quickDeviceId = 10; await vm.quickStart(row)
|
|
assert.equal(vm.quickBusyId, null); assert.match(vm.quickMessages[1], /结果未确认/)
|
|
})
|
|
test('definite candidate rejection explains cause without AI or a write retry', async () => {
|
|
let links = 0
|
|
const { vm, calls } = instance({ linkShopeeProductPdd: async () => { links++; throw { response: { status: 409, data: { message: '最近一次临时采集不完整,请先补采' } } } } })
|
|
vm.quickDeviceId = 10; await vm.quickStart(row)
|
|
assert.equal(links, 1); assert.equal(calls.match.length, 0); assert.equal(calls.reads, 1)
|
|
assert.equal(vm.quickBusyId, null); assert.equal(vm.quickMessages[1], '最近一次临时采集不完整,请先补采')
|
|
})
|
|
test('manual-required readiness retains association without AI call', async () => {
|
|
const { vm, calls } = instance({ previewPurchaseTasks: async () => ({ data: { items: [{ aiMatchEligible: false, eligible: false }] } }) })
|
|
vm.quickDeviceId = 10; await vm.quickStart(row)
|
|
assert.equal(calls.link.length, 1); assert.equal(calls.match.length, 0); assert.match(vm.quickMessages[1], /人工处理/)
|
|
})
|
|
test('preference isolates user and API environment, handles storage failure', () => {
|
|
const source = fs.readFileSync(path.join(__dirname, '../../src/utils/quick-link-device-preference.js'), 'utf8').replace(/export function/g, 'function')
|
|
const map = new Map(); const process = { env: { VUE_APP_BASE_API: '/a' } }
|
|
const storage = { getItem: k => map.get(k), setItem: (k, v) => map.set(k, v) }
|
|
const p = new Function('localStorage', 'process', source + '; return {readQuickLinkDevice,rememberQuickLinkDevice}')(storage, process)
|
|
p.rememberQuickLinkDevice(1, 10); assert.equal(p.readQuickLinkDevice(1), 10); assert.equal(p.readQuickLinkDevice(2), null)
|
|
process.env.VUE_APP_BASE_API = '/b'; assert.equal(p.readQuickLinkDevice(1), null)
|
|
storage.setItem = () => { throw new Error('denied') }; assert.equal(p.rememberQuickLinkDevice(1, 20), false)
|
|
})
|
|
|
|
function replacementInstance(overrides = {}) {
|
|
const writes = [], previews = [], confirms = []
|
|
const context = instance({
|
|
getShopeeProduct: async () => ({ data: { product: { pddProductId: 44, specContextVersion: 'old' }, autoMatchTimeoutSeconds: 130 } }),
|
|
ElMessageBox: { confirm: async (...args) => { confirms.push(args) }, close() {} },
|
|
linkShopeeProductPdd: async (id, body) => {
|
|
const r = body.latestCollection.replacement
|
|
;(r.preview ? previews : writes).push(body)
|
|
return { data: { product: { specContextVersion: 'new', pddProductId: r.preview ? 44 : 55 }, collectionSource: { taskId: 9, pddProductId: 55, targetVersion: 'target', title: '新商品', previousTitle: '原商品', deviceName: 'A' } } }
|
|
}, ...overrides
|
|
})
|
|
context.vm.purchaseReady = () => ({ pddProductId: 44, processNextAction: 'open_pdd', processStage: 'pdd_collection_failed' })
|
|
context.vm.quickDeviceId = 10
|
|
return { ...context, writes, previews, confirms }
|
|
}
|
|
test('replacement confirms once and submits the frozen source', async () => {
|
|
const { vm, writes, previews, confirms, calls } = replacementInstance()
|
|
await vm.quickStart(row)
|
|
assert.equal(previews.length, 1); assert.equal(confirms.length, 1); assert.equal(writes.length, 1)
|
|
assert.deepEqual(writes[0].latestCollection.replacement, { preview: false, expectedPddProductId: 44, taskId: 9, targetPddProductId: 55, targetVersion: 'target' })
|
|
assert.equal(calls.match.length, 1)
|
|
})
|
|
test('cancel replacement never writes or matches', async () => {
|
|
const { vm, writes, calls } = replacementInstance({ ElMessageBox: { confirm: async () => { throw 'cancel' } } })
|
|
await vm.quickStart(row)
|
|
assert.equal(writes.length, 0); assert.equal(calls.match.length, 0); assert.match(vm.quickMessages[1], /已取消/)
|
|
})
|
|
test('same product continues matching without replacement confirmation', async () => {
|
|
const { vm, confirms, calls } = replacementInstance({ linkShopeeProductPdd: async () => ({ data: { product: { specContextVersion: 'old' }, collectionSource: { taskId: 9, pddProductId: 44, targetVersion: 'same' } } }) })
|
|
await vm.quickStart(row)
|
|
assert.equal(confirms.length, 0); assert.equal(calls.match.length, 1)
|
|
})
|
|
test('failed replacement readback does not call old association a success', async () => {
|
|
const { vm, calls } = replacementInstance({ linkShopeeProductPdd: async () => { throw new Error('timeout') } })
|
|
await vm.quickStart(row)
|
|
assert.match(vm.quickMessages[1], /仍为原商品/); assert.equal(calls.match.length, 0)
|
|
})
|
|
test('replacement is not shown for collecting, successful, unknown or other manual stages', () => {
|
|
const { vm } = replacementInstance()
|
|
for (const stage of ['pdd_collecting', 'purchase_succeeded', 'order_review', 'task_created', 'color_mapping', 'manual_action']) {
|
|
vm.purchaseReady = () => ({ pddProductId: 44, processNextAction: 'open_pdd', processStage: stage })
|
|
assert.equal(vm.quickCanLink(row), false, stage)
|
|
}
|
|
})
|