Smail/test/review/rule-frontend.test.mjs
smail c82be24178
Some checks are pending
Security Checks / security (push) Waiting to run
修复全量测试竞态并更新审查夹具,保存证书覆盖调整前基线
2026-09-20 01:25:34 +08:00

101 lines
5.3 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { compileFunction } from 'node:vm'
import { createRequire } from 'node:module'
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
const Vue = require('vue')
const { parse, compileScript } = require('@vue/compiler-sfc')
const read = extension => readFileSync(new URL(`../../frontend/src/views/components/RuleSettings/RuleSettings.${extension}`, import.meta.url), 'utf8')
test('规则编辑取消后,新建按钮必须创建独立规则且不能污染原列表', async () => {
const source = read('js').replace(/^import .*$/gm, '')
.replace('export function usePage', 'function usePage').replace(/export \{[^}]+\}\s*$/, '')
const original = {
id: 7, name: '原有规则', sort: 0, action: 1, params: '',
rules: [{ field: 'Subject', type: 'contains', rule: 'original' }]
}
const requests = []
const transport = {
post: async (url, body) => {
if (url.endsWith('/get')) return { errorNo: 0, data: [structuredClone(original)] }
requests.push({ url, body: JSON.parse(JSON.stringify(body)) })
return { errorNo: 0, data: '' }
},
get: async () => ({ errorNo: 0, data: [] })
}
const lang = { cancel: '取消', submit: '提交', new_rule: '新建规则' }
const usePage = compileFunction(source + '\nreturn usePage', [
'reactive', 'ref', 'lang', 'http', 'ElNotification', 'Delete', 'Edit', 'InfoFilled', 'Plus',
'computed', 'useGlobalStatusStore'
])(Vue.reactive, Vue.ref, lang, transport, () => {}, {}, {}, {}, {},
Vue.computed, () => ({ hasCapability: () => true }))
const { descriptor, errors } = parse(read('vue'))
assert.deepEqual(errors, [])
const compiled = compileScript(descriptor, { id: 'audit-rules', inlineTemplate: true })
const script = compiled.content
.replace(/import \{([^}]+)\} from ["']vue["']/g, (_, fields) => `const {${fields.replace(/\bas\b/g, ':')}} = Vue`)
.replace(/^import .*$/gm, '').replace('export default', 'return')
let page
const paginationSource = readFileSync(new URL('../../frontend/src/views/components/ConfigSettings/ConfigPagination/ConfigPagination.js', import.meta.url), 'utf8')
const usePagination = compileFunction(paginationSource.replace(/^import .*$/gm, '').replace('export function', 'function')
+ '\nreturn usePagination', ['computed', 'ref', 'watch'])(Vue.computed, Vue.ref, Vue.watch)
const component = compileFunction(script, ['Vue', 'usePage', 'Plus', 'usePagination', 'ConfigPagination', 'MailPolicySettings'])(
Vue, () => { page = usePage(); return page }, {}, usePagination, { render: () => null }, { render: () => null }
)
const node = tag => ({ tag, children: [] })
const renderer = Vue.createRenderer({
createElement: node, createText: text => ({ ...node('text'), text }), createComment: node,
setText: (item, text) => { item.text = text }, setElementText: (item, text) => { item.text = text },
parentNode: item => item.parent, nextSibling: () => null, remove() {},
insert: (item, parent) => { item.parent = parent; parent.children.push(item) },
patchProp: (item, key, previous, value) => { item[key] = value }
})
const app = renderer.createApp(component)
app.component('ElTable', { setup: () => () => Vue.h('audit-table') })
// 保留真实页面模板和点击处理器,只用简单容器呈现弹窗插槽。
app.component('ElDialog', {
setup(props, { slots }) {
return () => Vue.h('audit-dialog', [...(slots.default?.() || []), ...(slots.footer?.() || [])])
}
})
app.config.warnHandler = () => {}
const root = node('root')
app.mount(root)
const flatten = item => [item, ...item.children.flatMap(flatten)]
const textOf = item => (item.text || '') + item.children.map(textOf).join('')
try {
await Vue.nextTick()
await Vue.nextTick()
assert.equal(page.data.value.length, 1)
page.editRule(page.data.value[0])
page.addRuleForm.rules[0].rule = 'unsaved'
await Vue.nextTick()
const buttons = flatten(root).filter(item => item.tag === 'el-button')
const cancel = buttons.find(item => textOf(item).trim() === lang.cancel)
const create = buttons.find(item => item.class === 'add-btn')
console.log(`rule_buttons=${JSON.stringify(buttons.map(item => ({ text: textOf(item), class: item.class, click: typeof item.onClick })))}`)
assert.equal(typeof cancel?.onClick, 'function')
assert.equal(typeof create?.onClick, 'function')
cancel.onClick()
await Vue.nextTick()
assert.equal(page.dialogVisible.value, false)
const cancelledListRule = page.data.value[0].rules[0].rule
create.onClick()
await Vue.nextTick()
assert.equal(page.dialogVisible.value, true)
const newRuleID = page.addRuleForm.id
page.addRuleForm.name = '用户想新建的规则'
buttons.find(item => textOf(item).trim() === lang.submit).onClick()
await Vue.nextTick()
assert.equal(requests.length, 1)
console.log(`rule_actual=${JSON.stringify({ cancelledListRule, newRuleID, request: requests[0] })}`)
assert.deepEqual({ cancelledListRule, newRuleID, url: requests[0].url }, {
cancelledListRule: 'original', newRuleID: 0, url: '/api/v1/rule/add'
}, '取消必须丢弃编辑草稿,新建必须重置旧规则 ID')
} finally {
app.unmount()
}
})