Files
makelore/tests/unit/learning-client.test.ts

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',
});
});
});