import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' import ts from 'typescript' import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc' import * as vue from 'vue' const read = (path) => readFileSync(path, 'utf8') function evaluate(source, imports = {}) { const code = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText const module = { exports: {} } new Function('require', 'module', 'exports', code)( (name) => { if (name.endsWith('.vue')) return { default: {} } assert.ok(imports[name], `Unexpected import ${name}`) return imports[name] }, module, module.exports ) return module.exports } const format = evaluate(read('src/utils/business/format.ts')) const gateway = evaluate(read('src/utils/business/gateway.ts'), { './format': format }) const polling = evaluate(read('src/utils/business/pollingScheduler.ts'), { './gateway': gateway }) const response = (data) => ({ code: 0, data }) const deferred = () => { let resolve, reject const promise = new Promise((a, b) => { resolve = a reject = b }) return { promise, resolve, reject } } const tick = async () => { await vue.nextTick() await Promise.resolve() await vue.nextTick() } function serviceHarness(overrides = {}) { const calls = [] const service = new Proxy( {}, { get: (_, method) => async (...args) => { calls.push({ method, args }) return overrides[method] ? overrides[method](...args) : response({ items: [], total: 0 }) } } ) const modules = { './packageManage': { PackageManageService: service }, './packageSeries': { PackageSeriesService: service }, './shop': { ShopService: service }, './card': { CardService: service }, './device': { DeviceService: service }, './account': { AccountService: service }, './packageSilence': { PackageSilenceService: service }, '@/utils/business/gateway': gateway } return { ...evaluate(read('src/api/modules/businessOptions.ts'), modules), calls } } test('entity search uses documented keyword/name fields and returns labels, never database IDs as labels', async () => { const h = serviceHarness({ getPackages: () => response({ items: [{ id: 8, package_name: '实名套餐' }], total: 41 }), getPackageSeries: () => response({ items: [{ id: 9, series_name: '流量系列' }], total: 1 }), getShops: () => response({ items: [{ id: 10, shop_name: '一号代理' }], total: 1 }), getAccounts: () => response({ items: [{ id: 11, username: '张三', shop_name: '一号代理' }], total: 1 }), getStandaloneIotCards: () => response({ items: [{ id: 12, iccid: '89860001', virtual_no: 'V-01' }], total: 1 }), getDevices: () => response({ items: [{ id: 13, virtual_no: 'D-01', device_name: '设备甲' }], total: 1 }) }) for (const [kind, method, field] of [ ['package', 'getPackages', 'package_name'], ['series', 'getPackageSeries', 'series_name'], ['shop', 'getShops', 'shop_name'], ['account', 'getAccounts', 'username'], ['card', 'getStandaloneIotCards', 'keyword'] ]) { const result = await h.searchBusinessOptions(kind, ' 关键字 ', 2) assert.ok(result.options.length) assert.equal(typeof result.options[0].value, 'number') assert.notEqual(result.options[0].label, String(result.options[0].value)) const call = h.calls.at(-1) assert.equal(call.method, method) assert.deepEqual(call.args[0], { page: 2, page_size: 20, [field]: '关键字' }) assert.equal(result.hasMore, kind === 'package') } const assets = await h.searchBusinessOptions('asset', '01') assert.deepEqual( assets.options.map((x) => x.value), ['89860001', 'D-01'] ) assert.match(assets.options[0].label, /V-01/) assert.match(assets.options[1].label, /设备甲/) }) test('package record selection uses real usage IDs with asset/package context and readable order labels', async () => { const h = serviceHarness({ usages: () => response({ items: [ { package_usage_id: 99, package_name: '实名套餐', asset_identifier: '8986', order_no: 'ORD-01' } ], total: 1 }) }) const result = await h.searchBusinessOptions('usage', '', 1, { asset_identifier: '8986', package_id: 8 }) assert.deepEqual(h.calls[0].args[0], { page: 1, page_size: 20, view: 'all', asset_identifier: '8986', package_id: 8 }) assert.deepEqual(result.options, [{ value: 99, label: '实名套餐 · 8986 · ORD-01' }]) await h.searchBusinessOptions('usage', '8987', 1, { asset_identifier: '8986' }) assert.equal(h.calls.at(-1).args[0].asset_identifier, '8987') }) test('asset lookup keeps permitted card results when the device lookup fails and reports the partial failure', async () => { const h = serviceHarness({ getStandaloneIotCards: () => response({ items: [{ id: 12, iccid: '89860001' }], total: 1 }), getDevices: () => ({ code: 403, msg: '无设备查看权限' }) }) const result = await h.searchBusinessOptions('asset', '') assert.equal(result.options[0].value, '89860001') assert.match(result.warning, /设备.*无设备查看权限/) }) test('policy selection resolves existing package/series names through detail endpoints and propagates business errors', async () => { const h = serviceHarness({ getPackageDetail: () => response({ package_name: '已有套餐' }), getPackageSeriesDetail: () => response({ series_name: '已有系列' }), getShops: () => ({ code: 403, msg: '无数据权限' }) }) assert.deepEqual(await h.resolveBusinessOption('package', 8), { value: 8, label: '已有套餐' }) assert.deepEqual(await h.resolveBusinessOption('series', 9), { value: 9, label: '已有系列' }) await assert.rejects(h.searchBusinessOptions('shop', ''), /无数据权限/) }) function componentHarness({ entity = 'shop', modelValue, search, resolve, allowed = true } = {}) { const hooks = [], calls = [], events = [] const user = vue.reactive({ info: { id: 1, user_type: 2, shop_id: 1 }, isLogin: true }) const props = vue.reactive({ entity, modelValue, allowed, clearable: true }) const runtime = { ...vue, onBeforeUnmount: (fn) => hooks.push(fn), onDeactivated: (fn) => hooks.push(fn), onActivated() {} } const { descriptor } = parse(read('src/components/business/BusinessEntitySelect.vue')) const component = evaluate(compileScript(descriptor, { id: 'entity-select' }).content, { vue: runtime, '@/store/modules/user': { useUserStore: () => user }, '@/utils/business/gateway': gateway, '@/utils/business/pollingScheduler': polling, '@/api/modules/businessOptions': { BUSINESS_OPTION_LABELS: { shop: '店铺', package: '套餐', series: '系列', asset: '资产', account: '操作人' }, searchBusinessOptions: async (...args) => { calls.push(args) return search ? search(...args) : { options: [], hasMore: false } }, resolveBusinessOption: resolve || (async (_, value) => ({ value, label: '已有名称' })) } }).default const scope = vue.effectScope() const state = scope.run(() => component.setup(props, { expose() {}, emit: (event, value) => { events.push([event, value]) props.modelValue = value } }) ) return { state, props, user, calls, events, dispose: () => { hooks.forEach((fn) => fn()) scope.stop() } } } test('rapid keywords ignore late responses, selection retains its label through later searches and clear emits no ID', async () => { const old = deferred() const h = componentHarness({ search: (_, keyword) => keyword === 'old' ? old.promise : { options: [ { value: keyword === 'new' ? 2 : 3, label: keyword === 'new' ? '新店铺' : '其他店铺' } ], hasMore: false } }) const pending = h.state.search('old') await h.state.search('new') old.resolve({ options: [{ value: 1, label: '旧店铺' }], hasMore: false }) await pending assert.deepEqual(h.state.options.value, [{ value: 2, label: '新店铺' }]) h.state.selection.value = 2 await h.state.search('other') assert.deepEqual( h.state.options.value.map((x) => x.label), ['新店铺', '其他店铺'] ) assert.deepEqual(h.events[0], ['update:modelValue', 2]) h.state.selection.value = undefined assert.deepEqual( h.state.options.value.map((x) => x.label), ['其他店铺'] ) h.dispose() }) test('load more appends options and request failures are retryable rather than fabricated empty results', async () => { let fail = true const h = componentHarness({ search: (_, keyword, page) => { if (keyword === 'fail' && fail) throw new Error('网络失败') return { options: [{ value: page, label: `店铺${page}` }], hasMore: page < 2 } } }) await h.state.search('') assert.equal(h.state.hasMore.value, true) await h.state.load(2) assert.deepEqual( h.state.options.value.map((x) => x.value), [1, 2] ) assert.equal(h.state.hasMore.value, false) await h.state.search('fail') assert.equal(h.state.error.value, '网络失败') fail = false await h.state.load(1) assert.equal(h.state.error.value, '') assert.equal(h.calls.at(-1)[1], 'fail') h.dispose() }) test('permission/account/context changes and unmount discard in-flight results without new unauthorized requests', async () => { for (const action of ['permission', 'account', 'context', 'unmount']) { const pending = deferred(), h = componentHarness({ search: () => pending.promise }) const loading = h.state.search('a') if (action === 'permission') h.props.allowed = false if (action === 'account') h.user.info.id = 2 if (action === 'context') h.props.context = { package_id: 8 } if (action === 'unmount') h.dispose() await tick() pending.resolve({ options: [{ value: 8, label: '已失效数据' }], hasMore: true }) await loading assert.deepEqual(h.state.options.value, [], action) assert.equal(h.state.loading.value, false) if (action === 'permission' || action === 'unmount') { await h.state.search('b') assert.equal(h.calls.length, 1) } if (action !== 'unmount') h.dispose() } }) test('existing multi-selection hydrates readable labels and system operator preserves numeric zero', async () => { const h = componentHarness({ entity: 'package', modelValue: [7, 8], resolve: async (_, value) => ({ value, label: `套餐名${value}` }) }) await tick() assert.deepEqual( h.state.options.value.map((x) => x.label), ['套餐名7', '套餐名8'] ) h.props.modelValue = [8] await tick() assert.deepEqual( h.state.options.value.map((x) => x.label), ['套餐名8'] ) h.dispose() const account = componentHarness({ entity: 'account' }) account.props.includeSystem = true account.state.selection.value = 0 assert.deepEqual(account.events.at(-1), ['update:modelValue', 0]) assert.deepEqual(account.state.options.value, [{ value: 0, label: '系统' }]) account.dispose() }) test('all affected templates compile and raw-ID search controls are replaced throughout the shared pages', () => { const files = [ 'src/components/business/BusinessEntitySelect.vue', 'src/components/core/forms/art-search-bar/widget/art-search-business-select/index.vue', 'src/components/core/forms/art-search-bar/index.vue', 'src/components/business/package-silence/SilenceManagement.vue', 'src/components/business/package-silence/SilencePolicyDialog.vue', 'src/views/order-management/order-list/index.vue', 'src/views/finance/refund/index.vue', 'src/views/polling-management/priority-queue/index.vue', 'src/views/polling-management/polling-pauses/index.vue', 'src/views/asset-management/phone-asset-association/index.vue', 'src/views/asset-management/record-management/asset-assign/index.vue' ] for (const file of files) { const { descriptor, errors } = parse(read(file)) assert.deepEqual(errors, [], file) assert.deepEqual( compileTemplate({ source: descriptor.template.content, filename: file, id: file }).errors, [], file ) } const management = read(files[3]) assert.match(read(files[2]), /v-show="index < visibleItemCount"/) for (const field of ['asset_identifier', 'package_id', 'shop_id', 'package_usage_id']) assert.match(management, new RegExp(`entity\\('${field}'`)) assert.doesNotMatch(management, /请输入套餐 ID|number\('(?:package_id|shop_id|package_usage_id)'/) assert.doesNotMatch(read(files[4]), /label: `ID/) assert.doesNotMatch(read(files[7]), /请输入卡ID/) })