171 lines
9.2 KiB
JavaScript
171 lines
9.2 KiB
JavaScript
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' }])
|
||
|
|
})
|