258 lines
9.4 KiB
JavaScript
258 lines
9.4 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 = {}, 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,一行一个"/)
|
||
})
|