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