video.test.js
1 import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 import { CommandExecutionError } from '@jackwener/opencli/errors'; 3 4 const { mockApiGet } = vi.hoisted(() => ({ 5 mockApiGet: vi.fn(), 6 })); 7 8 vi.mock('./utils.js', async (importOriginal) => ({ 9 ...(await importOriginal()), 10 apiGet: mockApiGet, 11 })); 12 13 import { getRegistry } from '@jackwener/opencli/registry'; 14 import './video.js'; 15 16 describe('bilibili video', () => { 17 const command = getRegistry().get('bilibili/video'); 18 const page = { 19 goto: vi.fn().mockResolvedValue(undefined), 20 evaluate: vi.fn(), 21 }; 22 23 beforeEach(() => { 24 mockApiGet.mockReset(); 25 page.goto.mockClear(); 26 page.evaluate.mockReset(); 27 }); 28 29 it('returns a field/value table of video metadata on success', async () => { 30 mockApiGet.mockResolvedValueOnce({ 31 code: 0, 32 data: { 33 bvid: 'BV1xx411c7mD', 34 aid: 12345678, 35 title: '三层结构笔记法', 36 tname: '教程', 37 pubdate: 1775053078, // 2026-04-01 14:17:58 UTC 38 duration: 434, 39 videos: 1, 40 pic: 'https://i1.hdslb.com/some.jpg', 41 desc: 'Obsidian 教程', 42 owner: { mid: 507578555, name: 'IOI科技' }, 43 stat: { view: 6128, danmaku: 0, reply: 21, like: 162, coin: 48, favorite: 564, share: 26 }, 44 }, 45 }); 46 47 const rows = await command.func(page, { bvid: 'BV1xx411c7mD' }); 48 49 // Every row has { field, value } 50 expect(Array.isArray(rows)).toBe(true); 51 for (const row of rows) { 52 expect(row).toHaveProperty('field'); 53 expect(row).toHaveProperty('value'); 54 } 55 56 const byField = Object.fromEntries(rows.map((r) => [r.field, r.value])); 57 expect(byField.bvid).toBe('BV1xx411c7mD'); 58 expect(byField.title).toBe('三层结构笔记法'); 59 expect(byField.author).toBe('IOI科技 (mid: 507578555)'); 60 expect(byField.duration).toBe('7m14s (434s)'); 61 expect(byField.view).toBe('6128'); 62 expect(byField.like).toBe('162'); 63 64 // Navigation primes the session 65 expect(page.goto).toHaveBeenCalledWith('https://www.bilibili.com/video/BV1xx411c7mD/'); 66 // API called without signing 67 expect(mockApiGet).toHaveBeenCalledWith(page, '/x/web-interface/view', { params: { bvid: 'BV1xx411c7mD' } }); 68 }); 69 70 it('throws CommandExecutionError when bilibili view API returns non-zero code', async () => { 71 mockApiGet.mockResolvedValueOnce({ 72 code: -404, 73 message: '啥都木有', 74 data: null, 75 }); 76 77 await expect(command.func(page, { bvid: 'BV1xx411c7mD' })).rejects.toSatisfy( 78 (err) => err instanceof CommandExecutionError && /啥都木有|-404/.test(err.message), 79 ); 80 }); 81 82 it('extracts BV ID from full bilibili.com URL input', async () => { 83 mockApiGet.mockResolvedValueOnce({ 84 code: 0, 85 data: { bvid: 'BV1xx411c7mD', stat: {}, owner: {}, desc: '' }, 86 }); 87 88 await command.func(page, { bvid: 'https://www.bilibili.com/video/BV1xx411c7mD/' }); 89 90 expect(page.goto).toHaveBeenCalledWith('https://www.bilibili.com/video/BV1xx411c7mD/'); 91 expect(mockApiGet).toHaveBeenCalledWith(page, '/x/web-interface/view', { params: { bvid: 'BV1xx411c7mD' } }); 92 }); 93 94 it('extracts BV ID from bilibili URL with trailing query string', async () => { 95 mockApiGet.mockResolvedValueOnce({ 96 code: 0, 97 data: { bvid: 'BV1Je9EBnEha', stat: {}, owner: {}, desc: '' }, 98 }); 99 100 await command.func(page, { 101 bvid: 'https://www.bilibili.com/video/BV1Je9EBnEha/?spm_id_from=333.1007&vd_source=abc', 102 }); 103 104 expect(mockApiGet).toHaveBeenCalledWith(page, '/x/web-interface/view', { params: { bvid: 'BV1Je9EBnEha' } }); 105 }); 106 107 it('extracts BV ID from m.bilibili.com mobile URL', async () => { 108 mockApiGet.mockResolvedValueOnce({ 109 code: 0, 110 data: { bvid: 'BV1xx411c7mD', stat: {}, owner: {}, desc: '' }, 111 }); 112 113 await command.func(page, { bvid: 'https://m.bilibili.com/video/BV1xx411c7mD' }); 114 115 expect(mockApiGet).toHaveBeenCalledWith(page, '/x/web-interface/view', { params: { bvid: 'BV1xx411c7mD' } }); 116 }); 117 118 it('returns full description without truncation or whitespace collapse', async () => { 119 const longDesc = '第一行描述\n\n第二段,有多个空格 和换行\n\n' + 'x'.repeat(500); 120 mockApiGet.mockResolvedValueOnce({ 121 code: 0, 122 data: { bvid: 'BV1xx411c7mD', stat: {}, owner: {}, desc: longDesc }, 123 }); 124 125 const rows = await command.func(page, { bvid: 'BV1xx411c7mD' }); 126 const byField = Object.fromEntries(rows.map((r) => [r.field, r.value])); 127 // JSON/YAML consumers must receive the complete description verbatim, 128 // including original whitespace and length > 200 chars. 129 expect(byField.description).toBe(longDesc); 130 expect(byField.description.length).toBeGreaterThan(200); 131 }); 132 });