This commit is contained in:
289
scripts/tests/assetBindingCardRender.test.mjs
Normal file
289
scripts/tests/assetBindingCardRender.test.mjs
Normal file
@@ -0,0 +1,289 @@
|
||||
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/realNameTime': { formatRealNameTime: (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]), /重启设备/)
|
||||
})
|
||||
744
scripts/tests/assetInfoState.test.mjs
Normal file
744
scripts/tests/assetInfoState.test.mjs
Normal file
@@ -0,0 +1,744 @@
|
||||
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 { parse } from '@vue/compiler-sfc'
|
||||
|
||||
const sourcePath = 'src/views/asset-management/asset-information/composables/useAssetInfo.ts'
|
||||
const compiledSource = ts.transpileModule(readFileSync(sourcePath, 'utf8'), {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||
}).outputText
|
||||
|
||||
const response = (data) => ({ code: 0, data })
|
||||
const deferred = () => {
|
||||
let resolve, reject
|
||||
const promise = new Promise((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
const boundCards = () => [
|
||||
{
|
||||
card_id: 11,
|
||||
iccid: 'CARD-1',
|
||||
msisdn: '13800000001',
|
||||
carrier_name: '运营商账户',
|
||||
carrier_type: 'CTCC',
|
||||
slot_position: 1,
|
||||
is_current: true,
|
||||
network_status: 1,
|
||||
real_name_status: 1,
|
||||
real_name_at: '2026-10-01T01:00:00Z',
|
||||
realname_policy: 'before_order',
|
||||
gateway_extend: 'old reason'
|
||||
},
|
||||
{
|
||||
card_id: 12,
|
||||
iccid: 'CARD-2',
|
||||
msisdn: '13800000002',
|
||||
carrier_name: '另一运营商账户',
|
||||
carrier_type: 'CMCC',
|
||||
slot_position: 2,
|
||||
is_current: false,
|
||||
network_status: 0,
|
||||
real_name_status: 0,
|
||||
real_name_at: null
|
||||
}
|
||||
]
|
||||
|
||||
const detail = (identifier = 'DEVICE-A', cards = boundCards()) => ({
|
||||
asset_type: 'device',
|
||||
asset_id: identifier === 'DEVICE-A' ? 101 : 202,
|
||||
virtual_no: identifier,
|
||||
bound_card_count: cards.length,
|
||||
cards,
|
||||
real_total_mb: 1024,
|
||||
real_used_mb: 100
|
||||
})
|
||||
|
||||
function harness(t, overrides = {}) {
|
||||
const user = vue.reactive({ info: { id: 1, user_type: 1 }, isLogin: true })
|
||||
const messages = []
|
||||
const api = {
|
||||
resolveAsset: async (identifier) => response(detail(identifier)),
|
||||
getRealtimeStatus: async () => response(null),
|
||||
getCurrentPackage: async () => response(null),
|
||||
getAssetWallet: async () => response(null),
|
||||
getAssetPackages: async () => response({ items: [], total: 0 }),
|
||||
refreshAsset: async () => response(null),
|
||||
...overrides
|
||||
}
|
||||
const imports = {
|
||||
vue,
|
||||
'element-plus': {
|
||||
ElMessage: Object.fromEntries(
|
||||
['error', 'success', 'warning'].map((type) => [
|
||||
type,
|
||||
(message) => messages.push({ type, message })
|
||||
])
|
||||
)
|
||||
},
|
||||
'@/api/modules': { AssetService: api },
|
||||
'@/utils/business/apiRateLimit': { FrontendRateLimitError: class extends Error {} },
|
||||
'@/utils/business/apiError': {
|
||||
normalizeApiError: (error) => ({ message: error?.message || 'unknown error' })
|
||||
},
|
||||
'@/store/modules/user': { useUserStore: () => user }
|
||||
}
|
||||
const module = { exports: {} }
|
||||
new Function('require', 'module', 'exports', compiledSource)(
|
||||
(name) => {
|
||||
assert.ok(imports[name], `Unexpected runtime dependency: ${name}`)
|
||||
return imports[name]
|
||||
},
|
||||
module,
|
||||
module.exports
|
||||
)
|
||||
const scope = vue.effectScope()
|
||||
const state = scope.run(() => module.exports.useAssetInfo())
|
||||
t.after(() => scope.stop())
|
||||
return { state, api, user, messages, scope }
|
||||
}
|
||||
|
||||
test('partial realtime responses retain the complete resolved binding-card roster', async (t) => {
|
||||
const { state } = harness(t, {
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-1', network_status: 0 }]
|
||||
})
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||
['CARD-1', 'CARD-2']
|
||||
)
|
||||
assert.equal(state.cardInfo.value.cards[0].network_status, 0)
|
||||
assert.equal(state.cardInfo.value.cards[1].msisdn, '13800000002')
|
||||
})
|
||||
|
||||
test('realtime fields cannot erase resolved card identity, carrier, slot, policy, or real-name time', async (t) => {
|
||||
const { state } = harness(t, {
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [
|
||||
{
|
||||
iccid: 'CARD-1',
|
||||
card_id: 0,
|
||||
msisdn: '',
|
||||
carrier_name: '',
|
||||
carrier_type: '',
|
||||
slot_position: 0,
|
||||
realname_policy: '',
|
||||
real_name_at: 'untrusted realtime time',
|
||||
network_status: 0,
|
||||
real_name_status: 0,
|
||||
is_current: false,
|
||||
gateway_extend: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(state.cardInfo.value.cards[0], {
|
||||
...boundCards()[0],
|
||||
network_status: 0,
|
||||
real_name_status: 0,
|
||||
is_current: false,
|
||||
gateway_extend: ''
|
||||
})
|
||||
})
|
||||
|
||||
test('empty or omitted realtime cards preserve the detail roster; detail reload can remove bindings', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
for (const cards of [[], undefined, null]) {
|
||||
api.getRealtimeStatus = async () => response({ asset_type: 'device', asset_id: 101, cards })
|
||||
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
assert.equal(state.cardInfo.value.cards.length, 2)
|
||||
}
|
||||
api.resolveAsset = async () => response(detail('DEVICE-A', []))
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(state.cardInfo.value.cards, [])
|
||||
})
|
||||
|
||||
test('unknown realtime cards do not introduce bindings absent from the authorized detail response', async (t) => {
|
||||
const { state } = harness(t, {
|
||||
getRealtimeStatus: async () =>
|
||||
response({ asset_type: 'device', asset_id: 101, cards: [{ iccid: 'UNRESOLVED-CARD' }] })
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||
['CARD-1', 'CARD-2']
|
||||
)
|
||||
})
|
||||
|
||||
test('realtime card order does not change the resolved roster; explicit nullable timestamps can clear', async (t) => {
|
||||
const cards = boundCards()
|
||||
cards[0].last_data_check_at = 'old timestamp'
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async () => response(detail('DEVICE-A', cards)),
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [
|
||||
{ iccid: 'CARD-2' },
|
||||
{ iccid: 'CARD-1', last_data_check_at: null, real_name_status: null }
|
||||
]
|
||||
})
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||
['CARD-1', 'CARD-2']
|
||||
)
|
||||
assert.equal(state.cardInfo.value.cards[0].last_data_check_at, null)
|
||||
assert.equal(state.cardInfo.value.cards[0].real_name_status, 1)
|
||||
})
|
||||
|
||||
test('a late resolve response cannot replace a newer search', async (t) => {
|
||||
const oldDetail = deferred()
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: (identifier) =>
|
||||
identifier === 'DEVICE-A' ? oldDetail.promise : Promise.resolve(response(detail(identifier)))
|
||||
})
|
||||
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
oldDetail.resolve(response(detail('DEVICE-A')))
|
||||
await oldSearch
|
||||
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||
})
|
||||
|
||||
test('late realtime responses cannot write old card data into a different asset', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const oldRealtime = deferred()
|
||||
api.getRealtimeStatus = () => oldRealtime.promise
|
||||
const oldLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
api.getRealtimeStatus = async () => response(null)
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
oldRealtime.resolve(
|
||||
response({ asset_type: 'device', asset_id: 101, cards: [{ iccid: 'OLD-CARD' }] })
|
||||
)
|
||||
await oldLoad
|
||||
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||
['CARD-1', 'CARD-2']
|
||||
)
|
||||
})
|
||||
|
||||
test('the newest same-asset realtime request wins and stale completion cannot end its loading state', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const older = deferred()
|
||||
const newer = deferred()
|
||||
api.getRealtimeStatus = () => older.promise
|
||||
const oldLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
api.getRealtimeStatus = () => newer.promise
|
||||
const newLoad = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
older.resolve(
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-1', network_status: 0 }]
|
||||
})
|
||||
)
|
||||
await oldLoad
|
||||
assert.equal(state.realtimeLoading.value, true)
|
||||
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||
newer.resolve(
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-1', network_status: 1 }]
|
||||
})
|
||||
)
|
||||
await newLoad
|
||||
assert.equal(state.realtimeLoading.value, false)
|
||||
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||
})
|
||||
|
||||
test('realtime responses identifying a different asset are ignored', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
for (const identity of [
|
||||
{ asset_type: 'device', asset_id: 202 },
|
||||
{ asset_type: 'card', asset_id: 101 }
|
||||
]) {
|
||||
api.getRealtimeStatus = async () =>
|
||||
response({ ...identity, cards: [{ iccid: 'CARD-1', network_status: 0 }] })
|
||||
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
assert.equal(state.cardInfo.value.cards[0].network_status, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('starting a new search clears previous device, package, wallet, and binding data immediately', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
state.deviceRealtime.value = { current_iccid: 'CARD-1' }
|
||||
state.currentPackage.value = { package_name: 'old package' }
|
||||
state.packageList.value = [{ id: 1 }]
|
||||
state.walletInfo.value = { total_balance: 123 }
|
||||
const newDetail = deferred()
|
||||
api.resolveAsset = () => newDetail.promise
|
||||
const nextSearch = state.fetchAssetDetail('DEVICE-B')
|
||||
assert.equal(state.cardInfo.value, null)
|
||||
assert.equal(state.deviceRealtime.value, null)
|
||||
assert.equal(state.currentPackage.value, null)
|
||||
assert.equal(state.walletInfo.value, null)
|
||||
assert.deepEqual(state.packageList.value, [])
|
||||
newDetail.resolve({ code: 403, msg: 'forbidden' })
|
||||
await nextSearch
|
||||
assert.equal(state.cardInfo.value, null)
|
||||
})
|
||||
|
||||
test('changing account invalidates pending requests even when searching the same identifier', async (t) => {
|
||||
const oldDetail = deferred()
|
||||
const { state, user, api } = harness(t, { resolveAsset: () => oldDetail.promise })
|
||||
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||
user.info = { id: 2, user_type: 3 }
|
||||
api.resolveAsset = async () => response(detail('DEVICE-A', [boundCards()[1]]))
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
oldDetail.resolve(response(detail('DEVICE-A')))
|
||||
await oldSearch
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.iccid),
|
||||
['CARD-2']
|
||||
)
|
||||
})
|
||||
|
||||
test('logout clears visible data and invalidates pending realtime responses', async (t) => {
|
||||
const { state, user, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const pending = deferred()
|
||||
api.getRealtimeStatus = () => pending.promise
|
||||
const load = state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
user.isLogin = false
|
||||
assert.equal(state.cardInfo.value, null)
|
||||
pending.resolve(response({ asset_type: 'device', asset_id: 101, cards: boundCards() }))
|
||||
await load
|
||||
assert.equal(state.cardInfo.value, null)
|
||||
})
|
||||
|
||||
test('a late failure of an old search does not clear the current successful asset', async (t) => {
|
||||
const oldDetail = deferred()
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: (identifier) =>
|
||||
identifier === 'DEVICE-A' ? oldDetail.promise : Promise.resolve(response(detail(identifier)))
|
||||
})
|
||||
const oldSearch = state.fetchAssetDetail('DEVICE-A')
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
oldDetail.reject(new Error('old request failed'))
|
||||
await oldSearch
|
||||
assert.equal(state.cardInfo.value?.identifier, 'DEVICE-B')
|
||||
})
|
||||
|
||||
test('account types 1/2/3/4 retain the same resolved bindings despite sparse realtime fields', async (t) => {
|
||||
for (const userType of [1, 2, 3, 4]) {
|
||||
const { state, user } = harness(t, {
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-1', carrier_name: '' }]
|
||||
})
|
||||
})
|
||||
user.info = { id: userType, user_type: userType }
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(state.cardInfo.value.cards, boundCards())
|
||||
}
|
||||
})
|
||||
|
||||
test('a current-card change clears the previous current flag even for a partial realtime list', async (t) => {
|
||||
const { state } = harness(t, {
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-2', is_current: true }]
|
||||
})
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(
|
||||
state.cardInfo.value.cards.map((card) => card.is_current),
|
||||
[false, true]
|
||||
)
|
||||
})
|
||||
|
||||
test('same-identifier re-search never takes a real-name timestamp from a previous search or realtime', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
api.resolveAsset = async () =>
|
||||
response(detail('DEVICE-A', [{ ...boundCards()[0], real_name_at: null }]))
|
||||
api.getRealtimeStatus = async () =>
|
||||
response({
|
||||
asset_type: 'device',
|
||||
asset_id: 101,
|
||||
cards: [{ iccid: 'CARD-1', real_name_at: 'stale time' }]
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.equal(state.cardInfo.value.cards[0].real_name_at, null)
|
||||
})
|
||||
|
||||
test('manual refresh resolves new binding relations and preserves the existing package page', async (t) => {
|
||||
let resolveCalls = 0
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async () => response(detail('DEVICE-A', ++resolveCalls === 1 ? boundCards() : []))
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
state.packageList.value = [{ id: 88, children: [{ id: 89 }] }]
|
||||
assert.equal(await state.refreshAsset('DEVICE-A', 'device'), true)
|
||||
assert.equal(resolveCalls, 2)
|
||||
assert.deepEqual(state.cardInfo.value.cards, [])
|
||||
assert.deepEqual(state.packageList.value, [{ id: 88, children: [{ id: 89 }] }])
|
||||
})
|
||||
|
||||
test('a pending refresh does not re-search or overwrite a newer asset', async (t) => {
|
||||
const pending = deferred()
|
||||
const calls = []
|
||||
const { state } = harness(t, {
|
||||
refreshAsset: () => pending.promise,
|
||||
resolveAsset: async (identifier) => {
|
||||
calls.push(identifier)
|
||||
return response(detail(identifier))
|
||||
}
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const refresh = state.refreshAsset('DEVICE-A', 'device')
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
pending.resolve(response(null))
|
||||
assert.equal(await refresh, false)
|
||||
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||
assert.deepEqual(calls, ['DEVICE-A', 'DEVICE-B'])
|
||||
})
|
||||
|
||||
test('late current-package, wallet, and package-list responses do not contaminate a new asset', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const pending = { package: deferred(), wallet: deferred(), list: deferred() }
|
||||
api.getCurrentPackage = () => pending.package.promise
|
||||
api.getAssetWallet = () => pending.wallet.promise
|
||||
api.getAssetPackages = () => pending.list.promise
|
||||
const oldLoads = [
|
||||
state.loadCurrentPackage('DEVICE-A'),
|
||||
state.loadAssetWallet('DEVICE-A'),
|
||||
state.loadPackageList('DEVICE-A')
|
||||
]
|
||||
api.getCurrentPackage = async () => response(null)
|
||||
api.getAssetWallet = async () => response(null)
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
pending.package.resolve(response({ package_name: 'old package' }))
|
||||
pending.wallet.resolve(response({ total_balance: 123 }))
|
||||
pending.list.resolve(response({ items: [{ id: 1 }], total: 1 }))
|
||||
const results = await Promise.all(oldLoads)
|
||||
assert.equal(state.currentPackage.value, null)
|
||||
assert.equal(state.walletInfo.value, null)
|
||||
assert.deepEqual(state.packageList.value, [])
|
||||
assert.equal(results[2], null)
|
||||
})
|
||||
|
||||
test('a newer same-asset package page wins even if the older page returns last', async (t) => {
|
||||
const { state, api } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const older = deferred()
|
||||
api.getAssetPackages = () => older.promise
|
||||
const firstPage = state.loadPackageList('DEVICE-A', 1)
|
||||
api.getAssetPackages = async () =>
|
||||
response({ items: [{ id: 2, children: [{ id: 3 }] }], total: 9, page: 2 })
|
||||
await state.loadPackageList('DEVICE-A', 2)
|
||||
older.resolve(response({ items: [{ id: 1 }], total: 1, page: 1 }))
|
||||
assert.equal(await firstPage, null)
|
||||
assert.deepEqual(state.packageList.value, [{ id: 2, children: [{ id: 3 }] }])
|
||||
})
|
||||
|
||||
test('unmount invalidates outstanding resolve requests', async (t) => {
|
||||
const pending = deferred()
|
||||
const { state, scope } = harness(t, { resolveAsset: () => pending.promise })
|
||||
const search = state.fetchAssetDetail('DEVICE-A')
|
||||
scope.stop()
|
||||
pending.resolve(response(detail('DEVICE-A')))
|
||||
assert.equal(await search, null)
|
||||
assert.equal(state.cardInfo.value, null)
|
||||
})
|
||||
|
||||
test('failed realtime loading preserves resolved bindings and a sparse identified request can update status', async (t) => {
|
||||
t.mock.method(console, 'error', () => {})
|
||||
const { state, api } = harness(t, {
|
||||
getRealtimeStatus: async () => {
|
||||
throw new Error('realtime unavailable')
|
||||
}
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
assert.deepEqual(state.cardInfo.value.cards, boundCards())
|
||||
assert.equal(state.realtimeLoading.value, false)
|
||||
api.getRealtimeStatus = async () => response({ cards: [{ iccid: 'CARD-1', network_status: 0 }] })
|
||||
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
assert.equal(state.cardInfo.value.cards[0].network_status, 0)
|
||||
assert.equal(state.cardInfo.value.cards.length, 2)
|
||||
})
|
||||
|
||||
test('updating user metadata without changing the account or data scope does not clear asset data', async (t) => {
|
||||
const { state, user } = harness(t)
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const asset = state.cardInfo.value
|
||||
user.info = { ...user.info, username: 'updated display name' }
|
||||
await vue.nextTick()
|
||||
assert.equal(state.cardInfo.value, asset)
|
||||
})
|
||||
|
||||
test('a disposed asset composable cannot restart loads after an operation finishes late', async (t) => {
|
||||
let calls = 0
|
||||
const { state, scope } = harness(t, {
|
||||
resolveAsset: async () => {
|
||||
calls += 1
|
||||
return response(detail('DEVICE-A'))
|
||||
}
|
||||
})
|
||||
scope.stop()
|
||||
assert.equal(await state.fetchAssetDetail('DEVICE-A'), null)
|
||||
assert.equal(calls, 0)
|
||||
})
|
||||
|
||||
test('standalone card realtime status still updates while the resolved gateway IMEI remains authoritative', async (t) => {
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async () =>
|
||||
response({
|
||||
...detail('CARD-1', []),
|
||||
asset_type: 'card',
|
||||
iccid: 'CARD-1',
|
||||
gateway_card_imei: 'resolved-imei'
|
||||
}),
|
||||
getRealtimeStatus: async () =>
|
||||
response({
|
||||
asset_type: 'card',
|
||||
asset_id: 202,
|
||||
network_status: 0,
|
||||
real_name_status: 1,
|
||||
gateway_card_imei: 'realtime-imei'
|
||||
})
|
||||
})
|
||||
await state.fetchAssetDetail('CARD-1')
|
||||
assert.equal(state.cardInfo.value.network_status, 0)
|
||||
assert.equal(state.cardInfo.value.real_name_status, 1)
|
||||
assert.equal(state.cardInfo.value.gateway_card_imei, 'resolved-imei')
|
||||
})
|
||||
|
||||
function pageHandlers(state) {
|
||||
const { descriptor } = parse(
|
||||
readFileSync('src/views/asset-management/asset-information/index.vue', 'utf8')
|
||||
)
|
||||
const source = ts.createSourceFile(
|
||||
'asset-information.ts',
|
||||
descriptor.scriptSetup.content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
)
|
||||
const wanted = new Set([
|
||||
'handleSearch',
|
||||
'handleRefresh',
|
||||
'handleSpeedTierConfirm',
|
||||
'showUpdateRealnameStatusDialog',
|
||||
'handleUpdateRealnameStatusSuccess',
|
||||
'handleUpdateBindingCardRealnameStatus',
|
||||
'handleBindingCardRealnameStatusSuccess'
|
||||
])
|
||||
const declarations = []
|
||||
function visit(node) {
|
||||
if (ts.isVariableDeclaration(node) && wanted.has(node.name.getText(source)))
|
||||
declarations.push(node.getText(source))
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
assert.equal(declarations.length, wanted.size)
|
||||
const bindings = {
|
||||
cardInfo: state.cardInfo,
|
||||
fetchAssetDetail: state.fetchAssetDetail,
|
||||
loadPackageList: state.loadPackageList,
|
||||
refreshAsset: state.refreshAsset,
|
||||
refreshSubmitting: vue.ref(false),
|
||||
refreshDisabled: vue.ref(false),
|
||||
updateRateLimitNow: () => {},
|
||||
loadRealtimeStatus: state.loadRealtimeStatus,
|
||||
hasAuth: () => true,
|
||||
JULY_PERMISSIONS: { speedTier: { update: 'speed:update' } },
|
||||
CardService: {
|
||||
setSpeedTier: async () => response({ speed_tier_name: 'test tier' })
|
||||
},
|
||||
ElMessage: { success: () => {}, error: () => {} },
|
||||
normalizeApiError: (error) => ({ message: error?.message || 'unknown error' }),
|
||||
speedLimitDialogVisible: vue.ref(false),
|
||||
speedTierSubmitting: vue.ref(false),
|
||||
assetSearchCardRef: vue.ref(null),
|
||||
startFailureMessage: vue.ref(''),
|
||||
packagePagination: vue.ref({ page: 1, total: 0, pageSize: 5, status: undefined }),
|
||||
walletTransactionSearchToken: vue.ref(0),
|
||||
pollingEnabled: vue.ref(false),
|
||||
updateRealnameStatusDialogVisible: vue.ref(false),
|
||||
bindingCardRealnameStatusDialogVisible: vue.ref(false),
|
||||
bindingCardRealnameStatusIccid: vue.ref(''),
|
||||
bindingCardRealnameStatusValue: vue.ref(0),
|
||||
nextTick: vue.nextTick
|
||||
}
|
||||
const compiled = ts.transpileModule(
|
||||
`let searchRequestId = 0, pollingInitializing = false, realnameStatusAsset = null, bindingCardRealnameStatusAsset = null\n${declarations.map((declaration) => `const ${declaration}`).join('\n')}\nreturn { ${[...wanted].join(', ')} }`,
|
||||
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } }
|
||||
).outputText
|
||||
return {
|
||||
...new Function(...Object.keys(bindings), compiled)(...Object.values(bindings)),
|
||||
...bindings
|
||||
}
|
||||
}
|
||||
|
||||
test('page search resolves a device only once and uses its canonical identifier for related requests', async (t) => {
|
||||
const calls = []
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async (identifier) => {
|
||||
calls.push(['resolve', identifier])
|
||||
return response(detail('DEVICE-A'))
|
||||
},
|
||||
getRealtimeStatus: async (identifier) => {
|
||||
calls.push(['realtime', identifier])
|
||||
return response(null)
|
||||
},
|
||||
getAssetPackages: async (identifier) => {
|
||||
calls.push(['packages', identifier])
|
||||
return response({ items: [], total: 0 })
|
||||
}
|
||||
})
|
||||
await pageHandlers(state).handleSearch({ identifier: 'IMEI-ALIAS' })
|
||||
assert.deepEqual(calls, [
|
||||
['resolve', 'IMEI-ALIAS'],
|
||||
['realtime', 'DEVICE-A'],
|
||||
['packages', 'DEVICE-A']
|
||||
])
|
||||
})
|
||||
|
||||
test('active gateway synchronization is called only by the sync button, never by searches or data reloads', async (t) => {
|
||||
const syncCalls = []
|
||||
const { state } = harness(t, {
|
||||
refreshAsset: async (identifier) => {
|
||||
syncCalls.push(identifier)
|
||||
return response(null)
|
||||
}
|
||||
})
|
||||
const page = pageHandlers(state)
|
||||
await page.handleRefresh()
|
||||
await page.handleSearch({ identifier: 'DEVICE-A' })
|
||||
await page.handleSearch({ identifier: 'DEVICE-A' })
|
||||
await state.loadRealtimeStatus('DEVICE-A', 'device')
|
||||
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||
await page.handleBindingCardRealnameStatusSuccess()
|
||||
page.showUpdateRealnameStatusDialog()
|
||||
await page.handleUpdateRealnameStatusSuccess()
|
||||
assert.deepEqual(syncCalls, [])
|
||||
|
||||
await page.handleRefresh()
|
||||
assert.deepEqual(syncCalls, ['DEVICE-A'])
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
page.refreshDisabled.value = true
|
||||
await page.handleRefresh()
|
||||
assert.deepEqual(syncCalls, ['DEVICE-A'])
|
||||
})
|
||||
|
||||
test('successful speed-tier updates reload saved status without calling active gateway synchronization', async (t) => {
|
||||
const syncCalls = []
|
||||
const realtimeCalls = []
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async () =>
|
||||
response({ ...detail('CARD-1', []), asset_type: 'card', iccid: 'CARD-1' }),
|
||||
refreshAsset: async (identifier) => {
|
||||
syncCalls.push(identifier)
|
||||
return response(null)
|
||||
},
|
||||
getRealtimeStatus: async (identifier) => {
|
||||
realtimeCalls.push(identifier)
|
||||
return response(null)
|
||||
}
|
||||
})
|
||||
await state.fetchAssetDetail('CARD-1')
|
||||
realtimeCalls.length = 0
|
||||
await pageHandlers(state).handleSpeedTierConfirm({ code: 1 })
|
||||
assert.deepEqual(syncCalls, [])
|
||||
assert.deepEqual(realtimeCalls, ['CARD-1'])
|
||||
})
|
||||
|
||||
test('main retains the legacy 19-digit ICCID fallback without triggering synchronization', async (t) => {
|
||||
const identifiers = []
|
||||
let syncCalls = 0
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: async (identifier) => {
|
||||
identifiers.push(identifier)
|
||||
if (identifier.length === 19) return { code: 1006, msg: 'not found' }
|
||||
return response({ ...detail('CARD-1', []), asset_type: 'card', iccid: identifier })
|
||||
},
|
||||
refreshAsset: async () => {
|
||||
syncCalls += 1
|
||||
return response(null)
|
||||
}
|
||||
})
|
||||
await state.fetchAssetDetail('8986000000000000000')
|
||||
assert.equal(identifiers.length, 2)
|
||||
assert.equal(identifiers[0].length, 19)
|
||||
assert.equal(identifiers[1].length, 20)
|
||||
assert.equal(state.cardInfo.value.identifier, identifiers[1])
|
||||
assert.equal(syncCalls, 0)
|
||||
})
|
||||
|
||||
test('a stale page search cannot start additional loads or reset the newer search pagination', async (t) => {
|
||||
const pending = deferred()
|
||||
const { state } = harness(t, {
|
||||
resolveAsset: (identifier) =>
|
||||
identifier === 'DEVICE-A' ? pending.promise : Promise.resolve(response(detail(identifier))),
|
||||
getAssetPackages: async () => response({ items: [{ id: 2 }], total: 22 })
|
||||
})
|
||||
const page = pageHandlers(state)
|
||||
const older = page.handleSearch({ identifier: 'DEVICE-A' })
|
||||
await page.handleSearch({ identifier: 'DEVICE-B' })
|
||||
pending.resolve(response(detail('DEVICE-A')))
|
||||
await older
|
||||
assert.equal(page.packagePagination.value.total, 22)
|
||||
assert.equal(page.walletTransactionSearchToken.value, 1)
|
||||
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||
})
|
||||
|
||||
test('successful bound-card real-name update reloads detail fields but an old dialog cannot reload a new asset', async (t) => {
|
||||
let resolveCalls = 0
|
||||
const { state, api } = harness(t, {
|
||||
resolveAsset: async (identifier) => {
|
||||
resolveCalls += 1
|
||||
return response(detail(identifier))
|
||||
}
|
||||
})
|
||||
await state.fetchAssetDetail('DEVICE-A')
|
||||
const page = pageHandlers(state)
|
||||
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||
api.resolveAsset = async () => {
|
||||
resolveCalls += 1
|
||||
return response(
|
||||
detail('DEVICE-A', [{ ...boundCards()[0], real_name_status: 0, real_name_at: null }])
|
||||
)
|
||||
}
|
||||
await page.handleBindingCardRealnameStatusSuccess()
|
||||
assert.equal(state.cardInfo.value.cards[0].real_name_status, 0)
|
||||
assert.equal(state.cardInfo.value.cards[0].real_name_at, null)
|
||||
assert.equal(resolveCalls, 2)
|
||||
api.resolveAsset = async (identifier) => {
|
||||
resolveCalls += 1
|
||||
return response(detail(identifier))
|
||||
}
|
||||
page.handleUpdateBindingCardRealnameStatus({ card: boundCards()[0] })
|
||||
await state.fetchAssetDetail('DEVICE-B')
|
||||
await page.handleBindingCardRealnameStatusSuccess()
|
||||
assert.equal(state.cardInfo.value.identifier, 'DEVICE-B')
|
||||
assert.equal(resolveCalls, 3)
|
||||
})
|
||||
Reference in New Issue
Block a user