Files
one-pipe-system/scripts/tests/businessEntitySelect.test.mjs
luo f2614d8efb
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 54s
fix: some
2026-10-28 15:18:44 +08:00

342 lines
13 KiB
JavaScript

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