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(/]*@click="showRecallDialog"[^>]*>/)[0] assert.match(entry, /v-permission="'iot_card:batch_recycle'"/) assert.doesNotMatch(entry, /:disabled/) const dialog = source.match(//)[0] assert.match(dialog, /一行一个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,一行一个"/) })