305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { parse } from '@vue/compiler-sfc'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
|
|
const names = ['VehicleLibrary', 'CarrierLibrary', 'TransportAddress', 'Contract', 'Product']
|
|
const tick = async () => {
|
|
await vue.nextTick()
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
}
|
|
function deferred() {
|
|
let resolve: any, reject: any
|
|
const promise = new Promise((a, b) => {
|
|
resolve = a
|
|
reject = b
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
async function harness(name: string, extra = {}) {
|
|
const props = vue.reactive({
|
|
modelValue: false,
|
|
selectableStatuses: ['approved', 'executing'],
|
|
...extra
|
|
})
|
|
const account = vue.reactive({ accessToken: 'a', getUserInfo: { id: 'a' } })
|
|
const calls: any[] = [],
|
|
details: any[] = [],
|
|
events: any[] = []
|
|
const request = (queue: any[]) => (params: any) => {
|
|
const d = deferred()
|
|
queue.push({ params, ...d })
|
|
return d.promise
|
|
}
|
|
const list = request(calls),
|
|
get = request(details)
|
|
const source = parse(
|
|
readFileSync(new URL(`../src/components/mp/Mp${name}SelectSheet.vue`, import.meta.url), 'utf8')
|
|
).descriptor.scriptSetup!.content
|
|
const ast = ts.createSourceFile('sheet.ts', source, ts.ScriptTarget.Latest, true)
|
|
const code = ast.statements
|
|
.filter((s) => !ts.isImportDeclaration(s))
|
|
.map((s) => s.getText(ast))
|
|
.join('\n')
|
|
.replace(/export type /g, 'type ')
|
|
let helper = {}
|
|
try {
|
|
helper = await import('../src/components/mp/useMobilePickerList')
|
|
} catch {
|
|
// The pre-fix SFC snapshot has no helper dependency.
|
|
}
|
|
const bindings = {
|
|
ref: vue.ref,
|
|
computed: vue.computed,
|
|
watch: vue.watch,
|
|
onScopeDispose: vue.onScopeDispose,
|
|
...helper,
|
|
defineProps: () => props,
|
|
withDefaults: (p: any) => p,
|
|
defineEmits:
|
|
() =>
|
|
(...args: any[]) =>
|
|
events.push(args),
|
|
defineOptions: () => {},
|
|
useUserStore: () => account,
|
|
useDictStore: () => ({ getLabel: (_: any, v: any) => v }),
|
|
ElMessage: { warning() {}, error() {} },
|
|
vehicleApi: { list },
|
|
carrierApi: { list },
|
|
transportAddressApi: { list },
|
|
listContract: list,
|
|
getContract: get,
|
|
fetchProductList: list,
|
|
fetchProductDetail: get,
|
|
pickBxPageResult: (r: any) => ({ list: r.rows || [], total: r.total || 0 })
|
|
}
|
|
const scope = vue.effectScope()
|
|
const h = scope.run(() =>
|
|
new Function(
|
|
...Object.keys(bindings),
|
|
ts.transpile(code, { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.None }) +
|
|
';return { switchTab: typeof switchTab === "undefined" ? undefined : switchTab, activeTab: typeof activeTab === "undefined" ? undefined : activeTab, rows, loading, keyword, fetchList, loadMore, innerVisible, error: typeof error === "undefined" ? undefined : error, handlePick: typeof handlePick === "undefined" ? undefined : handlePick, handleConfirm: typeof handleConfirm === "undefined" ? undefined : handleConfirm, selectedProducts: typeof selectedProducts === "undefined" ? undefined : selectedProducts, toggleSelect: typeof toggleSelect === "undefined" ? undefined : toggleSelect }'
|
|
)(...Object.values(bindings))
|
|
)
|
|
props.modelValue = true
|
|
await tick()
|
|
return { ...h, props, account, calls, details, events, stop: () => scope.stop() }
|
|
}
|
|
for (const name of names) {
|
|
test(`${name}: reversed requests and close/reopen cannot write old rows or finally`, async () => {
|
|
const h = await harness(name)
|
|
h.keyword.value = 'new'
|
|
const fresh = h.fetchList(true)
|
|
h.calls[0].resolve({ rows: [{ id: 'old' }], total: 1 })
|
|
await tick()
|
|
assert.deepEqual(h.rows.value, [])
|
|
assert.equal(h.loading.value, true)
|
|
h.calls.at(-1).resolve({ rows: [{ id: 'new' }], total: 1 })
|
|
await fresh
|
|
h.props.modelValue = false
|
|
await tick()
|
|
h.props.modelValue = true
|
|
await tick()
|
|
const old = h.calls.at(-1)
|
|
h.props.modelValue = false
|
|
await tick()
|
|
h.props.modelValue = true
|
|
await tick()
|
|
old.reject(new Error('old failure'))
|
|
await tick()
|
|
assert.equal(h.loading.value, true)
|
|
assert.equal(h.error.value, '')
|
|
h.stop()
|
|
})
|
|
test(`${name}: failed page retries same page, deduplicates, account invalidates`, async () => {
|
|
const h = await harness(name)
|
|
h.calls[0].resolve({ rows: [{ id: 'a' }], total: 4 })
|
|
await tick()
|
|
h.loadMore()
|
|
h.loadMore()
|
|
assert.equal(h.calls.length, 2)
|
|
h.calls[1].reject(new Error('offline'))
|
|
await tick()
|
|
assert.match(h.error.value, /offline/)
|
|
h.loadMore()
|
|
assert.equal(h.calls[2].params.page ?? h.calls[2].params.pageNum, 2)
|
|
h.calls[2].resolve({ rows: [{ id: 'a' }, { id: 'b' }], total: 4 })
|
|
await tick()
|
|
assert.deepEqual(
|
|
h.rows.value.map((r: any) => r.id),
|
|
['a', 'b']
|
|
)
|
|
h.keyword.value = 'pending'
|
|
h.account.accessToken = 'b'
|
|
await tick()
|
|
assert.deepEqual(h.rows.value, [])
|
|
h.stop()
|
|
const count = h.calls.length
|
|
await new Promise((r) => setTimeout(r, 350))
|
|
assert.equal(h.calls.length, count)
|
|
})
|
|
}
|
|
test('contract detail cannot emit or clear a newer confirmation after customer changes', async () => {
|
|
const h = await harness('Contract', { customerId: 'a' })
|
|
const old = h.handlePick({ id: 'one', status: 'approved' })
|
|
h.props.customerId = 'b'
|
|
await tick()
|
|
const fresh = h.handlePick({ id: 'two', status: 'approved' })
|
|
h.details[0].resolve({ id: 'one', status: 'approved', customerId: 'a' })
|
|
await old
|
|
assert.equal(h.events.length, 0)
|
|
h.details[1].resolve({
|
|
id: 'two',
|
|
status: 'approved',
|
|
customerId: 'b',
|
|
products: [{ id: 'line', productId: 'p' }]
|
|
})
|
|
await fresh
|
|
assert.equal(h.events[0][1].products[0].id, 'line')
|
|
h.stop()
|
|
})
|
|
test('restricted products expose detail failure and reject stale allowed IDs', async () => {
|
|
const h = await harness('Product', { allowedProductIds: ['a'] })
|
|
h.details[0].reject(new Error('offline'))
|
|
await tick()
|
|
assert.match(h.error.value, /offline/)
|
|
const retry = h.fetchList(true)
|
|
h.props.allowedProductIds = ['b']
|
|
await tick()
|
|
h.details[1].resolve({ id: 'a' })
|
|
await retry
|
|
assert.deepEqual(h.rows.value, [])
|
|
h.details[2].resolve({ id: 'b', productName: 'B' })
|
|
await tick()
|
|
await tick()
|
|
h.toggleSelect(h.rows.value[0])
|
|
h.keyword.value = 'missing'
|
|
const search = h.fetchList(true)
|
|
h.details[3].resolve({ id: 'b', productName: 'B' })
|
|
await search
|
|
assert.equal(h.selectedProducts.value[0].id, 'b')
|
|
h.innerVisible.value = false
|
|
assert.equal(
|
|
h.events.some((e: any[]) => e[0] === 'confirm'),
|
|
false
|
|
)
|
|
h.stop()
|
|
})
|
|
for (const name of names) {
|
|
test(`${name}: clear search restores results and unmount cancels pending debounce`, async () => {
|
|
const h = await harness(name)
|
|
h.calls[0].resolve({ rows: [{ id: 'a' }], total: 1 })
|
|
await tick()
|
|
h.keyword.value = 'absent'
|
|
const missing = h.fetchList(true)
|
|
h.calls[1].resolve({ rows: [], total: 0 })
|
|
await missing
|
|
assert.deepEqual(h.rows.value, [])
|
|
h.keyword.value = ''
|
|
const restore = h.fetchList(true)
|
|
assert.equal(h.calls[2].params.q ?? h.calls[2].params.keyword, undefined)
|
|
h.calls[2].resolve({ rows: [{ id: 'a' }], total: 1 })
|
|
await restore
|
|
assert.equal(h.rows.value[0].id, 'a')
|
|
h.keyword.value = 'later'
|
|
h.stop()
|
|
await new Promise((r) => setTimeout(r, 350))
|
|
assert.equal(h.calls.length, 3)
|
|
})
|
|
}
|
|
test('vehicle tab switch keeps self until close and reopen restores carrier', async () => {
|
|
const h = await harness('VehicleLibrary')
|
|
assert.equal(h.calls[0].params.vehicleType, 'carrier')
|
|
h.switchTab('self')
|
|
assert.equal(h.activeTab.value, 'self')
|
|
assert.equal(h.calls[1].params.vehicleType, 'self')
|
|
h.calls[0].resolve({ rows: [{ id: 'old-carrier' }], total: 1 })
|
|
await tick()
|
|
assert.deepEqual(h.rows.value, [])
|
|
h.calls[1].resolve({ rows: [{ id: 'self' }], total: 1 })
|
|
await tick()
|
|
assert.equal(h.rows.value[0].id, 'self')
|
|
h.innerVisible.value = false
|
|
h.props.modelValue = false
|
|
await tick()
|
|
h.props.modelValue = true
|
|
await tick()
|
|
assert.equal(h.activeTab.value, 'carrier')
|
|
assert.equal(h.calls.at(-1).params.vehicleType, 'carrier')
|
|
assert.equal(h.calls.length, 3)
|
|
h.stop()
|
|
})
|
|
test('restricted empty IDs never expand into full catalogue and confirm retains metadata', async () => {
|
|
const h = await harness('Product', { allowedProductIds: [] })
|
|
await tick()
|
|
assert.equal(h.calls.length, 0)
|
|
assert.equal(h.details.length, 0)
|
|
assert.equal(h.error.value, '')
|
|
h.props.allowedProductIds = ['p']
|
|
await tick()
|
|
h.details[0].resolve({
|
|
id: 'p',
|
|
productName: 'P',
|
|
brands: [{ id: 'brand' }],
|
|
aliases: [{ id: 'alias' }]
|
|
})
|
|
await tick()
|
|
await tick()
|
|
h.toggleSelect(h.rows.value[0])
|
|
h.handleConfirm()
|
|
const result = h.events.find((e: any[]) => e[0] === 'confirm')[1][0]
|
|
assert.equal(result.id, 'p')
|
|
assert.equal(result.brands[0].id, 'brand')
|
|
assert.equal(result.aliases[0].id, 'alias')
|
|
h.stop()
|
|
})
|
|
test('restricted response must match requested product ID', async () => {
|
|
const h = await harness('Product', { allowedProductIds: ['p'] })
|
|
h.details[0].resolve({ id: 'wrong' })
|
|
await tick()
|
|
await tick()
|
|
assert.deepEqual(h.rows.value, [])
|
|
assert.match(h.error.value, /不匹配/)
|
|
h.stop()
|
|
})
|
|
test('contract stale rejection cannot clear newer confirming state or emit', async () => {
|
|
const h = await harness('Contract', { customerId: 'a' })
|
|
const old = h.handlePick({ id: 'one', status: 'approved' })
|
|
h.innerVisible.value = false
|
|
h.props.modelValue = false
|
|
await tick()
|
|
h.props.modelValue = true
|
|
await tick()
|
|
const fresh = h.handlePick({ id: 'two', status: 'executing' })
|
|
h.details[0].reject(new Error('old'))
|
|
await old
|
|
await h.handlePick({ id: 'three', status: 'approved' })
|
|
assert.equal(h.details.length, 2)
|
|
h.details[1].resolve({ id: 'two', customerId: 'a', status: 'executing' })
|
|
await fresh
|
|
assert.equal(h.events.filter((e: any[]) => e[0] === 'confirm').length, 1)
|
|
h.stop()
|
|
})
|
|
test('equivalent account and allowed ID objects preserve selection; actual change and back invalidates', async () => {
|
|
const h = await harness('Product', { allowedProductIds: ['p'] })
|
|
h.details[0].resolve({ id: 'p' })
|
|
await tick()
|
|
await tick()
|
|
h.toggleSelect(h.rows.value[0])
|
|
h.account.getUserInfo = { id: 'a' }
|
|
h.props.allowedProductIds = ['p']
|
|
assert.equal(h.selectedProducts.value.length, 1)
|
|
assert.equal(h.details.length, 1)
|
|
h.account.accessToken = 'b'
|
|
h.account.accessToken = 'a'
|
|
assert.equal(h.selectedProducts.value.length, 0)
|
|
h.details[1].resolve({ id: 'p', productName: 'old-account' })
|
|
await tick()
|
|
await tick()
|
|
assert.deepEqual(h.rows.value, [])
|
|
h.stop()
|
|
})
|