const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const JoinPages = require('../public/js/join-pages.js'); const genealogyId = '2062179707935264769'; const applyId = '2062179707935264770'; function read(relativePath) { return fs.readFileSync(path.resolve(__dirname, '..', relativePath), 'utf8'); } test('加入申请只构造 AppGenealogyJoinApplyBody 并裁剪可选文本', () => { assert.deepEqual(JoinPages.buildJoinApplyBody({ applicantName: ' 叶子 ', phone: ' 测试联系方式 ', relationDesc: ' 族亲 ', applyReason: ' 申请加入 ', inviterUserId: 'must-drop', genealogyId: 'must-drop' }), { applicantName: '叶子', phone: '测试联系方式', relationDesc: '族亲', applyReason: '申请加入' }); assert.deepEqual(JoinPages.buildJoinApplyBody({ applicantName: ' ', phone: '', relationDesc: null }), {}); }); test('加入申请按后端长度约束校验四个可选字段', () => { assert.equal(JoinPages.validateJoinApplyBody({ applicantName: '叶'.repeat(50) }), ''); assert.match(JoinPages.validateJoinApplyBody({ applicantName: '叶'.repeat(51) }), /50/); assert.equal(JoinPages.validateJoinApplyBody({ phone: '1'.repeat(30) }), ''); assert.match(JoinPages.validateJoinApplyBody({ phone: '1'.repeat(31) }), /30/); assert.equal(JoinPages.validateJoinApplyBody({ relationDesc: '亲'.repeat(100) }), ''); assert.match(JoinPages.validateJoinApplyBody({ relationDesc: '亲'.repeat(101) }), /100/); assert.equal(JoinPages.validateJoinApplyBody({ applyReason: '因'.repeat(500) }), ''); assert.match(JoinPages.validateJoinApplyBody({ applyReason: '因'.repeat(501) }), /500/); }); test('可申请家谱必须提供稳定字符串 ID、谱名和姓氏,并拒绝停用家谱', () => { assert.deepEqual(JoinPages.normalizeJoinGenealogy({ genealogyId, genealogyName: '叶氏家谱', surname: '叶', regionFullName: '四川省', memberCount: 12, joinMode: '0', status: '0', ownerUserId: 'must-drop' }), { genealogyId, genealogyName: '叶氏家谱', surname: '叶', regionFullName: '四川省', memberCount: 12, joinMode: '0' }); assert.equal(JoinPages.normalizeJoinGenealogy({ genealogyId: Number.MAX_SAFE_INTEGER + 1, genealogyName: '叶氏家谱', surname: '叶', status: '0' }), null); assert.equal(JoinPages.normalizeJoinGenealogy({ genealogyId, genealogyName: '叶氏家谱', surname: '叶', status: '1' }), null); }); test('我的申请只接收稳定 ID 和 0/1/2/3 状态', () => { assert.deepEqual(JoinPages.normalizeJoinApply({ applyId, genealogyId, genealogyName: '叶氏家谱', surname: '叶', relationDesc: '族亲', applyReason: '申请加入', auditRemark: '已核实', auditTime: '2026-07-29 12:00:00', status: '1', appUserId: 'must-drop', inviterUserId: 'must-drop', auditUserId: 'must-drop', appUserPhone: 'must-drop' }), { applyId, genealogyId, genealogyName: '叶氏家谱', surname: '叶', relationDesc: '族亲', applyReason: '申请加入', auditRemark: '已核实', auditTime: '2026-07-29 12:00:00', status: '1' }); assert.equal(JoinPages.normalizeJoinApply({ applyId: Number.MAX_SAFE_INTEGER + 1, genealogyId, genealogyName: '叶氏家谱', status: '0' }), null); assert.equal(JoinPages.normalizeJoinApply({ applyId, genealogyId, genealogyName: '叶氏家谱', status: '9' }), null); }); test('申请数组只要包含非法元素就整批拒绝', () => { const valid = { applyId, genealogyId, genealogyName: '叶氏家谱', status: '0' }; assert.equal(JoinPages.normalizeJoinApplies([valid]).length, 1); assert.deepEqual(JoinPages.normalizeJoinApplies([valid, { status: '0' }]), []); assert.deepEqual(JoinPages.normalizeJoinApplies({ rows: [valid] }), []); }); test('普通用户列表隐藏内部字段且只有待审核申请可以撤销', () => { const html = JoinPages.renderMyJoinApplies([{ applyId, genealogyId, genealogyName: '<叶氏家谱>', surname: '叶', relationDesc: '族亲', applyReason: '申请加入', status: '0', appUserId: 'secret-user', inviterUserId: 'secret-inviter', auditUserId: 'secret-auditor', appUserPhone: 'secret-phone' }]); const approvedHtml = JoinPages.renderMyJoinApplies([{ applyId, genealogyId, genealogyName: '叶氏家谱', status: '1' }]); assert.match(html, /<叶氏家谱>/); assert.match(html, new RegExp('data-join-cancel-id="' + applyId + '"')); assert.doesNotMatch(approvedHtml, /data-join-cancel-id/); assert.doesNotMatch(html, /secret-user|secret-inviter|secret-auditor|secret-phone|must-drop/); }); test('申请加入页由后端家谱选项驱动且不允许手填业务 ID 或邀请码', () => { const source = read('join-genealogy.html'); assert.match(source, /data-join-apply-page/); assert.match(source, /data-join-genealogy-search/); assert.match(source, /data-join-genealogy-search novalidate aria-describedby="join-options-status"/); assert.match(source, /data-join-genealogy-options/); assert.match(source, /data-join-options-status role="status" aria-live="polite"/); assert.match(source, /data-join-selected-genealogy role="status" aria-live="polite"/); assert.match(source, /data-join-apply-form/); assert.match(source, /data-join-apply-form novalidate aria-describedby="join-apply-status"/); assert.match(source, /name="applicantName"/); assert.match(source, /name="phone"/); assert.match(source, /name="relationDesc"/); assert.match(source, /name="applyReason"/); assert.match(source, /class="join-page-grid"/); assert.match(source, /class="join-search-panel"/); assert.match(source, /class="[^"]*join-apply-form/); assert.match(source, /
{ const source = read('public/js/join-pages.js'); const profilePage = read('profile-join-family.html'); assert.match(source, /root\.ProfileUI && root\.ProfileUI\.renderApiState/); assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/); assert.match(source, /function setOptionsState\(options, type, message\)/); assert.match(source, /function clearSelectedGenealogy\(\)/); assert.match(source, /clearSelectedGenealogy\(\);\s+setText\('\[data-join-options-status\]'/); assert.match(source, /setOptionsState\(options, 'loading', '正在读取可申请家谱…'\)/); assert.match(source, /\[data-join-options-status\]/); assert.match(source, /getApiStateType\(error\)/); assert.match(source, /setSubmitPending\(/); assert.match(source, /正在提交…/); assert.match(profilePage, /
{ const source = read('profile-join-family.html'); assert.doesNotMatch(source, /data-feature-status="pending"|pending-pages\.js/); assert.match(source, /data-my-join-applies-page/); assert.match(source, /data-join-apply-list="mine"/); assert.match(source, /data-join-apply-refresh/); assert.match(source, /public\/js\/join-pages\.js/); assert.doesNotMatch(source, /邀请码|分享码|输入.*ID/); }); test('加入申请运行时提交与撤销后都重新读取我的申请', () => { const source = read('public/js/join-pages.js'); assert.match(source, /await api\.applyToGenealogy\(/); assert.match(source, /await api\.myGenealogyJoinApplies\(\)/); assert.match(source, /await api\.cancelGenealogyJoinApply\(/); assert.match(source, /提交后无法读取同一申请/); assert.match(source, /撤销后申请仍处于待审核状态/); });