utils.test.js
1 import { describe, expect, it } from 'vitest'; 2 import { MAX_TIEBA_LIMIT, buildTiebaPostCardsFromPagePc, buildTiebaPostItems, buildTiebaSearchItems, buildTiebaReadItems, normalizeTiebaLimit, signTiebaPcParams, } from './utils.js'; 3 describe('normalizeTiebaLimit', () => { 4 it('caps list commands at the declared tieba maximum', () => { 5 expect(MAX_TIEBA_LIMIT).toBe(20); 6 expect(normalizeTiebaLimit(undefined)).toBe(20); 7 expect(normalizeTiebaLimit(25)).toBe(20); 8 expect(normalizeTiebaLimit(7)).toBe(7); 9 }); 10 }); 11 describe('signTiebaPcParams', () => { 12 it('matches Tieba PC forum-list signing for stable page_pc requests', () => { 13 expect(signTiebaPcParams({ 14 kw: encodeURIComponent('李毅'), 15 pn: '1', 16 sort_type: '-1', 17 is_newfrs: '1', 18 is_newfeed: '1', 19 rn: '30', 20 rn_need: '20', 21 tbs: '', 22 subapp_type: 'pc', 23 _client_type: '20', 24 })).toBe('466f2e091dd4ed17c6661a842b5ec342'); 25 }); 26 }); 27 describe('buildTiebaPostCardsFromPagePc', () => { 28 it('extracts thread cards from signed page_pc feed payloads', () => { 29 const cards = buildTiebaPostCardsFromPagePc([ 30 { 31 layout: 'feed', 32 feed: { 33 schema: 'tiebaapp://router/portal?params=%7B%22pageParams%22%3A%7B%22tid%22%3A10596901456%7D%7D', 34 log_param: [ 35 { key: 'tid', value: '10596901456' }, 36 ], 37 business_info_map: { 38 thread_id: '10596901456', 39 title: '崇拜希特勒的人都是日本的汉奸走狗', 40 }, 41 components: [ 42 { 43 component: 'feed_head', 44 feed_head: { 45 extra_data: [ 46 { 47 business_info_map: { time_prefix: '回复于' }, 48 text: { text: '1774343231' }, 49 }, 50 ], 51 main_data: [ 52 { 53 text: { text: '上帝的子民º♬' }, 54 }, 55 ], 56 }, 57 }, 58 { 59 component: 'feed_title', 60 feed_title: { 61 data: [{ text_info: { text: '崇拜希特勒的人都是日本的汉奸走狗' } }], 62 }, 63 }, 64 { 65 component: 'feed_social', 66 feed_social: { 67 comment_num: 12, 68 }, 69 }, 70 ], 71 }, 72 }, 73 ]); 74 expect(cards).toEqual([ 75 { 76 title: '崇拜希特勒的人都是日本的汉奸走狗', 77 author: '上帝的子民º♬', 78 descInfo: '回复于2026-03-24 17:07', 79 commentCount: 12, 80 actionTexts: [], 81 threadId: '10596901456', 82 url: 'https://tieba.baidu.com/p/10596901456', 83 }, 84 ]); 85 }); 86 }); 87 describe('buildTiebaPostItems', () => { 88 it('builds stable thread ids and urls from card props without page hops', () => { 89 const items = buildTiebaPostItems([ 90 { 91 title: '我来说个事', 92 author: '暴躁的小伙子', 93 descInfo: '回复于2分钟前', 94 actionTexts: ['分享', '评论 5', '点赞 2'], 95 threadId: '10590564788', 96 }, 97 ], 5); 98 expect(items).toEqual([ 99 { 100 rank: 1, 101 title: '我来说个事', 102 author: '暴躁的小伙子', 103 replies: 5, 104 last_reply: '2分钟前', 105 id: '10590564788', 106 url: 'https://tieba.baidu.com/p/10590564788', 107 }, 108 ]); 109 }); 110 it('honors the public 20-item limit contract', () => { 111 const raw = Array.from({ length: 25 }, (_, index) => ({ 112 title: `帖子 ${index + 1}`, 113 author: `作者 ${index + 1}`, 114 descInfo: '回复于刚刚', 115 actionTexts: ['分享', `评论 ${index + 1}`], 116 threadId: String(1000 + index), 117 })); 118 const items = buildTiebaPostItems(raw, 25); 119 expect(items).toHaveLength(20); 120 expect(items[19]).toMatchObject({ 121 rank: 20, 122 id: '1019', 123 url: 'https://tieba.baidu.com/p/1019', 124 }); 125 }); 126 it('parses Chinese count units and keeps date-time last-reply text intact', () => { 127 const items = buildTiebaPostItems([ 128 { 129 title: '复杂格式帖子', 130 author: '作者', 131 descInfo: '回复于03-29 11:35', 132 actionTexts: ['分享', '评论 1.2万'], 133 url: 'https://tieba.baidu.com/p/123456', 134 }, 135 ], 5); 136 expect(items[0]).toMatchObject({ 137 replies: 12000, 138 last_reply: '03-29 11:35', 139 id: '123456', 140 url: 'https://tieba.baidu.com/p/123456', 141 }); 142 }); 143 }); 144 describe('buildTiebaSearchItems', () => { 145 it('keeps up to 20 search results when the page provides more than 10 cards', () => { 146 const raw = Array.from({ length: 25 }, (_, index) => ({ 147 title: `结果 ${index + 1}`, 148 forum: '编程吧', 149 author: `作者 ${index + 1}`, 150 time: '2026-03-29', 151 snippet: `摘要 ${index + 1}`, 152 id: String(2000 + index), 153 url: `https://tieba.baidu.com/p/${2000 + index}`, 154 })); 155 const items = buildTiebaSearchItems(raw, 25); 156 expect(items).toHaveLength(20); 157 expect(items[19]).toMatchObject({ 158 rank: 20, 159 id: '2019', 160 url: 'https://tieba.baidu.com/p/2019', 161 }); 162 }); 163 it('fills missing search ids from stable thread urls', () => { 164 const items = buildTiebaSearchItems([ 165 { 166 title: '搜索结果', 167 forum: '编程吧', 168 author: '作者', 169 time: '2026-03-29 11:35', 170 snippet: '摘要', 171 id: '', 172 url: 'https://tieba.baidu.com/p/654321', 173 }, 174 ], 5); 175 expect(items[0]).toMatchObject({ 176 id: '654321', 177 url: 'https://tieba.baidu.com/p/654321', 178 }); 179 }); 180 }); 181 describe('buildTiebaReadItems', () => { 182 it('prefers visible main-post fields and still keeps floor 1 for media-only threads', () => { 183 const items = buildTiebaReadItems({ 184 mainPost: { 185 title: '刚开始读博士的人据说都这样', 186 author: '湖水之岸', 187 contentText: '', 188 structuredText: '', 189 visibleTime: '03-24', 190 structuredTime: 1774343231, 191 hasMedia: true, 192 }, 193 replies: [], 194 }, { limit: 5, includeMainPost: true }); 195 expect(items).toEqual([ 196 { 197 floor: 1, 198 author: '湖水之岸', 199 content: '刚开始读博士的人据说都这样 [media]', 200 time: '03-24', 201 }, 202 ]); 203 }); 204 it('falls back to structured main-post data when visible text is missing', () => { 205 const items = buildTiebaReadItems({ 206 mainPost: { 207 title: '标题', 208 author: '', 209 fallbackAuthor: '结构化作者', 210 contentText: '', 211 structuredText: '结构化正文', 212 visibleTime: '', 213 structuredTime: 1774343231, 214 hasMedia: false, 215 }, 216 replies: [ 217 { floor: 2, author: '回复者', content: '二楼内容', time: '第2楼 2026-03-25 12:34 广东' }, 218 ], 219 }, { limit: 5, includeMainPost: true }); 220 expect(items[0]).toMatchObject({ 221 floor: 1, 222 author: '结构化作者', 223 content: '标题 结构化正文', 224 time: '2026-03-24 17:07', 225 }); 226 expect(items[1]).toMatchObject({ 227 floor: 2, 228 author: '回复者', 229 content: '二楼内容', 230 time: '2026-03-25 12:34', 231 }); 232 }); 233 it('strips trailing location metadata from reply times', () => { 234 const items = buildTiebaReadItems({ 235 mainPost: { 236 title: '主楼', 237 author: '楼主', 238 contentText: '正文', 239 visibleTime: '03-24', 240 }, 241 replies: [ 242 { floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 3小时前 福建' }, 243 { floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 刚刚 江苏' }, 244 ], 245 }, { limit: 5, includeMainPost: false }); 246 expect(items).toEqual([ 247 { 248 floor: 2, 249 author: '二楼', 250 content: '二楼内容', 251 time: '3小时前', 252 }, 253 { 254 floor: 3, 255 author: '三楼', 256 content: '三楼内容', 257 time: '刚刚', 258 }, 259 ]); 260 }); 261 it('counts limit as replies and skips main post on later pages', () => { 262 const items = buildTiebaReadItems({ 263 mainPost: { 264 title: '主楼', 265 author: '楼主', 266 contentText: '正文', 267 visibleTime: '03-24', 268 }, 269 replies: [ 270 { floor: 2, author: '二楼', content: '二楼内容', time: '第2楼 03-25' }, 271 { floor: 3, author: '三楼', content: '三楼内容', time: '第3楼 03-26' }, 272 { floor: 4, author: '四楼', content: '四楼内容', time: '第4楼 03-27' }, 273 ], 274 }, { limit: 2, includeMainPost: true }); 275 expect(items).toHaveLength(3); 276 expect(items.map((item) => item.floor)).toEqual([1, 2, 3]); 277 const page2 = buildTiebaReadItems({ 278 mainPost: { 279 title: '主楼', 280 author: '楼主', 281 contentText: '正文', 282 visibleTime: '03-24', 283 }, 284 replies: [ 285 { floor: 26, author: '二十六楼', content: '二十六楼内容', time: '第26楼 03-29' }, 286 ], 287 }, { limit: 2, includeMainPost: false }); 288 expect(page2.map((item) => item.floor)).toEqual([26]); 289 }); 290 });