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, / { 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('')) 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('')) 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' }]) })