Files
goauto/web/tests/unit/syb-quick-link.test.cjs
T

90 lines
6.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() {} },
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)
})