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: '今日修谱。', mediaOssIds: '301,302', 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: '今日修谱。', mediaOssIds: '301,302', 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('家族圈页面提供真实详情、分页和上传控件,不暴露 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|data-genealogy-context-link[^>]+data-feed-create-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-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/); });