Files
one-pipe-system/scripts/tests/iotCardRecallLineInput.test.mjs
luo f5730089a1
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 54s
fix: 一行一个ICCID批量回收
2026-10-29 16:18:45 +08:00

258 lines
9.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = {}, globals = {}) {
const compiled = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
const module = { exports: {} }
new Function('require', 'module', 'exports', ...Object.keys(globals), compiled)(
(name) => {
assert.ok(imports[name], `Unexpected dependency: ${name}`)
return imports[name]
},
module,
module.exports,
...Object.values(globals)
)
return module.exports
}
const read = (path) => readFileSync(path, 'utf8')
const cardTypes = evaluate(read('src/types/api/card.ts'))
const { descriptor } = parse(read('src/views/asset-management/iot-card-management/index.vue'))
const script = compileScript(descriptor, { id: 'iot-card-recall-lines' }).content
function harness() {
const writes = []
const warnings = []
const errors = []
let refreshes = 0
let response = {
code: 0,
data: { allocation_no: 'RECALL-1', total_count: 2, success_count: 1, fail_count: 1 }
}
class BaseService {
static async post(url, data) {
writes.push({ url, data: structuredClone(vue.toRaw(data)) })
return response
}
}
const { CardService } = evaluate(read('src/api/modules/card.ts'), {
'../BaseService': { BaseService }
})
CardService.getStandaloneIotCards = async () => {
refreshes += 1
return { code: 0, data: { items: [], total: 0 } }
}
const runtimeVue = { ...vue, onMounted() {} }
const imports = {
vue: runtimeVue,
'vue-router': { useRouter: () => ({ push() {} }) },
'@/router/routesAlias': { RoutesAlias: {} },
'@/config/constants/audit': { AUDIT_PERMISSIONS: {} },
'@/utils/business/auditNavigation': { resolveAuditResourceTarget() {} },
'@/components/business/audit/investigationController': { openAuditInvestigation() {} },
'@/api/modules': {
CardService,
CarrierService: { getCarriers: async () => ({ code: 0, data: { items: [] } }) }
},
'element-plus': {
ElMessage: {
warning: (message) => warnings.push(message),
error: (message) => errors.push(message)
}
},
'@element-plus/icons-vue': {},
'@/composables/useCheckedColumns': {
useCheckedColumns: () => ({ columnChecks: vue.ref([]), columns: vue.ref([]) })
},
'@/composables/useAuth': { useAuth: () => ({ hasAuth: () => true }) },
'@/store/modules/user': { useUserStore: () => ({}) },
pinia: { storeToRefs: () => ({ info: vue.ref({ user_type: 1 }) }) },
'@/utils/business/format': {},
'@/utils/business/expiryEstimate': {},
'@/utils/business/apiRateLimit': {},
'@/types/api/card': cardTypes
}
for (const name of [
'@/components/device/RealnamePolicyDialog.vue',
'@/components/business/BatchRealnamePolicyDialog.vue',
'@/components/business/UpdateRealnameStatusDialog.vue',
'@/components/business/ExportTaskCreateDialog.vue'
]) {
imports[name] = { default: {} }
}
const state = evaluate(script, imports, runtimeVue).default.setup({}, { expose() {} })
const validationErrors = []
state.recallFormRef.value = {
// Run the component's own validators for the fields mounted in the active mode.
async validate(callback) {
validationErrors.length = 0
const fields =
state.recallForm.selection_type === 'lines'
? ['iccids_text']
: state.recallForm.selection_type === 'range'
? ['iccid_start', 'iccid_end']
: []
for (const field of fields) {
for (const rule of state.recallRules[field]) {
rule.validator(rule, state.recallForm[field], (error) => {
if (error) validationErrors.push(error.message)
})
}
}
await callback(validationErrors.length === 0)
},
resetFields() {
// Emulate Element Plus restoring its initial selection when reopening.
state.recallForm.selection_type = 'list'
state.recallForm.iccids_text = ''
},
clearValidate() {
validationErrors.length = 0
}
}
return {
state,
writes,
warnings,
errors,
validationErrors,
refreshes: () => refreshes,
setResponse(value) {
response = value
}
}
}
test('unselected entry defaults to line input; reopening resets input without losing default', () => {
const { state } = harness()
state.showRecallDialog()
assert.equal(state.recallDialogVisible.value, true)
assert.equal(state.recallForm.selection_type, 'lines')
state.recallForm.iccids_text = 'OLD-CARD'
state.handleRecallDialogClose()
assert.equal(state.recallForm.iccids_text, '')
state.showRecallDialog()
assert.equal(state.recallForm.selection_type, 'lines')
assert.equal(state.recallForm.iccids_text, '')
state.selectedCards.value = [{ iccid: 'SELECTED-CARD' }]
state.showRecallDialog()
assert.equal(state.recallForm.selection_type, 'list')
})
for (const ending of ['\n', '\r\n']) {
test(`line mode trims and skips blank lines, preserves strings, and reuses list API (${JSON.stringify(ending)})`, async () => {
const { state, writes, refreshes } = harness()
state.selectedCards.value = [{ iccid: 'MUST-NOT-BE-SENT' }]
state.showRecallDialog()
Object.assign(state.recallForm, {
selection_type: 'lines',
iccids_text: [' 089860012345678901234 ', '', '\t', '89860012345678901235F'].join(ending),
iccid_start: 'OLD-START',
iccid_end: 'OLD-END',
carrier_id: 7,
batch_no: 'OLD-BATCH',
remark: '回收测试'
})
await state.handleRecall()
assert.deepEqual(writes, [
{
url: '/api/admin/iot-cards/standalone/recall',
data: {
selection_type: 'list',
iccids: ['089860012345678901234', '89860012345678901235F'],
remark: '回收测试'
}
}
])
assert.equal(state.recallDialogVisible.value, false)
assert.equal(state.resultDialogVisible.value, true)
assert.equal(state.resultTitle.value, '批量回收结果')
assert.equal(state.allocationResult.value.success_count, 1)
assert.equal(state.allocationResult.value.fail_count, 1)
assert.deepEqual(state.selectedCards.value, [])
assert.equal(state.recallLoading.value, false)
assert.equal(refreshes(), 1)
})
}
for (const text of ['', ' \r\n \t\n']) {
test(`empty input is rejected without a request (${JSON.stringify(text)})`, async () => {
const { state, writes, validationErrors } = harness()
state.showRecallDialog()
state.recallForm.iccids_text = text
await state.handleRecall()
assert.deepEqual(writes, [])
assert.deepEqual(validationErrors, ['请输入ICCID,一行一个'])
assert.equal(state.recallDialogVisible.value, true)
assert.equal(state.recallLoading.value, false)
})
}
for (const [mode, expected] of [
['list', { iccids: ['SELECTED-CARD'] }],
['range', { iccid_start: '89860000000000000000', iccid_end: '89860000000000000999' }],
['filter', { carrier_id: 7, batch_no: 'BATCH-1' }]
]) {
test(`switching from line input to ${mode} preserves its existing parameters`, async () => {
const { state, writes } = harness()
state.selectedCards.value = [{ iccid: 'SELECTED-CARD' }]
state.showRecallDialog()
Object.assign(state.recallForm, {
selection_type: 'lines',
iccids_text: 'MANUAL-CARD',
iccid_start: '89860000000000000000',
iccid_end: '89860000000000000999',
carrier_id: 7,
batch_no: 'BATCH-1'
})
state.recallForm.selection_type = mode
assert.equal(state.recallRangeCountText.value, '1,000')
await state.handleRecall()
assert.deepEqual(writes[0].data, { selection_type: mode, remark: '', ...expected })
})
}
test('list mode still rejects an empty table selection', async () => {
const { state, writes, warnings } = harness()
state.showRecallDialog()
state.recallForm.selection_type = 'list'
await state.handleRecall()
assert.deepEqual(writes, [])
assert.deepEqual(warnings, ['请先选择要回收的卡'])
})
test('failed requests retain manual input for retry and do not refresh the table', async () => {
const { state, errors, setResponse, refreshes } = harness()
state.showRecallDialog()
state.recallForm.iccids_text = 'RETRY-CARD'
setResponse({ code: 1, msg: '回收失败' })
await state.handleRecall()
assert.deepEqual(errors, ['回收失败'])
assert.equal(state.recallForm.iccids_text, 'RETRY-CARD')
assert.equal(state.recallDialogVisible.value, true)
assert.equal(state.recallLoading.value, false)
assert.equal(refreshes(), 0)
})
test('template keeps recall permission, enables its entry, and binds the labeled textarea', () => {
const source = descriptor.template.content
const entry = source.match(/<ElButton\b[^>]*@click="showRecallDialog"[^>]*>/)[0]
assert.match(entry, /v-permission="'iot_card:batch_recycle'"/)
assert.doesNotMatch(entry, /:disabled/)
const dialog = source.match(/<ElDialog\s+v-model="recallDialogVisible"[\s\S]*?<\/ElDialog>/)[0]
assert.match(dialog, /<ElRadio label="lines">一行一个ICCID<\/ElRadio>/)
assert.match(
dialog,
/v-if="recallForm\.selection_type === 'lines'"\s+label="ICCID"\s+prop="iccids_text"/
)
assert.match(dialog, /v-model="recallForm\.iccids_text"\s+type="textarea"/)
assert.match(dialog, /placeholder="请输入ICCID,一行一个"/)
})