const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const FeedPages = require('../public/js/feed-pages.js'); const rootDir = path.resolve(__dirname, '..'); function read(file) { return fs.readFileSync(path.join(rootDir, file), 'utf8'); } test('动态表单只构造最新文档定义的 FamilyFeedBody 字段', () => { assert.deepEqual( FeedPages.buildFeedBody({ feedContent: '今天上传一张老照片。', mediaOssIds: '101,102', status: '0', visibility: '2' }), { feedType: 'text', feedContent: '今天上传一张老照片。', mediaOssIds: '101,102', status: '0' } ); }); test('动态页只从 URL 读取真实家谱和动态编号', () => { assert.equal(FeedPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001'); assert.equal(FeedPages.getCurrentGenealogyId(''), ''); assert.equal(FeedPages.getCurrentFeedId('?feedId=7001'), '7001'); }); test('动态列表要求服务端返回 feedId 和 feedContent', () => { assert.deepEqual(FeedPages.normalizeFeed({ feedId: 7001, feedContent: '家谱动态' }), { feedId: '7001', feedContent: '家谱动态' }); assert.equal(FeedPages.normalizeFeed({ feedId: 7001, content: '旧字段' }), null); }); test('评论请求只提交 Apifox 定义的 commentContent 和父评论 ID', () => { assert.deepEqual(FeedPages.buildCommentBody({ commentContent: '收到', parentCommentId: '2060000000000000000', replyUserId: '2060000000000000001' }), { commentContent: '收到', parentCommentId: '2060000000000000000' }); }); test('评论响应使用 commentContent,并保留已删除评论的占位状态', () => { assert.deepEqual(FeedPages.normalizeComment({ commentId: '2060000000000000000', commentContent: '收到', appUserNickName: '小李', replyCount: 2, userDeleted: '0' }), { commentId: '2060000000000000000', commentContent: '收到', appUserNickName: '小李', replyCount: 2, userDeleted: false }); assert.deepEqual(FeedPages.normalizeComment({ commentId: '2060000000000000001', commentContent: null, userDeleted: '1' }), { commentId: '2060000000000000001', commentContent: '该评论已删除', appUserNickName: undefined, replyCount: undefined, userDeleted: true }); }); test('动态响应保留页面闭环所需的完整 FamilyFeedView 字段和字符串 ID', () => { assert.deepEqual(FeedPages.normalizeFeed({ feedId: 2060000000000000000n, genealogyId: '2061000000000000000', genealogyNo: 'G20260728001', genealogyName: '叶氏家谱', publisherUserId: '2062000000000000000', publisherNickName: '叶子', publisherStatus: '0', feedType: 'text', feedContent: '今日修谱。', likedByMe: true, likeCount: 8, commentCount: 3, pinned: '1', pinnedTime: '2026-07-28 10:00:00', sortOrder: 2, status: '0', remark: '族长发布', createTime: '2026-07-28 09:00:00', updateTime: '2026-07-28 09:30:00' }), { feedId: '2060000000000000000', genealogyId: '2061000000000000000', genealogyNo: 'G20260728001', genealogyName: '叶氏家谱', publisherUserId: '2062000000000000000', publisherNickName: '叶子', publisherStatus: '0', feedType: 'text', feedContent: '今日修谱。', likedByMe: true, likeCount: 8, commentCount: 3, pinned: '1', pinnedTime: '2026-07-28 10:00:00', sortOrder: 2, status: '0', remark: '族长发布', createTime: '2026-07-28 09:00:00', updateTime: '2026-07-28 09:30:00' }); }); test('评论响应保留回复、归属、层级和时间字段', () => { assert.deepEqual(FeedPages.normalizeComment({ commentId: '2060000000000000001', genealogyId: '2061000000000000000', feedId: '2060000000000000000', parentCommentId: '2060000000000000002', appUserId: '2062000000000000000', parentAppUserId: '2062000000000000003', appUserNickName: '叶子', appUserAvatar: '401', parentAppUserNickName: '小叶', commentContent: '收到', replyCount: 2, commentLevel: 'reply', userDeleted: '0', status: '0', createTime: '2026-07-28 10:30:00' }), { commentId: '2060000000000000001', genealogyId: '2061000000000000000', feedId: '2060000000000000000', parentCommentId: '2060000000000000002', appUserId: '2062000000000000000', parentAppUserId: '2062000000000000003', appUserNickName: '叶子', appUserAvatar: '401', parentAppUserNickName: '小叶', commentContent: '收到', replyCount: 2, commentLevel: 'reply', userDeleted: false, status: '0', createTime: '2026-07-28 10:30:00' }); }); test('动态、评论和回复分页只接受正整数页码并使用固定页面大小', () => { assert.deepEqual(FeedPages.buildPageQuery('3'), { pageNum: 3, pageSize: 20 }); assert.deepEqual(FeedPages.buildPageQuery('-1'), { pageNum: 1, pageSize: 20 }); assert.deepEqual(FeedPages.buildPageQuery('1.5'), { pageNum: 1, pageSize: 20 }); }); test('动态列表、详情和编辑深链始终同时携带真实家谱和动态 ID', () => { assert.equal( FeedPages.buildFeedUrl('detail', '2061000000000000000', '2060000000000000000'), 'profile-feed-detail.html?genealogyId=2061000000000000000&feedId=2060000000000000000' ); assert.equal( FeedPages.buildFeedUrl('edit', '2061000000000000000', '2060000000000000000'), 'profile-feed-edit.html?genealogyId=2061000000000000000&feedId=2060000000000000000' ); assert.equal( FeedPages.buildFeedUrl('list', '2061000000000000000'), 'profile-feed.html?genealogyId=2061000000000000000' ); }); test('正常动态保存后进入详情,停用动态保存后返回可读取的列表', () => { assert.equal( FeedPages.buildPostSaveUrl('2061000000000000000', '2060000000000000000', '0'), 'profile-feed-detail.html?genealogyId=2061000000000000000&feedId=2060000000000000000' ); assert.equal( FeedPages.buildPostSaveUrl('2061000000000000000', '2060000000000000000', '1'), 'profile-feed.html?genealogyId=2061000000000000000' ); }); test('动态表单省略未填写的可选字段并拒绝契约外状态', () => { assert.deepEqual(FeedPages.buildFeedBody({ feedContent: '仅发布文字' }), { feedType: 'text', feedContent: '仅发布文字' }); assert.throws( () => FeedPages.buildFeedBody({ feedContent: '错误状态', status: 'draft' }), /状态只能是 0 或 1/ ); }); test('同一动态动作在请求完成前只执行一次', async () => { let resolveAction; let calls = 0; const action = new Promise((resolve) => { resolveAction = resolve; }); const first = FeedPages.withActionLock('like:2060', async () => { calls += 1; await action; return 'ok'; }); const second = FeedPages.withActionLock('like:2060', async () => { calls += 1; }); assert.equal(first, second); assert.equal(calls, 1); resolveAction(); assert.equal(await first, 'ok'); }); test('家谱能力和当前用户决定编辑、管理与本人删除上下文', () => { assert.deepEqual( FeedPages.normalizeViewerContext( { canEditContent: true, canManage: false }, { userId: 2062000000000000000n } ), { userId: '2062000000000000000', canEditContent: true, canManage: false } ); }); test('家族圈编辑能力只接受真实布尔权限,并允许管理者维护内容', () => { assert.equal(FeedPages.canEditFeeds({ canEditContent: true }), true); assert.equal(FeedPages.canEditFeeds({ canManage: true }), true); assert.equal(FeedPages.canEditFeeds({ canEditContent: 'true', canManage: false }), false); assert.equal(FeedPages.canEditFeeds({ canEditContent: false, canManage: 'true' }), false); }); test('家族圈页面提供真实详情、分页和上传控件,不暴露 OSS ID 或草稿伪语义', () => { const listPage = read('profile-feed.html'); const editPage = read('profile-feed-edit.html'); const detailPage = read('profile-feed-detail.html'); const script = read('public/js/feed-pages.js'); assert.match(listPage, /data-feed-page-prev/); assert.match(listPage, /data-feed-page-next/); assert.match(listPage, /data-feed-create-link[^>]+data-genealogy-context-link[^>]+hidden|data-feed-create-link[^>]+hidden[^>]+data-genealogy-context-link/); assert.match(detailPage, /data-feed-detail-page/); assert.match(detailPage, /data-feed-detail/); assert.match(detailPage, /data-feed-detail-comments/); assert.match(detailPage, /data-feed-comment-form novalidate/); assert.match(detailPage, /name="commentContent"[^>]+required/); assert.match(detailPage, /data-feed-comment-form-status/); assert.match(detailPage, /data-feed-list-link[^>]+data-genealogy-context-link|data-genealogy-context-link[^>]+data-feed-list-link/); assert.doesNotMatch(editPage, /type="text"[^>]+name="mediaOssIds"|name="mediaOssIds"[^>]+type="text"/); assert.match(editPage, /name="mediaOssIds"[^>]+type="hidden"|type="hidden"[^>]+name="mediaOssIds"/); assert.match(editPage, /multiple/); assert.match(editPage, /data-feed-advanced[^>]+hidden/); assert.match(editPage, /name="status"[^>]+disabled|disabled[^>]+name="status"/); assert.doesNotMatch(editPage, /草稿|图片 OSS ID/); assert.match(script, /feedCommentsPage/); assert.match(script, /feedCommentRepliesPage/); assert.match(script, /currentProfile/); assert.match(script, /genealogyDetail/); assert.match(script, /field\.disabled/); }); test('评论和回复使用详情页表单与统一状态,不再使用浏览器提示框', () => { const detailPage = read('profile-feed-detail.html'); const script = read('public/js/feed-pages.js'); assert.match(detailPage, /data-feed-comment-form-target/); assert.match(detailPage, /data-feed-comment-cancel/); assert.match(detailPage, /data-feed-detail[^>]*>[\s\S]*data-api-state="loading"/); assert.match(detailPage, /data-feed-detail-comments[^>]*>[\s\S]*data-api-state="loading"/); assert.match(script, /function openFeedCommentForm\(feedId, parentCommentId\)/); assert.match(script, /function submitComment\(form\)/); assert.match(script, /function renderFeedState\(type, message\)/); assert.match(script, /function setReplyState\(parentCommentId, type, message\)/); assert.match(script, /setFeedCommentFormStatus\('正在发布评论…', 'loading'\)/); assert.match(script, /setFeedCommentFormEnabled\(false\)/); assert.match(script, /setFeedContainerState\('\[data-feed-detail-comments\]', 'loading', '正在加载评论…'\)/); assert.match(script, /setReplyState\(commentId, 'loading', '正在加载回复…'\)/); assert.match(script, /当前账号无权查看回复。/); assert.doesNotMatch(script, /api-empty/); assert.doesNotMatch(script, /root\.prompt/); }); test('动态发布表单明确必填项,并以真实家谱能力控制加载、无权限和保存失败反馈', () => { const editPage = read('profile-feed-edit.html'); const script = read('public/js/feed-pages.js'); assert.match(editPage, /data-feed-form novalidate/); assert.match(editPage, /class="field-required"/); assert.match(editPage, /data-feed-form-status/); assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/); assert.match(script, /setFeedContainerState\('\[data-feed-list\]', 'loading'/); assert.match(script, /setFeedContainerState\('\[data-feed-detail\]', 'loading'/); assert.match(script, /'暂无家族动态'/); assert.match(script, /!viewerContext\.canEditContent/); assert.match(script, /setFeedFormEnabled\(false\)/); assert.match(script, /setFeedSubmitPending\(true\)/); assert.match(script, /正在保存…/); assert.match(script, /function canEditFeeds\(genealogy\)/); assert.match(script, /setFeedContentEditingEnabled\(viewerContext\.canEditContent\)/); assert.match(script, /if \(!viewerContext\.canEditContent\) \{\s*setFeedFormStatus\('当前账号无权发布或编辑该家谱的动态。', 'forbidden'\)/); assert.match(editPage, /data-feed-editor-action hidden disabled/); });