Files
one-pipe-system/scripts/tests/assetBindingCardRender.test.mjs
邹进 984db9889c
Some checks failed
构建并部署前端到测试环境 / deploy (push) Failing after 7m27s
feat: 流量记录查询、套餐人工冲正与退款资产后处理
- 新增采集成功/失败记录、卡日流量、套餐日流量详单、冲正流水、退款后处理尝试记录 6 个查询页
- 资产详情新增采集记录、冲正记录与人工冲正,同步后展示本次流量入账结果
- 退款列表与详情展示资产后处理状态,支持按状态筛选、人工重试与查看尝试记录
- 新增前端权限码常量(需在 tb_permission 补数据)
2026-11-07 13:19:26 +09:00

327 lines
12 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
import * as vue from 'vue'
import { compileScript, parse } from '@vue/compiler-sfc'
function evaluate(source, imports) {
const compiled = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
const module = { exports: {} }
new Function('require', 'module', 'exports', compiled)(
(name) => {
assert.ok(imports[name], `Unexpected runtime dependency: ${name}`)
return imports[name]
},
module,
module.exports
)
return module.exports
}
const basePath = 'src/views/asset-management/asset-information'
const { descriptor } = parse(readFileSync(`${basePath}/components/BasicInfoCard.vue`, 'utf8'))
const componentSource = compileScript(descriptor, {
id: 'binding-card-test',
inlineTemplate: true
}).content
const formatters = evaluate(readFileSync(`${basePath}/composables/useAssetFormatters.ts`, 'utf8'), {
'@/config/constants/carrierTypes': {
getCarrierTypeLabel: (code) => ({ CTCC: '中国电信', CMCC: '中国移动' })[code] || code
}
})
function createHostNode(tag, text = '') {
return {
tag,
text,
props: {},
children: [],
parentNode: null,
removeChild(child) {
const index = this.children.indexOf(child)
assert.notEqual(index, -1)
this.children.splice(index, 1)
child.parentNode = null
}
}
}
function mountBasicInfo(t, userType, permissions = []) {
const user = vue.reactive({ info: { id: userType, user_type: userType }, permissions })
const hasAuth = (permission) =>
Number(user.info.user_type) === 1 || user.permissions.includes(permission)
user.hasPermission = hasAuth
user.hasButton = hasAuth
const elements = Object.fromEntries(
[
'ElCard',
'ElDescriptions',
'ElDescriptionsItem',
'ElTag',
'ElTooltip',
'ElSwitch',
'ElDivider',
'ElButton',
'ElAlert',
'ElEmpty'
].map((name) => [
name,
vue.defineComponent({
name,
setup:
(_, { slots, attrs }) =>
() =>
vue.h(name, attrs, [slots.header?.(), slots.default?.()])
})
])
)
const component = evaluate(componentSource, {
vue,
'@vueuse/core': { useWindowSize: () => ({ width: vue.ref(1200) }) },
'element-plus': { ...elements, ElMessageBox: {}, ElMessage: {} },
'@element-plus/icons-vue': {
CopyDocument: vue.defineComponent(() => () => vue.h('copy-icon'))
},
'../composables/useAssetFormatters': formatters,
'@/composables/useAuth': { useAuth: () => ({ hasAuth }) },
'@/components/business/package-silence/SilenceRecordsDialog.vue': {
default: vue.defineComponent(() => () => null)
},
'@/store/modules/user': { useUserStore: () => user },
'@/config/constants': {
JULY_PERMISSIONS: { speedTier: { view: 'speed:view', update: 'speed:update' } }
},
'@/utils/business/format': { formatDateTime: (value) => String(value || '') },
'@/utils/business/packageSilence': evaluate(
readFileSync('src/utils/business/packageSilence.ts', 'utf8')
),
'@/utils/business/expiryEstimate': {
getExpiryEstimateText: () => '-',
getExpiryEstimateTooltip: () => '',
getExpiryEstimateClass: () => ''
},
'@/config/constants/audit': { AUDIT_PERMISSIONS: {} },
'@/config/constants/trafficRecords': {
TRAFFIC_RECORD_PERMISSIONS: {
assetCollections: {
card: 'iot_info:traffic_collections',
device: 'device_info:traffic_collections'
}
}
},
'@/utils/business/trafficRecords': { isEnterpriseAccount: (type) => type === 4 },
'@/utils/business/auditNavigation': { resolveAuditResourceTarget: () => null },
'@/components/business/audit/investigationController': { openAuditInvestigation: () => {} }
}).default
const renderer = vue.createRenderer({
createElement: (tag) => createHostNode(tag),
createText: (text) => createHostNode('#text', text),
createComment: (text) => createHostNode('#comment', text),
setText: (node, text) => {
node.text = text
},
setElementText: (node, text) => {
node.text = text
node.children = []
},
parentNode: (node) => node.parentNode,
nextSibling: (node) => {
const siblings = node.parentNode?.children || []
return siblings[siblings.indexOf(node) + 1] || null
},
patchProp: (node, key, _, value) => {
node.props[key] = value
},
insert: (node, parent, anchor) => {
node.parentNode?.removeChild(node)
const index = anchor ? parent.children.indexOf(anchor) : parent.children.length
assert.notEqual(index, -1, 'Vue tried to insert beside a node removed outside its renderer')
parent.children.splice(index, 0, node)
node.parentNode = parent
},
remove: (node) => node.parentNode?.removeChild(node)
})
const asset = vue.reactive({
asset_type: 'device',
asset_id: 101,
identifier: 'DEVICE-A',
cards: [
{
iccid: 'CARD-1',
msisdn: '13800000001',
carrier_name: '平台运营商账户',
carrier_type: 'CTCC',
slot_position: 1,
is_current: true,
network_status: 1
},
{
iccid: 'CARD-2',
msisdn: '13800000002',
carrier_name: '第二运营商账户',
carrier_type: 'CMCC',
slot_position: 2,
is_current: false,
network_status: 0
}
]
})
const root = createHostNode('root')
const app = renderer.createApp({
render: () => vue.h(component, { cardInfo: asset, deviceRealtime: null, pollingEnabled: false })
})
const directives = evaluate(readFileSync('src/directives/permission.ts', 'utf8'), {
'@/store/modules/user': { useUserStore: () => user }
})
directives.setupPermissionDirective(app)
app.directive('loading', {})
app.component(
'el-icon',
vue.defineComponent({
setup:
(_, { slots }) =>
() =>
vue.h('icon', slots.default?.())
})
)
app.mount(root)
t.after(() => app.unmount())
return { root, asset, user }
}
const textOf = (node) =>
node.tag === '#comment' ? '' : node.text + node.children.map(textOf).join('')
test('enterprise account hides polling and realname writes while authorized start/stop remain', async (t) => {
const { root, asset, user } = mountBasicInfo(t, 4)
user.permissions = [
'device_info:auto_polling',
'iot_info:auto_polling',
'iot_info:update_realname_status',
'bound_card:update_realname_status',
'iot_info:start',
'iot_info:stop'
]
await vue.nextTick()
assert.doesNotMatch(textOf(root), /自动轮询|更新实名状态/)
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
asset.asset_type = 'card'
asset.network_status = 0
await vue.nextTick()
assert.match(textOf(root), /启用此卡/)
assert.doesNotMatch(textOf(root), /自动轮询|更新实名状态/)
asset.network_status = 1
await vue.nextTick()
assert.match(textOf(root), /停用此卡/)
})
const nodesWithClass = (node, className) => [
...(String(node.props.class || '')
.split(' ')
.includes(className)
? [node]
: []),
...node.children.flatMap((child) => nodesWithClass(child, className))
]
test('all account types render both binding cards; only the established carrier label rule differs', async (t) => {
for (const userType of [1, 2, 3, 4]) {
const { root } = mountBasicInfo(t, userType)
await vue.nextTick()
const cards = nodesWithClass(root, 'binding-card')
assert.equal(cards.length, 2)
assert.match(textOf(cards[0]), /CARD-1/)
assert.match(textOf(cards[1]), /CARD-2/)
assert.match(textOf(cards[0]), /13800000001/)
assert.match(textOf(cards[0]), userType < 3 ? /平台运营商账户/ : /中国电信/)
}
})
test('permission changes and current-card/status updates preserve card data without external DOM removal', async (t) => {
const { root, asset, user } = mountBasicInfo(t, 3, ['iot_info:stop'])
await vue.nextTick()
assert.match(textOf(nodesWithClass(root, 'binding-card-actions')[0]), /停用此卡/)
user.permissions = ['iot_info:start', 'bound_card:update_realname_status']
asset.cards[0].network_status = 0
asset.cards[0].is_current = false
asset.cards[1].is_current = true
await vue.nextTick()
const cards = nodesWithClass(root, 'binding-card')
assert.equal(cards.length, 2)
assert.match(textOf(cards[0]), /CARD-2/)
assert.match(textOf(cards[1]), /CARD-1/)
assert.match(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /启用此卡/)
assert.match(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /更新实名状态/)
assert.doesNotMatch(textOf(nodesWithClass(cards[1], 'binding-card-actions')[0]), /停用此卡/)
user.permissions = []
await vue.nextTick()
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
for (const actions of nodesWithClass(root, 'binding-card-actions'))
assert.equal(textOf(actions).trim(), '')
})
test('missing network status renders unknown and exposes neither start nor stop', async (t) => {
const { root, asset } = mountBasicInfo(t, 3, ['iot_info:start', 'iot_info:stop'])
delete asset.cards[0].network_status
await vue.nextTick()
const card = nodesWithClass(root, 'binding-card')[0]
assert.match(textOf(card), /网络状态未知/)
assert.doesNotMatch(textOf(nodesWithClass(card, 'binding-card-actions')[0]), /启用此卡|停用此卡/)
})
test('read-only accounts render realtime start/stop transitions without losing binding-card nodes', async (t) => {
for (const userType of [2, 3, 4]) {
const { root, asset } = mountBasicInfo(t, userType)
asset.cards[0].network_status = 0
await vue.nextTick()
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /网络状态停机/)
asset.cards[0].network_status = 1
await vue.nextTick()
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /网络状态正常/)
for (const actions of nodesWithClass(root, 'binding-card-actions')) {
assert.equal(textOf(actions).trim(), '')
}
}
})
test('current-first sorting does not mutate source cards; zero, one, and multiple bindings render stably', async (t) => {
const { root, asset } = mountBasicInfo(t, 2)
const originalOrder = asset.cards.map((card) => card.iccid)
asset.cards[0].is_current = false
asset.cards[1].is_current = true
await vue.nextTick()
assert.deepEqual(
asset.cards.map((card) => card.iccid),
originalOrder
)
assert.match(textOf(nodesWithClass(root, 'binding-card')[0]), /CARD-2/)
asset.cards = [asset.cards[1]]
await vue.nextTick()
assert.equal(nodesWithClass(root, 'binding-card').length, 1)
asset.cards = []
await vue.nextTick()
assert.equal(nodesWithClass(root, 'binding-card').length, 0)
})
test('device/card type switches and denied actions do not leave removed DOM anchors', async (t) => {
const { root, asset, user } = mountBasicInfo(t, 3)
asset.asset_type = 'card'
asset.iccid = 'CARD-1'
asset.network_status = 1
await vue.nextTick()
asset.network_status = 0
await vue.nextTick()
assert.equal(textOf(nodesWithClass(root, 'card-operations')[0]).trim(), '')
user.permissions = ['iot_info:start']
await vue.nextTick()
assert.match(textOf(nodesWithClass(root, 'card-operations')[0]), /启用此卡/)
asset.asset_type = 'device'
user.permissions = ['device_info:reboot']
await vue.nextTick()
assert.equal(nodesWithClass(root, 'binding-card').length, 2)
assert.match(textOf(nodesWithClass(root, 'device-operations')[0]), /重启设备/)
})