merge: 合入 iteration/august(生产线)到 iteration/8-11

This commit is contained in:
邹进
2026-11-06 22:17:19 +09:00
5 changed files with 418 additions and 13 deletions

View 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,一行一个"/)
})