完善企业编辑页面并修复采购建单重试

This commit is contained in:
2026-10-30 15:42:38 +08:00
parent d6ef770e48
commit 55868177cd
19 changed files with 568 additions and 224 deletions
@@ -0,0 +1,170 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import test from 'node:test'
import ts from 'typescript'
import { createMemoryHistory, createRouter } from 'vue-router'
import { computed } from 'vue'
const memberSource = readFileSync(new URL('./MemberEditorView.vue', import.meta.url), 'utf8')
const profileSource = readFileSync(new URL('./CompanyProfileEditView.vue', import.meta.url), 'utf8')
const listSource = readFileSync(new URL('./MembersView.vue', import.meta.url), 'utf8')
const rolesSource = readFileSync(new URL('./RolesView.vue', import.meta.url), 'utf8')
const transpile = source => ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const compile = (source, bindings, result) => new Function(...Object.keys(bindings), transpile(source) + `\nreturn ${result}`)(...Object.values(bindings))
test('editor routes use their write permissions, have no sidebar entries, and preserve member IDs on refresh', () => {
const source = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8').replace(/^import .*$/gm, '').replace('export const', 'const')
const module = compile(source, { Building2: {}, KeyRound: {}, Settings: {}, ShieldCheck: {}, Users: {} }, 'tenantManagementModule')
const routes = module.routes.map(({ component, ...route }) => ({ ...route, component: {} }))
const router = createRouter({ history: createMemoryHistory(), routes: routes.map(route => ({ ...route, path: '/' + route.path })) })
for (const [name, path, permission] of [
['member-create', '/organization/members/create', 'company:member:create'],
['member-edit', '/organization/members/1234567890123456789/edit', 'company:member:edit'],
['company-profile-edit', '/company/profile/edit', 'company:profile:edit'],
]) {
const resolved = router.resolve(path)
assert.equal(resolved.name, name)
assert.equal(resolved.meta.permission, permission)
assert.equal(resolved.meta.keepAlive, false)
assert.equal(module.routes.find(route => route.name === name).navigation, undefined)
}
assert.equal(router.resolve('/organization/members/1234567890123456789/edit').params.memberId, '1234567890123456789')
assert.doesNotMatch(listSource, /<el-drawer/)
assert.match(listSource, /name: 'member-edit', params: \{ memberId: member.id \}/)
assert.match(rolesSource, /<el-dialog v-model="editorOpen"/)
assert.doesNotMatch(rolesSource, /<el-drawer/)
})
function memberLoader(id = '') {
const context = {
editingId: { value: id }, loading: { value: false }, loadError: { value: '' },
form: { password: 'old-input', roleIds: [] }, formRef: { value: { clearValidate() {} } },
store: { members: [], async loadRoles() {}, async loadMembers() {} },
}
const source = memberSource.slice(memberSource.indexOf('let loadVersion ='), memberSource.indexOf('onMounted('))
return { ...context, load: compile(source, context, 'loadEditor') }
}
test('direct member edit loads current fields and roles; unknown or failed records do not leave an editable blank form', async () => {
const state = memberLoader('member-a')
state.store.loadMembers = async () => { state.store.members = [{ id: 'member-a', name: '成员甲', account: 'member.a', phone: '', email: '', roleIds: ['role-a'] }] }
await state.load()
assert.equal(state.form.name, '成员甲')
assert.deepEqual(state.form.roleIds, ['role-a'])
assert.equal(state.form.password, '')
state.form.roleIds.push('role-b')
assert.deepEqual(state.store.members[0].roleIds, ['role-a'])
state.editingId.value = 'missing'
await state.load()
assert.match(state.loadError.value, /不存在或已不可访问/)
assert.equal(state.form.name, '')
state.store.loadMembers = async () => { throw new Error('查询失败') }
await state.load()
assert.equal(state.loadError.value, '查询失败')
assert.equal(state.loading.value, false)
})
test('a late previous member request cannot overwrite the newly selected member', async () => {
const state = memberLoader('member-a')
const resolves = []
state.store.loadRoles = () => new Promise(resolve => resolves.push(resolve))
state.store.members = [
{ id: 'member-a', name: '甲', account: 'member.a', phone: '', email: '', roleIds: [] },
{ id: 'member-b', name: '乙', account: 'member.b', phone: '', email: '', roleIds: [] },
]
const previous = state.load()
state.editingId.value = 'member-b'
const current = state.load()
resolves[1]()
await current
assert.equal(state.form.name, '乙')
resolves[0]()
await previous
assert.equal(state.form.name, '乙')
})
function memberSaver(editing = false) {
const requests = [], routes = [], errors = []
const context = {
editingId: { value: editing ? 'member-a' : '' }, saving: { value: false }, loading: { value: false }, loadError: { value: '' }, canSave: { value: true },
form: { name: ' 成员 ', account: 'member.a', password: 'Test1234', phone: '', email: '', roleIds: ['role-a'] },
formRef: { value: { async validate() { return true }, async validateField() { return true } } },
store: { async createMember(payload) { requests.push({ kind: 'create', payload }) }, async updateMember(id, payload) { requests.push({ kind: 'edit', id, payload }) } },
router: { async push(route) { routes.push(route) } }, ElMessage: { success() {}, error(message) { errors.push(message) } },
}
const source = memberSource.slice(memberSource.indexOf('async function saveMember()'), memberSource.indexOf('</script>'))
return { ...context, requests, routes, errors, save: compile(source, context, 'saveMember') }
}
test('member create and edit retain distinct payloads and return to the list only after success', async () => {
const create = memberSaver()
await create.save()
assert.equal(create.requests[0].kind, 'create')
assert.equal(create.requests[0].payload.password, 'Test1234')
assert.equal(create.form.password, '')
assert.deepEqual(create.routes, [{ name: 'members' }])
const edit = memberSaver(true)
await edit.save()
assert.equal(edit.requests[0].kind, 'edit')
assert.equal(edit.requests[0].id, 'member-a')
assert.equal(edit.requests[0].payload.password, undefined)
assert.deepEqual(edit.requests[0].payload.roleIds, ['role-a'])
})
test('member permission, loading and invalid forms block writes; save failure keeps input for retry', async () => {
for (const field of ['saving', 'loading', 'loadError', 'canSave']) {
const state = memberSaver()
state[field].value = field === 'canSave' ? false : true
await state.save()
assert.equal(state.requests.length, 0)
}
const invalid = memberSaver()
invalid.formRef.value.validate = async () => { throw new Error('invalid') }
await invalid.save()
assert.equal(invalid.requests.length, 0)
const failed = memberSaver()
failed.store.createMember = async () => { throw new Error('创建失败') }
await failed.save()
assert.deepEqual(failed.errors, ['创建失败'])
assert.deepEqual(failed.routes, [])
assert.equal(failed.form.password, 'Test1234')
assert.equal(failed.saving.value, false)
})
test('member validation still requires create password, valid login phone, email and at least one role', async () => {
const { default: Schema } = createRequire(import.meta.resolve('element-plus'))('async-validator')
const source = memberSource.slice(memberSource.indexOf('const memberRules:'), memberSource.indexOf('let loadVersion'))
const rules = compile(source, {}, 'memberRules')
const validator = new Schema(rules)
const valid = { name: '成员', account: 'member.a', password: 'Test1234', phone: '', email: '', roleIds: ['role-a'] }
await validator.validate(valid)
for (const patch of [{ password: '' }, { phone: '123' }, { email: 'bad-email' }, { roleIds: [] }, { name: ' ' }]) {
await assert.rejects(validator.validate({ ...valid, ...patch }))
}
})
test('archived company blocks direct editing, and failed company saves preserve contact data', async () => {
const expression = profileSource.match(/^const canEdit = .*$/m)[0]
const store = { company: { auditStatus: 'ARCHIVED' }, async updateCompany() { throw new Error('保存失败') } }
const canEdit = compile(expression, { computed, auth: { hasPermission: () => true }, store }, 'canEdit')
assert.equal(canEdit.value, false)
const errors = [], routes = []
const context = {
profileSubmitting: { value: false }, loading: { value: false }, loadError: { value: '' }, canEdit: { value: false },
profileFormRef: { value: { async validate() { return true } } }, draft: { companyName: '企业', contactPhone: '010-12345678' }, store,
ElMessage: { success() {}, error(message) { errors.push(message) } }, router: { async push(route) { routes.push(route) } },
}
const source = profileSource.slice(profileSource.indexOf('async function saveProfile()'), profileSource.indexOf('</script>'))
const save = compile(source, context, 'saveProfile')
await save()
assert.deepEqual(errors, [])
context.canEdit.value = true
await save()
assert.deepEqual(errors, ['保存失败'])
assert.equal(context.draft.contactPhone, '010-12345678')
assert.deepEqual(routes, [])
store.updateCompany = async payload => { assert.equal(payload.contactPhone, '010-12345678'); assert.equal(payload.phone, undefined) }
await save()
assert.deepEqual(routes, [{ name: 'company-profile' }])
})