318 lines
11 KiB
JavaScript
318 lines
11 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: {} },
|
|
'@/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]), /重启设备/)
|
|
})
|