355 lines
14 KiB
JavaScript
355 lines
14 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const LineagePages = require('../public/js/lineage-pages.js');
|
|
|
|
test('世系页只从 URL 读取真实家谱编号', () => {
|
|
assert.equal(LineagePages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
|
|
assert.equal(LineagePages.getCurrentGenealogyId(''), '');
|
|
});
|
|
|
|
test('世系管理页和家谱主页复用同一棵安全树的 HTML', () => {
|
|
const html = LineagePages.renderLineageTreeHtml([{
|
|
personId: '2062179707935264770',
|
|
genealogyId: '2062179707935264769',
|
|
name: '<始祖>',
|
|
generationName: '始',
|
|
spouses: [],
|
|
children: []
|
|
}]);
|
|
|
|
assert.match(html, /<始祖>/);
|
|
assert.match(html, /data-lineage-person="2062179707935264770"/);
|
|
assert.doesNotMatch(html, /2062179707935264769/);
|
|
assert.equal(LineagePages.renderLineageTreeHtml([]), '');
|
|
});
|
|
|
|
test('世系人物表单只构造 Apifox LineagePersonBody 字段', () => {
|
|
assert.deepEqual(
|
|
LineagePages.buildLineagePersonBody({
|
|
bindingMode: 'SPECIFIED',
|
|
appUserId: '2061000000000000001',
|
|
avatarOssId: '2061000000000000002',
|
|
name: '李明',
|
|
sex: '0',
|
|
generation: '3',
|
|
generationName: '德',
|
|
fatherId: '2061000000000000003',
|
|
motherId: '2061000000000000004',
|
|
personNo: 'P202607090001',
|
|
aliasName: '明远',
|
|
birthDate: '1990-01-01T08:30',
|
|
birthLunar: '0',
|
|
birthPlace: '成都',
|
|
deathDate: '2050-02-03T10:45',
|
|
deathLunar: '1',
|
|
deathPlace: '重庆',
|
|
burialPlace: '青城山',
|
|
personStatus: '1',
|
|
sortOrder: '1',
|
|
relationName: '妻',
|
|
biography: '人物简介',
|
|
remark: '家族备注',
|
|
personName: '旧字段',
|
|
generationNo: 4,
|
|
introduction: '旧字段'
|
|
}),
|
|
{
|
|
bindingMode: 'SPECIFIED',
|
|
appUserId: '2061000000000000001',
|
|
avatarOssId: '2061000000000000002',
|
|
name: '李明',
|
|
sex: '0',
|
|
generation: 3,
|
|
generationName: '德',
|
|
fatherId: '2061000000000000003',
|
|
motherId: '2061000000000000004',
|
|
personNo: 'P202607090001',
|
|
aliasName: '明远',
|
|
birthDate: '1990-01-01 08:30:00',
|
|
birthLunar: '0',
|
|
birthPlace: '成都',
|
|
deathDate: '2050-02-03 10:45:00',
|
|
deathLunar: '1',
|
|
deathPlace: '重庆',
|
|
burialPlace: '青城山',
|
|
personStatus: '1',
|
|
sortOrder: 1,
|
|
relationName: '妻',
|
|
biography: '人物简介',
|
|
remark: '家族备注'
|
|
}
|
|
);
|
|
assert.deepEqual(LineagePages.buildLineagePersonBody({
|
|
bindingMode: 'SELF',
|
|
appUserId: '2061000000000000001',
|
|
name: '李明'
|
|
}), {
|
|
bindingMode: 'SELF',
|
|
name: '李明'
|
|
});
|
|
});
|
|
|
|
test('世系人物要求安全的 ID、姓名和有效世代值', () => {
|
|
assert.deepEqual(
|
|
LineagePages.normalizeLineagePerson({
|
|
personId: '2060000000000000000',
|
|
name: '李明',
|
|
generation: 3,
|
|
generationName: '德',
|
|
sex: '0'
|
|
}),
|
|
{
|
|
personId: '2060000000000000000',
|
|
name: '李明',
|
|
sex: '0',
|
|
generationName: '德',
|
|
generation: 3
|
|
}
|
|
);
|
|
assert.equal(LineagePages.normalizeLineagePerson({ personId: Number('2060000000000000000'), name: '李明' }), null);
|
|
assert.equal(LineagePages.normalizeLineagePerson({
|
|
personId: '2060000000000000000',
|
|
fatherId: Number('2061000000000000000'),
|
|
name: '李明'
|
|
}), null);
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '' }), '请填写成员姓名');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ name: '李明' }), '请选择账号绑定方式');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'OTHER', name: '李明' }), '账号绑定方式无效');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'SPECIFIED', name: '李明' }), '指定账号绑定缺少用户选项');
|
|
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', generation: '1.5' })), '世代序号必须是整数');
|
|
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', sortOrder: '1.5' })), '排序值必须是整数');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', sex: '3' }), '性别选项无效');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', personStatus: '3' }), '人物状态选项无效');
|
|
assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', birthLunar: '2' }), '出生历法选项无效');
|
|
});
|
|
|
|
test('世系人物响应保留完整详情字段并拒绝不安全的嵌套 ID', () => {
|
|
assert.deepEqual(LineagePages.normalizeLineagePerson({
|
|
personId: '2060000000000000000',
|
|
genealogyId: '2060000000000000001',
|
|
appUserId: '2060000000000000002',
|
|
fatherId: '2060000000000000004',
|
|
motherId: '2060000000000000005',
|
|
genealogyName: '李氏家谱',
|
|
genealogyNo: 'G20260728001',
|
|
appUserNickName: '明远',
|
|
personNo: 'P202607090001',
|
|
name: '李明',
|
|
aliasName: '明远',
|
|
sex: '0',
|
|
generation: 3,
|
|
generationName: '德',
|
|
fatherName: '李父',
|
|
motherName: '王母',
|
|
spouseNames: '张氏',
|
|
birthDate: '1990-01-01 08:30:00',
|
|
birthLunar: '0',
|
|
birthPlace: '成都',
|
|
deathDate: '2050-02-03 10:45:00',
|
|
deathLunar: '1',
|
|
deathPlace: '重庆',
|
|
burialPlace: '青城山',
|
|
personStatus: '1',
|
|
biography: '人物简介',
|
|
sortOrder: 1,
|
|
status: '0',
|
|
remark: '家族备注'
|
|
}), {
|
|
personId: '2060000000000000000',
|
|
name: '李明',
|
|
genealogyName: '李氏家谱',
|
|
genealogyNo: 'G20260728001',
|
|
appUserNickName: '明远',
|
|
personNo: 'P202607090001',
|
|
aliasName: '明远',
|
|
sex: '0',
|
|
generationName: '德',
|
|
fatherName: '李父',
|
|
motherName: '王母',
|
|
spouseNames: '张氏',
|
|
birthDate: '1990-01-01 08:30:00',
|
|
birthLunar: '0',
|
|
birthPlace: '成都',
|
|
deathDate: '2050-02-03 10:45:00',
|
|
deathLunar: '1',
|
|
deathPlace: '重庆',
|
|
burialPlace: '青城山',
|
|
personStatus: '1',
|
|
biography: '人物简介',
|
|
status: '0',
|
|
remark: '家族备注',
|
|
appUserId: '2060000000000000002',
|
|
genealogyId: '2060000000000000001',
|
|
fatherId: '2060000000000000004',
|
|
motherId: '2060000000000000005',
|
|
generation: 3,
|
|
sortOrder: 1
|
|
});
|
|
});
|
|
|
|
test('家谱内容权限只控制世系写操作,读取和分页始终可用', () => {
|
|
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: false }), {
|
|
canEditContent: false
|
|
});
|
|
assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: true }), {
|
|
canEditContent: true
|
|
});
|
|
assert.equal(LineagePages.canUseLineagePagination(false), true);
|
|
assert.equal(LineagePages.canUseLineagePagination(true), false);
|
|
});
|
|
|
|
test('世代选项来自正常字辈列表并自动映射序号和字辈', () => {
|
|
assert.deepEqual(LineagePages.normalizeGenerationOption({
|
|
poemId: '2060000000000000000',
|
|
generationNo: 3,
|
|
generationText: '德',
|
|
status: '0'
|
|
}), {
|
|
generation: 3,
|
|
generationName: '德'
|
|
});
|
|
assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 0, generationText: '德' }), null);
|
|
assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 3, generationText: '' }), null);
|
|
});
|
|
|
|
test('已有账号绑定按当前登录用户区分 SELF 与只读保留的 SPECIFIED', () => {
|
|
assert.deepEqual(LineagePages.normalizeLineageBinding({}, '2060000000000000001'), {
|
|
bindingMode: 'NONE',
|
|
appUserId: ''
|
|
});
|
|
assert.deepEqual(LineagePages.normalizeLineageBinding({
|
|
appUserId: '2060000000000000001'
|
|
}, '2060000000000000001'), {
|
|
bindingMode: 'SELF',
|
|
appUserId: ''
|
|
});
|
|
assert.deepEqual(LineagePages.normalizeLineageBinding({
|
|
appUserId: '2060000000000000002'
|
|
}, '2060000000000000001'), {
|
|
bindingMode: 'SPECIFIED',
|
|
appUserId: '2060000000000000002'
|
|
});
|
|
});
|
|
|
|
test('指定账号绑定只使用当前家谱正常且已绑定账号的成员选项', () => {
|
|
assert.deepEqual(LineagePages.normalizeGenealogyMemberOption({
|
|
memberId: '2060000000000000001',
|
|
genealogyId: '2060000000000000002',
|
|
appUserId: '2060000000000000003',
|
|
appUserNickName: '明远',
|
|
memberName: '李明',
|
|
roleType: 'member',
|
|
status: '0'
|
|
}), {
|
|
memberId: '2060000000000000001',
|
|
genealogyId: '2060000000000000002',
|
|
appUserId: '2060000000000000003',
|
|
appUserNickName: '明远',
|
|
memberName: '李明',
|
|
roleType: 'member',
|
|
status: '0'
|
|
});
|
|
assert.equal(LineagePages.normalizeGenealogyMemberOption({
|
|
memberId: '1',
|
|
genealogyId: '2',
|
|
appUserId: null,
|
|
memberName: '未绑定成员',
|
|
status: '0'
|
|
}), null);
|
|
assert.equal(LineagePages.normalizeGenealogyMemberOption({
|
|
memberId: '1',
|
|
genealogyId: '2',
|
|
appUserId: '3',
|
|
memberName: '停用成员',
|
|
status: '1'
|
|
}), null);
|
|
assert.deepEqual(LineagePages.normalizeGenealogyMemberOptions([
|
|
{ memberId: '1', genealogyId: '2', appUserId: '3', memberName: '正常成员', status: '0' },
|
|
{ memberId: '4', genealogyId: '2', appUserId: null, memberName: '未绑定成员', status: '0' }
|
|
]).map((option) => option.appUserId), ['3']);
|
|
});
|
|
|
|
test('世系关系模式只使用 PC 已定义的四种新增关系', () => {
|
|
assert.equal(LineagePages.relationLabel('parents'), '父母');
|
|
assert.equal(LineagePages.relationLabel('spouses'), '配偶');
|
|
assert.equal(LineagePages.relationLabel('children'), '子女');
|
|
assert.equal(LineagePages.relationLabel('siblings'), '兄弟姐妹');
|
|
assert.equal(LineagePages.relationLabel('remove'), '');
|
|
});
|
|
|
|
test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => {
|
|
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
|
|
|
|
assert.deepEqual(LineagePages.buildLineagePageQuery({
|
|
keyword: ' 李明 ',
|
|
generation: '3',
|
|
personStatus: '1'
|
|
}, 2, 20), {
|
|
pageNum: 2,
|
|
pageSize: 20,
|
|
keyword: '李明',
|
|
generation: 3,
|
|
personStatus: '1'
|
|
});
|
|
assert.deepEqual(LineagePages.buildLineagePageQuery({}, 1, 20), {
|
|
pageNum: 1,
|
|
pageSize: 20
|
|
});
|
|
assert.match(page, /placeholder="输入姓名、别名或人物编号搜索"/);
|
|
assert.match(page, /data-lineage-keyword[^>]+aria-label="按姓名、别名或人物编号搜索成员"/);
|
|
assert.match(page, /data-lineage-generation-filter/);
|
|
assert.match(page, /data-lineage-status-filter/);
|
|
assert.match(page, /data-lineage-pagination/);
|
|
assert.match(page, /data-lineage-page-action="previous"/);
|
|
assert.match(page, /data-lineage-page-action="next"/);
|
|
});
|
|
|
|
test('世系表单为全部 LineagePersonBody 字段提供真实来源且不允许手填业务 ID', () => {
|
|
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
|
|
const styles = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'profile-module.css'), 'utf8');
|
|
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'lineage-pages.js'), 'utf8');
|
|
|
|
[
|
|
'bindingMode', 'appUserId', 'avatarOssId', 'personNo', 'name', 'aliasName', 'sex',
|
|
'generation', 'generationName', 'fatherId', 'motherId', 'birthDate', 'birthLunar',
|
|
'birthPlace', 'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus',
|
|
'biography', 'sortOrder', 'remark', 'relationName'
|
|
].forEach((name) => assert.match(page, new RegExp('name="' + name + '"')));
|
|
assert.match(page, /value="SPECIFIED"(?![^>]+disabled)/);
|
|
assert.match(page, /data-lineage-specified-member hidden/);
|
|
assert.match(page, /<label for="lineage-specified-member">指定账号<\/label>/);
|
|
assert.match(page, /select[^>]+name="appUserId"[^>]+data-lineage-member-options|select[^>]+data-lineage-member-options[^>]+name="appUserId"/);
|
|
assert.doesNotMatch(page, /name="appUserId"[^>]+type="(?:text|number)"|type="(?:text|number)"[^>]+name="appUserId"/);
|
|
assert.doesNotMatch(page, /name="appUserId"[^>]+type="hidden"|type="hidden"[^>]+name="appUserId"/);
|
|
assert.match(page, /name="avatarOssId"[^>]+type="hidden"|type="hidden"[^>]+name="avatarOssId"/);
|
|
assert.match(page, /data-upload-target="#lineage-avatar-oss-id"/);
|
|
assert.match(script, /specifiedMember\.hidden = !specified/);
|
|
assert.match(page, /name="generation"[^>]*data-lineage-generation/);
|
|
assert.match(page, /name="generationName"[^>]+readonly|readonly[^>]+name="generationName"/);
|
|
assert.match(page, /name="fatherId"[^>]*data-lineage-parent-option/);
|
|
assert.match(page, /name="motherId"[^>]*data-lineage-parent-option/);
|
|
assert.match(page, /data-lineage-management[^>]+hidden/);
|
|
assert.match(page, /data-lineage-form novalidate/);
|
|
assert.match(page, /data-lineage-submit/);
|
|
assert.match(page, /data-lineage-form-status/);
|
|
assert.match(page, /data-lineage-action-status/);
|
|
assert.match(page, /public\/js\/upload-pages\.js/);
|
|
assert.match(styles, /\[data-lineage-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s);
|
|
assert.match(styles, /\[data-lineage-spouse-field\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
|
assert.match(styles, /\[data-lineage-pagination\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
|
assert.match(script, /function setLineageState\(selector, type, message\)/);
|
|
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
|
assert.match(script, /setLineageState\('\[data-lineage-list\]', 'loading', '正在加载世系人物…'\)/);
|
|
assert.match(script, /setFormStatus\('正在保存成员…', 'loading'\)/);
|
|
assert.match(script, /setActionStatus\('正在停用成员…', 'loading'\)/);
|
|
assert.match(script, /function setLineageSubmitLabel\(label\)/);
|
|
});
|
|
|
|
test('世系管理的 403 不会被当作登录失效', () => {
|
|
assert.equal(LineagePages.isForbidden({ status: 403 }), true);
|
|
assert.equal(LineagePages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false);
|
|
});
|