|
| 1 | +import { describe, expect, it, vi } from 'vitest'; |
| 2 | +import type { IPage } from '../../types.js'; |
| 3 | +import { getRegistry } from '../../registry.js'; |
| 4 | +import { appendAudienceRows, appendTrendRows, parseCreatorNoteDetailText } from './creator-note-detail.js'; |
| 5 | +import './creator-note-detail.js'; |
| 6 | + |
| 7 | +function createPageMock(evaluateResult: any): IPage { |
| 8 | + return { |
| 9 | + goto: vi.fn().mockResolvedValue(undefined), |
| 10 | + evaluate: vi.fn().mockResolvedValue(evaluateResult), |
| 11 | + snapshot: vi.fn().mockResolvedValue(undefined), |
| 12 | + click: vi.fn().mockResolvedValue(undefined), |
| 13 | + typeText: vi.fn().mockResolvedValue(undefined), |
| 14 | + pressKey: vi.fn().mockResolvedValue(undefined), |
| 15 | + wait: vi.fn().mockResolvedValue(undefined), |
| 16 | + tabs: vi.fn().mockResolvedValue([]), |
| 17 | + closeTab: vi.fn().mockResolvedValue(undefined), |
| 18 | + newTab: vi.fn().mockResolvedValue(undefined), |
| 19 | + selectTab: vi.fn().mockResolvedValue(undefined), |
| 20 | + networkRequests: vi.fn().mockResolvedValue([]), |
| 21 | + consoleMessages: vi.fn().mockResolvedValue([]), |
| 22 | + scroll: vi.fn().mockResolvedValue(undefined), |
| 23 | + autoScroll: vi.fn().mockResolvedValue(undefined), |
| 24 | + installInterceptor: vi.fn().mockResolvedValue(undefined), |
| 25 | + getInterceptedRequests: vi.fn().mockResolvedValue([]), |
| 26 | + getCookies: vi.fn().mockResolvedValue([]), |
| 27 | + screenshot: vi.fn().mockResolvedValue(''), |
| 28 | + }; |
| 29 | +} |
| 30 | + |
| 31 | +describe('xiaohongshu creator-note-detail', () => { |
| 32 | + it('parses note detail page text into info and metric rows', () => { |
| 33 | + const bodyText = `笔记数据详情 |
| 34 | +一张图讲清 诡秘之主·耕种者途径 |
| 35 | +#诡秘之主 |
| 36 | +#耕种者序列 |
| 37 | +2026-03-18 20:01 |
| 38 | +切换笔记 |
| 39 | +笔记诊断 |
| 40 | +核心数据 |
| 41 | +观看来源 |
| 42 | +观众画像 |
| 43 | +核心数据 |
| 44 | +基础数据 |
| 45 | +部分数据统计中,次日可查看 |
| 46 | +导出数据 |
| 47 | +曝光数 |
| 48 | +1733 |
| 49 | +粉丝占比 6.6% |
| 50 | +实时 |
| 51 | +观看数 |
| 52 | +544 |
| 53 | +粉丝占比 7.2% |
| 54 | +封面点击率 |
| 55 | +18.6% |
| 56 | +粉丝 19.1% |
| 57 | +平均观看时长 |
| 58 | +51.5秒 |
| 59 | +粉丝 55.8秒 |
| 60 | +涨粉数 |
| 61 | +3 |
| 62 | +按小时 |
| 63 | +按天 |
| 64 | +互动数据 |
| 65 | +数据实时更新 |
| 66 | +导出数据 |
| 67 | +点赞数 |
| 68 | +19 |
| 69 | +粉丝占比 60% |
| 70 | +评论数 |
| 71 | +7 |
| 72 | +粉丝占比 33.3% |
| 73 | +收藏数 |
| 74 | +10 |
| 75 | +粉丝占比 33.3% |
| 76 | +分享数 |
| 77 | +6 |
| 78 | +粉丝占比 0%`; |
| 79 | + |
| 80 | + expect(parseCreatorNoteDetailText(bodyText, '69ba940500000000200384db')).toEqual([ |
| 81 | + { section: '笔记信息', metric: 'note_id', value: '69ba940500000000200384db', extra: '' }, |
| 82 | + { section: '笔记信息', metric: 'title', value: '一张图讲清 诡秘之主·耕种者途径', extra: '' }, |
| 83 | + { section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' }, |
| 84 | + { section: '基础数据', metric: '曝光数', value: '1733', extra: '粉丝占比 6.6%' }, |
| 85 | + { section: '基础数据', metric: '观看数', value: '544', extra: '粉丝占比 7.2%' }, |
| 86 | + { section: '基础数据', metric: '封面点击率', value: '18.6%', extra: '粉丝 19.1%' }, |
| 87 | + { section: '基础数据', metric: '平均观看时长', value: '51.5秒', extra: '粉丝 55.8秒' }, |
| 88 | + { section: '基础数据', metric: '涨粉数', value: '3', extra: '' }, |
| 89 | + { section: '互动数据', metric: '点赞数', value: '19', extra: '粉丝占比 60%' }, |
| 90 | + { section: '互动数据', metric: '评论数', value: '7', extra: '粉丝占比 33.3%' }, |
| 91 | + { section: '互动数据', metric: '收藏数', value: '10', extra: '粉丝占比 33.3%' }, |
| 92 | + { section: '互动数据', metric: '分享数', value: '6', extra: '粉丝占比 0%' }, |
| 93 | + ]); |
| 94 | + }); |
| 95 | + |
| 96 | + it('appends audience source and portrait rows from API payloads', () => { |
| 97 | + const rows = appendAudienceRows([], { |
| 98 | + audienceSource: { |
| 99 | + source: [ |
| 100 | + { |
| 101 | + title: '首页推荐', |
| 102 | + value_with_double: 89.9, |
| 103 | + info: { |
| 104 | + imp_count: 1469, |
| 105 | + view_count: 276, |
| 106 | + interaction_count: 15, |
| 107 | + }, |
| 108 | + }, |
| 109 | + ], |
| 110 | + }, |
| 111 | + audienceSourceDetail: { |
| 112 | + gender: [ |
| 113 | + { title: '男性', value: 82 }, |
| 114 | + { title: '女性', value: 18 }, |
| 115 | + ], |
| 116 | + age: [ |
| 117 | + { title: '25-34', value: 55 }, |
| 118 | + ], |
| 119 | + city: [ |
| 120 | + { title: '上海', value: 8 }, |
| 121 | + ], |
| 122 | + interest: [ |
| 123 | + { title: '二次元', value: 13 }, |
| 124 | + ], |
| 125 | + }, |
| 126 | + }); |
| 127 | + |
| 128 | + expect(rows).toEqual([ |
| 129 | + { section: '观看来源', metric: '首页推荐', value: '89.9%', extra: '曝光 1469 · 观看 276 · 互动 15' }, |
| 130 | + { section: '观众画像', metric: '性别/男性', value: '82%', extra: '' }, |
| 131 | + { section: '观众画像', metric: '性别/女性', value: '18%', extra: '' }, |
| 132 | + { section: '观众画像', metric: '年龄/25-34', value: '55%', extra: '' }, |
| 133 | + { section: '观众画像', metric: '城市/上海', value: '8%', extra: '' }, |
| 134 | + { section: '观众画像', metric: '兴趣/二次元', value: '13%', extra: '' }, |
| 135 | + ]); |
| 136 | + }); |
| 137 | + |
| 138 | + it('appends trend summary rows from hour/day series payloads', () => { |
| 139 | + const rows = appendTrendRows([], { |
| 140 | + audienceTrend: { |
| 141 | + no_data: true, |
| 142 | + no_data_tip_msg: '数据统计中,请稍后查看', |
| 143 | + }, |
| 144 | + noteBase: { |
| 145 | + hour: { |
| 146 | + view_list: [ |
| 147 | + { date: new Date('2026-03-18T21:00:00+08:00').getTime(), count: 54 }, |
| 148 | + { date: new Date('2026-03-18T22:00:00+08:00').getTime(), count: 51 }, |
| 149 | + ], |
| 150 | + like_list: [ |
| 151 | + { date: new Date('2026-03-18T21:00:00+08:00').getTime(), count: 2 }, |
| 152 | + ], |
| 153 | + }, |
| 154 | + day: { |
| 155 | + view_list: [ |
| 156 | + { date: new Date('2026-03-18T00:00:00+08:00').getTime(), count: 307 }, |
| 157 | + ], |
| 158 | + }, |
| 159 | + }, |
| 160 | + }); |
| 161 | + |
| 162 | + expect(rows).toEqual([ |
| 163 | + { section: '趋势说明', metric: '观众趋势', value: '暂不可用', extra: '数据统计中,请稍后查看' }, |
| 164 | + { section: '趋势数据', metric: '按小时/观看数', value: '2 points', extra: '03-18 21:00=54 | 03-18 22:00=51' }, |
| 165 | + { section: '趋势数据', metric: '按小时/点赞数', value: '1 points', extra: '03-18 21:00=2' }, |
| 166 | + { section: '趋势数据', metric: '按天/观看数', value: '1 points', extra: '2026-03-18=307' }, |
| 167 | + ]); |
| 168 | + }); |
| 169 | + |
| 170 | + it('navigates to the note detail page and returns parsed rows', async () => { |
| 171 | + const cmd = getRegistry().get('xiaohongshu/creator-note-detail'); |
| 172 | + expect(cmd?.func).toBeTypeOf('function'); |
| 173 | + |
| 174 | + const page = createPageMock(`笔记数据详情 |
| 175 | +示例笔记 |
| 176 | +2026-03-19 12:00 |
| 177 | +曝光数 |
| 178 | +100 |
| 179 | +粉丝占比 10% |
| 180 | +观看数 |
| 181 | +50 |
| 182 | +粉丝占比 20% |
| 183 | +封面点击率 |
| 184 | +12% |
| 185 | +粉丝 11% |
| 186 | +平均观看时长 |
| 187 | +30秒 |
| 188 | +粉丝 31秒 |
| 189 | +涨粉数 |
| 190 | +2 |
| 191 | +点赞数 |
| 192 | +8 |
| 193 | +粉丝占比 25% |
| 194 | +评论数 |
| 195 | +1 |
| 196 | +粉丝占比 0% |
| 197 | +收藏数 |
| 198 | +3 |
| 199 | +粉丝占比 50% |
| 200 | +分享数 |
| 201 | +0 |
| 202 | +粉丝占比 0%`); |
| 203 | + |
| 204 | + const result = await cmd!.func!(page, { note_id: 'demo-note-id' }); |
| 205 | + |
| 206 | + expect((page.goto as any).mock.calls[0][0]).toBe('https://creator.xiaohongshu.com/statistics/note-detail?noteId=demo-note-id'); |
| 207 | + expect((page.evaluate as any).mock.calls[0][0]).toBe('() => document.body.innerText'); |
| 208 | + expect(result).toEqual([ |
| 209 | + { section: '笔记信息', metric: 'note_id', value: 'demo-note-id', extra: '' }, |
| 210 | + { section: '笔记信息', metric: 'title', value: '示例笔记', extra: '' }, |
| 211 | + { section: '笔记信息', metric: 'published_at', value: '2026-03-19 12:00', extra: '' }, |
| 212 | + { section: '基础数据', metric: '曝光数', value: '100', extra: '粉丝占比 10%' }, |
| 213 | + { section: '基础数据', metric: '观看数', value: '50', extra: '粉丝占比 20%' }, |
| 214 | + { section: '基础数据', metric: '封面点击率', value: '12%', extra: '粉丝 11%' }, |
| 215 | + { section: '基础数据', metric: '平均观看时长', value: '30秒', extra: '粉丝 31秒' }, |
| 216 | + { section: '基础数据', metric: '涨粉数', value: '2', extra: '' }, |
| 217 | + { section: '互动数据', metric: '点赞数', value: '8', extra: '粉丝占比 25%' }, |
| 218 | + { section: '互动数据', metric: '评论数', value: '1', extra: '粉丝占比 0%' }, |
| 219 | + { section: '互动数据', metric: '收藏数', value: '3', extra: '粉丝占比 50%' }, |
| 220 | + { section: '互动数据', metric: '分享数', value: '0', extra: '粉丝占比 0%' }, |
| 221 | + ]); |
| 222 | + }); |
| 223 | +}); |
0 commit comments