90 lines
6.7 KiB
JavaScript
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)
|
|
})
|