285 lines
15 KiB
JavaScript
285 lines
15 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
|
|
function evaluate(source, dependencies = {}) {
|
|
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
|
|
const exports = {}
|
|
new Function('require', 'exports', code)(id => {
|
|
assert.ok(id in dependencies, `Unexpected dependency ${id}`)
|
|
return dependencies[id]
|
|
}, exports)
|
|
return exports
|
|
}
|
|
const eligibility = evaluate(readFileSync(new URL('../utils/customsEligibility.ts', import.meta.url), 'utf8'))
|
|
const customsApi = evaluate(readFileSync(new URL('../api/customs.ts', import.meta.url), 'utf8'), {
|
|
'@/api/http': {}, '@/modules/first-leg/api/firstLeg': {},
|
|
})
|
|
const order = (id, status = 'LOADED', taxMode = 'TAX_REFUND') => ({ id, status, taxMode, hasCompletedLoadingPlan: ['LOADED', 'DELIVERED', 'COMPLETED'].includes(status), orderNo: `ORDER-${id}`, businessSummary: { inboundCode: `IN-${id}` } })
|
|
const detail = id => ({ status: 'COMPLETED', merchantStatus: 'CUSTOMS_COMPLETED', inbounds: [{ orderId: id, taxMode: 'TAX_REFUND' }] })
|
|
function harness(t, orders, getDetail = async id => detail(id)) {
|
|
const store = { loadError: '', failPage: 0, transformPage: value => value }, calls = [], pageCalls = []
|
|
const unifiedApi = evaluate(readFileSync(new URL('../api/unifiedOrders.ts', import.meta.url), 'utf8'), {
|
|
'@/api/http': { apiRequest: async ({ method, url, params: { page, size, taxMode, loaded } }) => {
|
|
assert.equal(method, 'GET')
|
|
assert.equal(url, '/merchant/v1/orders')
|
|
pageCalls.push({ page, size, taxMode, loaded })
|
|
if (store.loadError || store.failPage === page) throw new Error(store.loadError || '第二页订单查询失败')
|
|
// Model server filtering BEFORE pagination and COUNT, using the actual
|
|
// request produced by the production API. Missing taxMode scans all rows.
|
|
const filtered = orders.filter(row => (!taxMode || row.taxMode === taxMode)
|
|
&& (loaded === undefined || row.hasCompletedLoadingPlan === loaded))
|
|
return store.transformPage({ page, size, total: filtered.length, pages: Math.ceil(filtered.length / size),
|
|
records: filtered.slice((page - 1) * size, page * size).map(row => ({ orderId: row.id, orderNo: row.orderNo, mainStatus: row.status, taxMode: row.taxMode, inboundCode: row.businessSummary.inboundCode })) })
|
|
} },
|
|
})
|
|
const reader = evaluate(readFileSync(new URL('../utils/customsDocumentsReader.ts', import.meta.url), 'utf8'), {
|
|
'./customsEligibility': eligibility,
|
|
'../api/unifiedOrders': unifiedApi,
|
|
'../api/customs': { getMerchantCustomsForOrder: id => { calls.push(id); return getDetail(id) }, isMerchantCustomsUnavailable: error => error?.code === 'A0404' },
|
|
})
|
|
const source = readFileSync(new URL('./CustomsDocumentsView.vue', import.meta.url), 'utf8').match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
|
|
const scope = vue.effectScope()
|
|
const view = scope.run(() => evaluate(source + '\nexport { load, records, loading, errorMessage, openDetail, selectedOrderId, selectedDetail, dialogOpen, search, queryModel, filteredRecords, pagedRecords, page, pageSize, queryFields, statusLabel, statusType, updateDetail }', {
|
|
vue: { ...vue, onMounted() {} },
|
|
'../utils/customsDocumentsReader': reader,
|
|
'../api/customs': customsApi,
|
|
}))
|
|
t.after(() => scope.stop())
|
|
return { view, store, calls, pageCalls }
|
|
}
|
|
|
|
test('server-filtered request reaches five refund records originally on page 28 without scanning buy orders', async t => {
|
|
const orders = Array.from({ length: 5528 }, (_, i) => order(`buy-${i}`, 'COMPLETED', 'BUY_DECLARATION'))
|
|
orders.push(...Array.from({ length: 2613 }, (_, i) => order(`not-loaded-${i}`, 'WAIT_MEASURE')))
|
|
orders.push(...Array.from({ length: 5 }, (_, i) => order(`historical-${i}`, 'COMPLETED')))
|
|
const { view, calls, pageCalls } = harness(t, orders)
|
|
await view.load()
|
|
assert.deepEqual(calls, Array.from({ length: 5 }, (_, i) => `historical-${i}`))
|
|
assert.equal(view.records.value.length, 5)
|
|
assert.deepEqual(pageCalls, [{ page: 1, size: 200, taxMode: 'TAX_REFUND', loaded: true }])
|
|
view.queryModel.value.inboundCode = 'historical-4'; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 1)
|
|
view.queryModel.value = { orderNo: '', inboundCode: '', containerNo: '', status: '' }; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 5)
|
|
assert.equal(pageCalls.length, 1)
|
|
assert.equal(calls.length, 5)
|
|
})
|
|
|
|
test('second page failure is explicit and retry does not retain an incomplete result', async t => {
|
|
const orders = Array.from({ length: 201 }, (_, i) => order(String(i)))
|
|
const { view, store, pageCalls } = harness(t, orders)
|
|
store.failPage = 2
|
|
await view.load()
|
|
assert.equal(view.records.value.length, 0)
|
|
assert.equal(view.errorMessage.value, '第二页订单查询失败')
|
|
store.failPage = 0
|
|
await view.load()
|
|
assert.equal(view.records.value.length, 201)
|
|
assert.equal(view.errorMessage.value, '')
|
|
assert.equal(pageCalls.length, 4)
|
|
})
|
|
|
|
test('display pagination reaches the final refund order without refetching or opening the wrong detail', async t => {
|
|
const { view, calls, pageCalls } = harness(t, Array.from({ length: 21 }, (_, i) => order(`refund-${i}`, 'COMPLETED')))
|
|
await view.load()
|
|
assert.equal(view.pagedRecords.value.length, 20)
|
|
view.page.value = 2
|
|
assert.deepEqual(view.pagedRecords.value.map(row => row.order.id), ['refund-20'])
|
|
view.openDetail(view.pagedRecords.value[0])
|
|
assert.equal(view.selectedOrderId.value, 'refund-20')
|
|
assert.deepEqual(view.selectedDetail.value.inbounds, [{ orderId: 'refund-20', taxMode: 'TAX_REFUND' }])
|
|
view.pageSize.value = 10
|
|
await vue.nextTick()
|
|
assert.equal(view.page.value, 1)
|
|
assert.equal(view.pagedRecords.value.length, 10)
|
|
view.page.value = 3
|
|
assert.deepEqual(view.pagedRecords.value.map(row => row.order.id), ['refund-20'])
|
|
assert.equal(pageCalls.length, 1)
|
|
assert.equal(calls.length, 21)
|
|
})
|
|
|
|
test('searching from a later display page resets pagination and clearing filters restores all documents', async t => {
|
|
const { view, calls } = harness(t, Array.from({ length: 21 }, (_, i) => order(`refund-${i}`)))
|
|
await view.load()
|
|
view.page.value = 2
|
|
view.queryModel.value.inboundCode = 'IN-refund-20'
|
|
view.search()
|
|
await vue.nextTick()
|
|
assert.equal(view.page.value, 1)
|
|
assert.deepEqual(view.pagedRecords.value.map(row => row.order.id), ['refund-20'])
|
|
view.queryModel.value.inboundCode = 'does-not-exist'
|
|
view.search()
|
|
await vue.nextTick()
|
|
assert.equal(view.page.value, 1)
|
|
assert.equal(view.pagedRecords.value.length, 0)
|
|
view.queryModel.value.inboundCode = ''
|
|
view.search()
|
|
assert.equal(view.filteredRecords.value.length, 21)
|
|
assert.equal(view.pagedRecords.value.length, 20)
|
|
assert.equal(calls.length, 21)
|
|
})
|
|
|
|
test('refresh failure on a later page clears stale rows and retry returns to a valid page', async t => {
|
|
const { view, store } = harness(t, Array.from({ length: 21 }, (_, i) => order(`refund-${i}`)))
|
|
await view.load()
|
|
view.page.value = 2
|
|
store.loadError = '资料列表请求失败'
|
|
await view.load()
|
|
await vue.nextTick()
|
|
assert.equal(view.page.value, 1)
|
|
assert.equal(view.pagedRecords.value.length, 0)
|
|
assert.equal(view.errorMessage.value, '资料列表请求失败')
|
|
store.loadError = ''
|
|
await view.load()
|
|
assert.equal(view.page.value, 1)
|
|
assert.equal(view.pagedRecords.value.length, 20)
|
|
assert.equal(view.filteredRecords.value.length, 21)
|
|
assert.equal(view.errorMessage.value, '')
|
|
})
|
|
|
|
test('empty pages, duplicate records and changing totals cannot silently truncate history', async t => {
|
|
for (const change of [
|
|
response => response.page === 2 ? { ...response, records: [] } : response,
|
|
response => response.page === 2 ? { ...response, records: [{ ...response.records[0], orderId: '0' }] } : response,
|
|
response => response.page === 2 ? { ...response, total: 202 } : response,
|
|
]) {
|
|
const { view, store, pageCalls } = harness(t, Array.from({ length: 201 }, (_, i) => order(String(i), 'COMPLETED')))
|
|
store.transformPage = change
|
|
await view.load()
|
|
assert.ok(view.errorMessage.value)
|
|
assert.equal(view.records.value.length, 0)
|
|
assert.equal(pageCalls.length, 2)
|
|
}
|
|
})
|
|
|
|
test('document fetch concurrency never exceeds four and each candidate is requested once', async t => {
|
|
let active = 0, peak = 0
|
|
const { view, calls, pageCalls } = harness(t, Array.from({ length: 205 }, (_, i) => order(String(i))), async id => {
|
|
active += 1; peak = Math.max(peak, active)
|
|
await new Promise(resolve => setImmediate(resolve))
|
|
active -= 1
|
|
return detail(id)
|
|
})
|
|
await view.load()
|
|
assert.equal(peak, 4)
|
|
assert.equal(new Set(calls).size, 205)
|
|
assert.equal(calls.length, 205)
|
|
assert.equal(pageCalls.length, 2)
|
|
assert.deepEqual(pageCalls, [{ page: 1, size: 200, taxMode: 'TAX_REFUND', loaded: true }, { page: 2, size: 200, taxMode: 'TAX_REFUND', loaded: true }])
|
|
assert.equal(view.records.value.length, 205)
|
|
})
|
|
|
|
test('empty result uses one page request and no document requests', async t => {
|
|
const { view, calls, pageCalls } = harness(t, [])
|
|
await view.load()
|
|
assert.equal(pageCalls.length, 1)
|
|
assert.equal(calls.length, 0)
|
|
assert.equal(view.errorMessage.value, '')
|
|
})
|
|
|
|
test('tax-refund documents remain accessible through delivered and completed orders; buy orders and preloading are excluded', async t => {
|
|
const stages = ['DRAFT', 'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED', 'COMPLETED']
|
|
const orders = stages.flatMap(status => [order(status, status), order(`BUY-${status}`, status, 'BUY_DECLARATION')])
|
|
const { view, calls } = harness(t, orders)
|
|
await view.load()
|
|
assert.deepEqual(calls, ['LOADED', 'DELIVERED', 'COMPLETED'])
|
|
assert.deepEqual(view.records.value.map(row => row.order.id), calls)
|
|
const completed = view.records.value.find(row => row.order.id === 'COMPLETED')
|
|
view.openDetail(completed)
|
|
assert.equal(view.selectedOrderId.value, 'COMPLETED')
|
|
assert.equal(view.selectedDetail.value.status, 'COMPLETED')
|
|
assert.equal(view.dialogOpen.value, true)
|
|
view.queryModel.value.status = 'CUSTOMS_COMPLETED'; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 3)
|
|
assert.equal(calls.length, 3, 'local filtering does not refetch documents')
|
|
})
|
|
|
|
test('only matching server-returned tax-refund ownership is displayed', async t => {
|
|
const { view } = harness(t, [order('one'), order('two')], async id => id === 'one' ? detail('someone-else') : { inbounds: [{ orderId: id, taxMode: 'BUY_DECLARATION' }] })
|
|
await view.load()
|
|
assert.equal(view.records.value.length, 0)
|
|
})
|
|
|
|
test('partial failures are visible and retry replaces the result; unavailable customs is not a transport failure', async t => {
|
|
let fail = true
|
|
const { view } = harness(t, [order('ok'), order('retry'), order('unavailable')], async id => {
|
|
if (id === 'unavailable') throw Object.assign(new Error('not associated'), { code: 'A0404' })
|
|
if (id === 'retry' && fail) throw new Error('network')
|
|
return detail(id)
|
|
})
|
|
await view.load()
|
|
assert.equal(view.records.value.length, 1)
|
|
assert.equal(view.errorMessage.value, '1 票订单的报关资料加载失败,请重新加载')
|
|
fail = false; await view.load()
|
|
assert.equal(view.records.value.length, 2)
|
|
assert.equal(view.errorMessage.value, '')
|
|
})
|
|
|
|
test('order query failure is shown instead of a successful empty result', async t => {
|
|
const { view, store, calls } = harness(t, [order('old')])
|
|
await view.load()
|
|
store.loadError = '订单查询失败'; await view.load()
|
|
assert.equal(view.records.value.length, 0)
|
|
assert.equal(view.errorMessage.value, '订单查询失败')
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(view.loading.value, false)
|
|
})
|
|
|
|
test('repeated refresh during an active load does not duplicate requests', async t => {
|
|
let resolve
|
|
const pending = new Promise(done => { resolve = done })
|
|
const { view, calls } = harness(t, [order('one')], () => pending)
|
|
const first = view.load()
|
|
await Promise.resolve()
|
|
await view.load()
|
|
assert.deepEqual(calls, ['one'])
|
|
resolve(detail('one')); await first
|
|
assert.equal(view.loading.value, false)
|
|
})
|
|
|
|
test('mixed-container labels and filters use only the current customer status', async t => {
|
|
const states = [
|
|
['uploaded', 'WAITING_CUSTOMER_DOCUMENTS', 'DOCUMENT_UPLOADED', 1, '资料已上传', 'success'],
|
|
['waiting', 'WAITING_CUSTOMER_DOCUMENTS', 'WAITING_UPLOAD', 0, '待上传资料', 'warning'],
|
|
['declaring', 'DECLARING', 'DECLARING', 1, '报关中', 'primary'],
|
|
['completed', 'COMPLETED', 'CUSTOMS_COMPLETED', 1, '报关完成', 'success'],
|
|
]
|
|
const { view, calls } = harness(t, states.map(([id]) => order(id)), async id => {
|
|
const [, status, merchantStatus, coveredTaxRefundCount] = states.find(row => row[0] === id)
|
|
return { ...detail(id), status, merchantStatus, coveredTaxRefundCount, taxRefundCount: 1 }
|
|
})
|
|
await view.load()
|
|
for (const [id, , merchantStatus, , label, tone] of states) {
|
|
const row = view.records.value.find(item => item.order.id === id)
|
|
assert.equal(view.statusLabel(row.detail), label)
|
|
assert.equal(view.statusType(row.detail), tone)
|
|
view.queryModel.value.status = merchantStatus; view.search()
|
|
assert.deepEqual(view.filteredRecords.value.map(item => item.order.id), [id])
|
|
}
|
|
assert.deepEqual(view.queryFields.find(field => field.key === 'status').options,
|
|
states.map(([, , value, , label]) => ({ value, label })).sort((a, b) =>
|
|
['WAITING_UPLOAD', 'DOCUMENT_UPLOADED', 'DECLARING', 'CUSTOMS_COMPLETED'].indexOf(a.value)
|
|
- ['WAITING_UPLOAD', 'DOCUMENT_UPLOADED', 'DECLARING', 'CUSTOMS_COMPLETED'].indexOf(b.value)))
|
|
view.queryModel.value.status = 'WAITING_CUSTOMER_DOCUMENTS'; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 0, 'container statuses are not customer filter values')
|
|
view.queryModel.value.status = ''; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 4)
|
|
assert.equal(calls.length, 4, 'filtering does not requery or infer status from coverage')
|
|
})
|
|
|
|
test('dialog update refreshes customer status and active filter without inferring from container status', async t => {
|
|
const { view } = harness(t, [order('one')], async id => ({ ...detail(id), status: 'WAITING_CUSTOMER_DOCUMENTS', merchantStatus: 'WAITING_UPLOAD' }))
|
|
await view.load(); view.openDetail(view.records.value[0])
|
|
view.queryModel.value.status = 'WAITING_UPLOAD'; view.search()
|
|
view.updateDetail({ ...detail('one'), status: 'WAITING_CUSTOMER_DOCUMENTS', merchantStatus: 'DOCUMENT_UPLOADED', coveredTaxRefundCount: 1, taxRefundCount: 1 })
|
|
assert.equal(view.filteredRecords.value.length, 0)
|
|
view.queryModel.value.status = 'DOCUMENT_UPLOADED'; view.search()
|
|
assert.equal(view.filteredRecords.value.length, 1)
|
|
assert.equal(view.statusLabel(view.filteredRecords.value[0].detail), '资料已上传')
|
|
assert.equal(view.statusLabel({ status: 'COMPLETED', coveredTaxRefundCount: 1, taxRefundCount: 1 }), '—')
|
|
assert.equal(view.statusType({ status: 'COMPLETED' }), 'info')
|
|
})
|