This commit is contained in:
288
scripts/tests/assetBindingCardRender.test.mjs
Normal file
288
scripts/tests/assetBindingCardRender.test.mjs
Normal file
@@ -0,0 +1,288 @@
|
||||
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 }) },
|
||||
'@/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/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('')
|
||||
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]), /重启设备/)
|
||||
})
|
||||
Reference in New Issue
Block a user