/ clis / tieba / utils.test.js
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  });