This commit is contained in:
257
scripts/tests/iotCardRecallLineInput.test.mjs
Normal file
257
scripts/tests/iotCardRecallLineInput.test.mjs
Normal file
@@ -0,0 +1,257 @@
|
||||
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,一行一个"/)
|
||||
})
|
||||
Reference in New Issue
Block a user