| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- 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()
- })
|