news.test.js
1 import { describe, it, expect, vi, afterEach } from 'vitest'; 2 const SAMPLE_RSS = `<?xml version="1.0" encoding="UTF-8"?> 3 <rss version="2.0"><channel><title>36氪</title> 4 <item> 5 <title>红杉中国领投AI公司「示例」,金额近2亿元</title> 6 <link><![CDATA[https://36kr.com/p/1111111111111111?f=rss]]></link> 7 <pubDate>2026-03-26 10:00:00 +0800</pubDate> 8 </item> 9 <item> 10 <title>马斯克旗下xAI估值突破1000亿美元</title> 11 <link><![CDATA[https://36kr.com/p/2222222222222222?f=rss]]></link> 12 <pubDate>2026-03-26 09:00:00 +0800</pubDate> 13 </item> 14 <item> 15 <title>OpenAI发布GPT-5,多模态能力大幅提升</title> 16 <link><![CDATA[https://36kr.com/p/3333333333333333?f=rss]]></link> 17 <pubDate>2026-03-25 20:00:00 +0800</pubDate> 18 </item> 19 </channel></rss>`; 20 afterEach(() => { 21 vi.restoreAllMocks(); 22 }); 23 describe('36kr/news RSS parsing', () => { 24 it('parses RSS feed into ranked news items', async () => { 25 vi.spyOn(globalThis, 'fetch').mockResolvedValue({ 26 ok: true, 27 text: async () => SAMPLE_RSS, 28 }); 29 // Direct RSS parse test using the same regex logic as news.ts 30 const xml = SAMPLE_RSS; 31 const items = []; 32 const itemRegex = /<item>([\s\S]*?)<\/item>/g; 33 let match; 34 while ((match = itemRegex.exec(xml)) && items.length < 10) { 35 const block = match[1]; 36 const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? ''; 37 const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ?? 38 block.match(/<link>(.*?)<\/link>/)?.[1] ?? 39 ''; 40 const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? ''; 41 const date = pubDate.slice(0, 10); 42 if (title) 43 items.push({ rank: items.length + 1, title, date, url: url.trim() }); 44 } 45 expect(items).toHaveLength(3); 46 expect(items[0].rank).toBe(1); 47 expect(items[0].title).toBe('红杉中国领投AI公司「示例」,金额近2亿元'); 48 expect(items[0].date).toBe('2026-03-26'); 49 expect(items[0].url).toBe('https://36kr.com/p/1111111111111111?f=rss'); 50 }); 51 it('respects limit — returns at most N items', async () => { 52 const xml = SAMPLE_RSS; 53 const limit = 2; 54 const items = []; 55 const itemRegex = /<item>([\s\S]*?)<\/item>/g; 56 let match; 57 while ((match = itemRegex.exec(xml)) && items.length < limit) { 58 const block = match[1]; 59 const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? ''; 60 const url = block.match(/<link><!\[CDATA\[(.*?)\]\]>/)?.[1] ?? ''; 61 const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? ''; 62 const date = pubDate.slice(0, 10); 63 if (title) 64 items.push({ rank: items.length + 1, title, date, url: url.trim() }); 65 } 66 expect(items).toHaveLength(2); 67 }); 68 it('skips items with empty title', async () => { 69 const xml = `<rss><channel> 70 <item><title></title><link>https://36kr.com/p/0</link><pubDate>2026-01-01</pubDate></item> 71 <item><title>有标题的文章</title><link>https://36kr.com/p/1</link><pubDate>2026-01-01</pubDate></item> 72 </channel></rss>`; 73 const items = []; 74 const itemRegex = /<item>([\s\S]*?)<\/item>/g; 75 let match; 76 while ((match = itemRegex.exec(xml))) { 77 const block = match[1]; 78 const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? ''; 79 if (title) 80 items.push({ title }); 81 } 82 expect(items).toHaveLength(1); 83 expect(items[0].title).toBe('有标题的文章'); 84 }); 85 });