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