90 lines
3.2 KiB
TypeScript
90 lines
3.2 KiB
TypeScript
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',
|
|
});
|
|
});
|
|
});
|