Files
one-pipe-system/scripts/tests/wecomSceneMappings.test.mjs
luo d82e716d33
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s
fix:some
2026-10-21 09:54:21 +08:00

364 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})