import { beforeEach, describe, expect, it, vi } from 'vitest'; import { downloadLearningProject, fetchLearningProject, fetchLearningProjectMedia, fetchLearningProjects, openLearningExternalLink, } from '@/lib/learning'; const hostApiFetchMock = vi.hoisted(() => vi.fn()); const invokeIpcMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), })); describe('Learning project renderer client', () => { beforeEach(() => { hostApiFetchMock.mockReset(); invokeIpcMock.mockReset(); }); it('reads paged projects and project detail through the Host API', async () => { const page = { items: [], nextCursor: 'next' }; const detail = { id: 'project-1', name: '机械臂' }; hostApiFetchMock .mockResolvedValueOnce({ success: true, data: page }) .mockResolvedValueOnce({ success: true, data: detail }); await expect(fetchLearningProjects({ cursor: 'cursor one', limit: 24 })).resolves.toBe(page); await expect(fetchLearningProject('project/one')).resolves.toBe(detail); expect(hostApiFetchMock).toHaveBeenNthCalledWith( 1, '/api/works/learning/projects?cursor=cursor+one&limit=24', undefined, ); expect(hostApiFetchMock).toHaveBeenNthCalledWith( 2, '/api/works/learning/projects/project%2Fone', undefined, ); }); it('starts the native Main-owned download through a fixed Host route', async () => { hostApiFetchMock.mockResolvedValue({ success: true, data: { status: 'saved' } }); await expect(downloadLearningProject('project-1')).resolves.toEqual({ status: 'saved' }); expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/works/learning/projects/project-1/download', { method: 'POST' }, ); expect(invokeIpcMock).not.toHaveBeenCalled(); }); it('accepts only fixed project media paths and validated raster data', async () => { hostApiFetchMock.mockResolvedValue({ mimeType: 'image/png', dataBase64: 'AQID' }); await expect(fetchLearningProjectMedia( '/api/learning/projects/project-1/media/readme-1', )).resolves.toBe('data:image/png;base64,AQID'); expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/works/learning/projects/project-1/media/readme-1', ); await expect(fetchLearningProjectMedia('https://tracker.example/image.png')).rejects.toMatchObject({ code: 'LEARNING_INVALID_MEDIA_URL', }); hostApiFetchMock.mockResolvedValueOnce({ mimeType: 'image/svg+xml', dataBase64: 'AQID' }); await expect(fetchLearningProjectMedia( '/api/learning/projects/project-1/media/readme-2', )).rejects.toMatchObject({ code: 'LEARNING_INVALID_MEDIA_RESPONSE' }); }); it('opens only credential-free HTTPS README links through the IPC facade', async () => { invokeIpcMock.mockResolvedValue(undefined); await openLearningExternalLink('https://example.com/guide'); expect(invokeIpcMock).toHaveBeenCalledWith('shell:openExternal', 'https://example.com/guide'); await expect(openLearningExternalLink('http://example.com')).rejects.toMatchObject({ code: 'LEARNING_INVALID_LINK', }); }); });