Files
jiapu/tests/growth-pages.test.js
T
2026-08-03 16:49:38 +08:00

231 lines
9.4 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const GrowthPages = require('../public/js/growth-pages.js');
test('成长记录页只从 URL 读取安全的真实家谱和记录编号', () => {
assert.equal(
GrowthPages.getCurrentGenealogyId('?genealogyId=2060000000000000001&recordId=2060000000000000002'),
'2060000000000000001'
);
assert.equal(
GrowthPages.getCurrentRecordId('?genealogyId=2060000000000000001&recordId=2060000000000000002'),
'2060000000000000002'
);
assert.equal(GrowthPages.getCurrentGenealogyId(''), '');
assert.equal(GrowthPages.getCurrentRecordId('?recordId=unsafe'), '');
});
test('成长记录写入只构造 YAML GrowthRecordBody 字段并转换后端日期格式', () => {
assert.deepEqual(
GrowthPages.buildGrowthRecordBody({
lineagePersonId: '2060000000000000003',
recordType: ' 入学 ',
recordTitle: ' 入学记录 ',
recordContent: ' 顺利入学 ',
recordDate: '2026-07-24',
remindTime: '2026-07-24T09:30',
mediaOssIds: '2060000000000000004,2060000000000000005',
sortOrder: '3',
status: '0',
recordId: 'should-not-send',
appUserId: 'should-not-send'
}),
{
lineagePersonId: '2060000000000000003',
recordType: '入学',
recordTitle: '入学记录',
recordContent: '顺利入学',
recordDate: '2026-07-24',
remindTime: '2026-07-24 09:30:00',
mediaOssIds: '2060000000000000004,2060000000000000005',
sortOrder: 3,
status: '0'
}
);
});
test('成长记录校验标题、选择器 ID、附件、排序和可重读状态', () => {
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '' }), '请填写记录标题');
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', lineagePersonId: 'A-1' }),
'请选择有效的世系人物'
);
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101, 102' }),
'附件上传结果无效'
);
assert.equal(
GrowthPages.validateGrowthRecordBody(GrowthPages.buildGrowthRecordBody({ recordTitle: '记录', sortOrder: '1.5' })),
'排序值必须是安全整数'
);
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', recordDate: '2026/07/24' }),
'记录日期格式无效'
);
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', remindTime: '2026-07-24T09:30:00+08:00' }),
'提醒时间格式无效'
);
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', status: '1' }),
'当前 PC 无法重新读取停用记录,暂不开放停用'
);
assert.equal(
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101,102', sortOrder: 1, status: '0' }),
''
);
});
test('成长记录写后重读必须返回同一条稳定记录', () => {
const detail = {
recordId: '2060000000000000001',
genealogyId: '2060000000000000002',
recordTitle: '入学记录',
status: '0'
};
assert.equal(GrowthPages.matchesSavedRecord(detail, '2060000000000000001'), true);
assert.equal(GrowthPages.matchesSavedRecord(detail, '2060000000000000009'), false);
assert.equal(GrowthPages.matchesSavedRecord({}, '2060000000000000001'), false);
});
test('成长记录响应使用 PC GrowthRecordVo 并拒绝不安全长 ID', () => {
assert.deepEqual(GrowthPages.normalizeGrowthRecord({
recordId: '2060000000000000001',
genealogyId: '2060000000000000002',
genealogyNo: 'G20260729001',
genealogyName: '叶氏家谱',
surname: '叶',
appUserId: '2060000000000000003',
appUserNickName: '叶子',
appUserPhone: '19100000000',
lineagePersonId: '2060000000000000004',
lineagePersonNo: 'P001',
lineagePersonName: '叶小明',
recordType: '入学',
recordTitle: '小学入学',
recordContent: '<p>第一天上学</p>',
recordDate: '2026-09-01 00:00:00',
remindTime: '2027-09-01 08:30:00',
mediaOssIds: '2060000000000000005,2060000000000000006',
sortOrder: 1,
status: '0',
remark: '成长节点'
}), {
recordId: '2060000000000000001',
genealogyId: '2060000000000000002',
genealogyNo: 'G20260729001',
genealogyName: '叶氏家谱',
surname: '叶',
appUserId: '2060000000000000003',
appUserNickName: '叶子',
appUserPhone: '19100000000',
lineagePersonId: '2060000000000000004',
lineagePersonNo: 'P001',
lineagePersonName: '叶小明',
recordType: '入学',
recordTitle: '小学入学',
recordContent: '<p>第一天上学</p>',
recordDate: '2026-09-01 00:00:00',
remindTime: '2027-09-01 08:30:00',
mediaOssIds: '2060000000000000005,2060000000000000006',
sortOrder: 1,
status: '0',
remark: '成长节点'
});
assert.equal(GrowthPages.normalizeGrowthRecord({
recordId: Number('2060000000000000001'),
genealogyId: '2',
recordTitle: '标题',
status: '0'
}), null);
});
test('成长记录渲染业务字段但不暴露手机号、OSS ID 或未转义正文', () => {
const record = GrowthPages.normalizeGrowthRecord({
recordId: '1',
genealogyId: '2',
appUserId: '3',
appUserNickName: '叶子',
appUserPhone: '19100000000',
lineagePersonId: '4',
lineagePersonName: '叶小明',
recordType: '入学',
recordTitle: '<script>alert(1)</script>',
recordContent: '<img src=x onerror=alert(1)>第一天上学',
recordDate: '2026-09-01 00:00:00',
mediaOssIds: '5,6',
sortOrder: 1,
status: '0'
});
const html = GrowthPages.renderGrowthDetail(record);
assert.doesNotMatch(html, /<script>|<img/);
assert.match(html, /第一天上学/);
assert.match(html, /叶小明/);
assert.match(html, /2 个附件/);
assert.doesNotMatch(html, /19100000000|>5<|>6</);
});
test('世系人物选择器只使用稳定人物编号和名称', () => {
assert.equal(
GrowthPages.renderLineageOptions([
{ personId: '2060000000000000001', name: '叶小明', generationName: '承' },
{ personId: Number('2060000000000000002'), name: '不安全编号' }
], '2060000000000000001'),
'<option value="">不关联世系人物</option><option value="2060000000000000001" selected>叶小明 · 承</option>'
);
});
test('成长记录列表和编辑页按家谱能力开放 PC CRUD,且不允许手填人物或 OSS ID', () => {
const root = path.join(__dirname, '..');
const listPage = fs.readFileSync(path.join(root, 'profile-growth.html'), 'utf8');
const editPage = fs.readFileSync(path.join(root, 'profile-growth-edit.html'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'public', 'css', 'profile-module.css'), 'utf8');
assert.match(listPage, /data-growth-list/);
assert.match(listPage, /data-growth-detail/);
assert.match(listPage, /data-growth-create-link hidden/);
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
assert.match(editPage, /name="lineagePersonId"[^>]*data-growth-lineage-person/);
assert.match(editPage, /data-growth-form[^>]*novalidate/);
assert.match(editPage, /for="growthTitle">标题<span class="field-required"/);
assert.match(editPage, /name="status" type="hidden" value="0"/);
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
assert.match(editPage, /data-growth-form-status role="status" aria-live="polite"/);
assert.match(editPage, /data-growth-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
assert.doesNotMatch(editPage, /name="(?:lineagePersonId|mediaOssIds)"[^>]*type="text"/);
assert.match(editPage, /public\/js\/md5\.js/);
assert.match(editPage, /public\/js\/upload-pages\.js/);
assert.match(editPage, /public\/js\/growth-pages\.js/);
assert.match(styles, /\[data-growth-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
});
test('成长记录只向内容维护者或管理员开放写操作', () => {
const root = path.join(__dirname, '..');
const script = fs.readFileSync(path.join(root, 'public', 'js', 'growth-pages.js'), 'utf8');
assert.equal(GrowthPages.canEditRecords({ canEditContent: true }), true);
assert.equal(GrowthPages.canEditRecords({ canManage: true }), true);
assert.equal(GrowthPages.canEditRecords({ canEditContent: false, canManage: false }), false);
assert.match(script, /await loadCapability\(api, genealogyId\);/);
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-growth-delete-id/);
});
test('成长记录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
const root = path.join(__dirname, '..');
const script = fs.readFileSync(path.join(root, 'public', 'js', 'growth-pages.js'), 'utf8');
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
assert.match(script, /setState\(query\('\[data-growth-list\]'\), 'loading', '正在加载成长记录…'\)/);
assert.match(script, /setState\(query\('\[data-growth-detail\]'\), 'loading', '正在加载成长记录详情…'\)/);
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
assert.match(script, /setFormStatus\(validation, 'error'\)/);
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
assert.match(script, /setFormStatus\('正在保存成长记录…', 'loading'\)/);
});