fix:some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-21 09:54:21 +08:00
parent 22e796ba23
commit d82e716d33
3 changed files with 548 additions and 75 deletions

View File

@@ -0,0 +1,363 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import * as vue from 'vue'
import { renderToString } from 'vue/server-renderer'
import { compileScript, parse } from '@vue/compiler-sfc'
import ts from 'typescript'
const { descriptor } = parse(readFileSync('src/views/settings/wecom/scenes/index.vue', 'utf8'))
const compiled = ts.transpileModule(
compileScript(descriptor, { id: 'wecom-mappings-test' }).content,
{
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}
).outputText
const field = (code, name = code, required_mapping = false) => ({
code,
name,
required_mapping,
value_type: 'string',
description: `${name}说明`
})
const refundFields = [
['refund_no', '退款单号'],
['order_id', '订单 ID'],
['order_no', '订单号'],
['asset_identifier', '资产标识'],
['actual_received_amount', '订单实收金额'],
['requested_refund_amount', '申请退款金额'],
['refund_voucher_key', '退款凭证'],
['refund_reason', '退款原因'],
['package_usage_id', '套餐使用记录 ID'],
['refund_asset_type', '资产类型', true],
['refund_device_type', '设备类型', true],
['refund_device_model', '设备型号', true],
['refund_package_used_mb', '当前退款套餐已用量(MB)', true],
['refund_package_total_mb', '当前退款套餐总量(MB)', true],
['refund_channel_trade_no', '原支付渠道交易流水号', true],
['refund_applicant_remark', '申请人备注', true],
['refund_method_code', '退款方式编码'],
['refund_method', '退款方式', true],
['customer_account_info', '客户收款信息'],
['customer_voucher_key', '客户收款凭证'],
['submitter_id', '提交人账号 ID'],
['submitter_name', '提交人名称']
].map((args) => field(...args))
const control = (id, required = false) => ({ id, title: `控件 ${id}`, type: 'Text', required })
const response = (data) => ({ code: 0, data })
const scene = (mapping = []) => ({
business_type: 'refund_approval',
application_id: 1,
template_id: 'template-1',
status: 1,
control_mapping: mapping
})
const settle = () => new Promise(setImmediate)
function deferred() {
let resolve, reject
const promise = new Promise((yes, no) => {
resolve = yes
reject = no
})
return { promise, resolve, reject }
}
function harness(overrides = {}) {
const calls = { fields: [], templates: [], saves: [], warnings: [], errors: [] }
const service = {
getBusinessFields: async (type) => {
calls.fields.push(type)
return response({ items: refundFields })
},
inspectTemplate: async (...args) => {
calls.templates.push(args)
return response({ name: '模板', controls: [control('text-1')] })
},
saveScene: async (...args) => {
calls.saves.push(args)
return response({})
},
getApplications: async () => response({ items: [] }),
getScenes: async () => response({ items: [] }),
...overrides
}
const imports = {
vue: { ...vue, onMounted() {} },
'element-plus': {
ElMessage: {
warning: (message) => calls.warnings.push(message),
error: (message) => calls.errors.push(message),
success() {}
},
ElTag: {}
},
'@/composables/useCheckedColumns': {
useCheckedColumns: () => ({ columns: vue.ref([]), columnChecks: vue.ref([]) })
},
'@/composables/useAuth': { useAuth: () => ({ hasAuth: () => true }) },
'@/api/modules': { WecomService: service },
'@/config/constants': { JULY_PERMISSIONS: { wecom: {} } },
'@/utils/business/format': { formatDateTime: (value) => value }
}
const module = { exports: {} }
new Function('require', 'module', 'exports', compiled)(
(name) => {
assert.ok(imports[name], `Unexpected dependency: ${name}`)
return imports[name]
},
module,
module.exports
)
const state = module.exports.default.setup({}, { expose() {} })
return { state, service, calls }
}
test('opening a new scene displays all 22 API fields before any template is synchronized', async () => {
const { state, calls } = harness()
state.createScene()
await settle()
assert.deepEqual(calls.fields, ['refund_approval'])
assert.equal(calls.templates.length, 0)
assert.equal(state.mappingRows.value.length, 22)
assert.deepEqual(
state.mappingRows.value.map((row) => row.business_field),
refundFields.map((f) => f.code)
)
for (const [index, row] of state.mappingRows.value.entries()) {
assert.equal(row.field_name, refundFields[index].name)
assert.equal(row.required_mapping, refundFields[index].required_mapping)
assert.equal(row.control_id, '')
}
})
test('selecting a business type immediately replaces old rows with all fields for that type', async () => {
const { state, service } = harness()
state.createScene()
await settle()
service.getBusinessFields = async (type) => {
assert.equal(type, 'offline_recharge_approval')
return response({ items: [field('recharge_no', '充值单号', true), field('remark', '备注')] })
}
state.sceneForm.business_type = 'offline_recharge_approval'
state.handleBusinessTypeChange()
assert.equal(state.mappingRows.value.length, 0)
await settle()
assert.deepEqual(
state.mappingRows.value.map((row) => row.business_field),
['recharge_no', 'remark']
)
})
for (const templateFirst of [false, true]) {
test(`editing retains saved bindings and option mappings when ${templateFirst ? 'template' : 'fields'} responds first`, async () => {
const fields = deferred()
const template = deferred()
const { state } = harness({
getBusinessFields: () => fields.promise,
inspectTemplate: () => template.promise
})
state.editScene(
scene([
{
business_field: 'refund_method',
control_id: 'select-1',
control_type: 'Selector',
option_mapping: { original: '原路退回' }
}
])
)
const finishFields = () => fields.resolve(response({ items: refundFields }))
const finishTemplate = () =>
template.resolve(
response({
name: '退款模板',
controls: [{ ...control('select-1'), type: 'Selector' }]
})
)
;(templateFirst ? finishTemplate : finishFields)()
await settle()
;(templateFirst ? finishFields : finishTemplate)()
await settle()
assert.equal(state.mappingRows.value.length, 22)
const mapping = state.mappingRows.value.find((row) => row.business_field === 'refund_method')
assert.equal(mapping.control_id, 'select-1')
assert.equal(mapping.control_title, '控件 select-1')
assert.equal(mapping.control_type, 'Selector')
assert.deepEqual(JSON.parse(mapping.option_mapping), { original: '原路退回' })
})
}
test('resyncing templates keeps all business rows but clears controls removed from the template', async () => {
const { state, service } = harness()
state.editScene(
scene([{ business_field: 'refund_no', control_id: 'text-1', control_type: 'Text' }])
)
await settle()
service.inspectTemplate = async () => response({ name: '新模板', controls: [control('text-2')] })
await state.syncTemplate()
assert.equal(state.mappingRows.value.length, 22)
assert.equal(state.mappingRows.value[0].control_id, '')
assert.equal(state.mappingRows.value[0].control_type, '')
})
test('required_mapping blocks saving missing business bindings; optional blank fields are not submitted', async () => {
const { state, calls } = harness({
getBusinessFields: async () =>
response({ items: [field('required', '必填字段', true), field('optional')] })
})
state.editScene(scene())
await settle()
await state.saveScene()
assert.equal(calls.saves.length, 0)
assert.match(calls.warnings.at(-1), /必填业务字段“必填字段”/)
const row = state.mappingRows.value[0]
row.control_id = 'text-1'
state.handleControlChange(row)
await state.saveScene()
assert.deepEqual(calls.saves[0], [
'refund_approval',
{
application_id: 1,
template_id: 'template-1',
status: 1,
control_mapping: [
{
business_field: 'required',
control_id: 'text-1',
control_type: 'Text',
option_mapping: {}
}
]
}
])
})
test('required template controls still require a business field independently of required_mapping', async () => {
const { state, calls } = harness({
getBusinessFields: async () => response({ items: [field('optional')] }),
inspectTemplate: async () => response({ controls: [control('required-control', true)] })
})
state.editScene(scene())
await settle()
assert.equal(state.parseMapping(), undefined)
assert.match(calls.warnings.at(-1), /必填控件/)
})
test('selecting and clearing controls updates control IDs and types without changing the business field', async () => {
const { state } = harness()
state.editScene(scene())
await settle()
const row = state.mappingRows.value[0]
row.control_id = 'text-1'
state.handleControlChange(row)
assert.equal(row.control_type, 'Text')
assert.equal(row.control_title, '控件 text-1')
row.control_id = ''
state.handleControlChange(row)
assert.equal(row.control_type, '')
assert.equal(row.control_title, '')
assert.equal(row.business_field, 'refund_no')
})
test('out-of-order business field responses cannot overwrite a newer business type', async () => {
const older = deferred()
const newer = deferred()
const { state } = harness({
getBusinessFields: (type) => (type === 'refund_approval' ? older.promise : newer.promise)
})
state.createScene()
state.sceneForm.business_type = 'offline_recharge_approval'
state.handleBusinessTypeChange()
newer.resolve(response({ items: [field('new-type')] }))
await settle()
older.resolve(response({ items: refundFields }))
await settle()
assert.equal(state.mappingRows.value[0].business_field, 'new-type')
assert.equal(state.fieldsLoading.value, false)
})
test('resetting the editor rejects pending responses even when the business type is unchanged', async () => {
const pending = deferred()
const { state, service } = harness({ getBusinessFields: () => pending.promise })
state.createScene()
service.getBusinessFields = async () => response({ items: [field('new-session')] })
state.createScene()
await settle()
pending.resolve(response({ items: refundFields }))
await settle()
assert.equal(state.mappingRows.value[0].business_field, 'new-session')
})
test('changing template settings rejects pending template responses and clears old control bindings', async () => {
const pending = deferred()
const { state } = harness({ inspectTemplate: () => pending.promise })
state.editScene(scene())
await settle()
state.sceneForm.template_id = 'new-template'
state.handleTemplateChange()
pending.resolve(response({ name: '过期模板', controls: [control('old-control')] }))
await settle()
assert.equal(state.templateControls.value.length, 0)
assert.equal(state.templateLoaded.value, false)
assert.equal(state.mappingRows.value.length, 22)
})
test('loading or failed field discovery cannot save an empty incomplete mapping', async (t) => {
const pending = deferred()
const { state, calls } = harness({ getBusinessFields: () => pending.promise })
t.mock.method(console, 'error', () => {})
state.sceneForm.template_id = 'template'
const loading = state.loadBusinessFields('refund_approval')
await state.saveScene()
assert.equal(calls.saves.length, 0)
pending.reject(new Error('network unavailable'))
await loading
await state.saveScene()
assert.equal(calls.saves.length, 0)
assert.equal(state.businessFieldsLoaded.value, false)
assert.equal(calls.errors.length, 1)
})
function findMappingSection(node) {
if (
node.type === 1 &&
node.props.some(
(prop) => prop.name === 'class' && prop.value?.content.includes('mapping-section')
)
)
return node
return node.children?.map(findMappingSection).find(Boolean)
}
test('rendered mapping section shows all 22 field names and exactly eight required stars before template sync', async () => {
const { state } = harness()
state.createScene()
await settle()
const section = findMappingSection(descriptor.template.ast)
const app = vue.createSSRApp({
setup: () => ({ ...state }),
render: vue.compile(section.loc.source)
})
for (const name of ['ElTag', 'ElSelect', 'ElOption', 'ElInput', 'ElEmpty']) {
app.component(
name,
vue.defineComponent({
setup:
(_, { attrs, slots }) =>
() =>
vue.h('div', attrs, slots.default?.())
})
)
}
app.directive('loading', { getSSRProps: () => ({}) })
const html = await renderToString(app)
assert.equal((html.match(/class="mapping-row"/g) || []).length, 22)
assert.equal((html.match(/aria-label="必填"/g) || []).length, 8)
for (const field of refundFields) assert.ok(html.includes(field.name), field.name)
assert.match(html, /请先同步模板/)
assert.doesNotMatch(html, /暂无控件映射,请新增一条/)
assert.doesNotMatch(html, /控件 ID|选择模板控件后自动填充/)
assert.doesNotMatch(html, /mapping-field__description/)
for (const field of refundFields) assert.ok(!html.includes(field.description), field.description)
})