diff --git a/mobile/src/api/note-prompts.test.ts b/mobile/src/api/note-prompts.test.ts new file mode 100644 index 0000000..a622532 --- /dev/null +++ b/mobile/src/api/note-prompts.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const fetchMock = vi.fn(); + +vi.mock('./client', () => ({ + getApiClient: () => ({ + fetch: fetchMock, + }), +})); + +import { + listPromptTemplates, + runPrompt, + suggestTags, + extractFromUrl, + copilotTransform, + getReadingTime, +} from './note-prompts'; + +describe('note-prompts API client', () => { + beforeEach(() => { + fetchMock.mockReset(); + }); + + it('listPromptTemplates returns items', async () => { + fetchMock.mockResolvedValue({ items: [{ id: 't1', slug: 'summarize' }] }); + const result = await listPromptTemplates(); + expect(result).toEqual([{ id: 't1', slug: 'summarize' }]); + expect(fetchMock).toHaveBeenCalledWith('/note-prompts'); + }); + + it('runPrompt posts input', async () => { + fetchMock.mockResolvedValue({ content: 'Summary', templateSlug: 'summarize' }); + const result = await runPrompt({ templateId: 'summarize', noteId: 'n1', workspaceId: 'ws1' }); + expect(result.content).toBe('Summary'); + expect(fetchMock).toHaveBeenCalledWith('/note-prompts/run', expect.objectContaining({ method: 'POST' })); + }); + + it('suggestTags returns tag array', async () => { + fetchMock.mockResolvedValue({ tags: ['tag1', 'tag2'] }); + const tags = await suggestTags('n1', 'ws1'); + expect(tags).toEqual(['tag1', 'tag2']); + }); + + it('extractFromUrl sends url and workspaceId', async () => { + fetchMock.mockResolvedValue({ title: 'Page', content: 'text', url: 'https://example.com', summarized: true }); + const result = await extractFromUrl('https://example.com', 'ws1'); + expect(result.title).toBe('Page'); + expect(fetchMock).toHaveBeenCalledWith('/note-prompts/url-extract', expect.objectContaining({ method: 'POST' })); + }); + + it('copilotTransform sends action and text', async () => { + fetchMock.mockResolvedValue({ text: 'Transformed' }); + const result = await copilotTransform('n1', 'ws1', 'shorten', 'Long text'); + expect(result).toBe('Transformed'); + }); + + it('getReadingTime returns word count and minutes', async () => { + fetchMock.mockResolvedValue({ wordCount: 500, readingTimeMinutes: 3 }); + const result = await getReadingTime('n1', 'ws1'); + expect(result.wordCount).toBe(500); + expect(result.readingTimeMinutes).toBe(3); + }); +}); diff --git a/mobile/src/store/prompt-store.test.ts b/mobile/src/store/prompt-store.test.ts new file mode 100644 index 0000000..2eff7f2 --- /dev/null +++ b/mobile/src/store/prompt-store.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const listPromptTemplatesMock = vi.fn(); +const runPromptMock = vi.fn(); + +vi.mock('../api/note-prompts', () => ({ + listPromptTemplates: (...args: unknown[]) => listPromptTemplatesMock(...args), + runPrompt: (...args: unknown[]) => runPromptMock(...args), +})); + +import { usePromptStore } from './prompt-store'; + +function resetStore() { + usePromptStore.setState({ + templates: [], + isLoading: false, + isRunning: false, + lastResult: null, + error: null, + }); +} + +describe('prompt-store', () => { + beforeEach(() => { + vi.clearAllMocks(); + resetStore(); + }); + + it('initial state is empty', () => { + const state = usePromptStore.getState(); + expect(state.templates).toEqual([]); + expect(state.isLoading).toBe(false); + expect(state.isRunning).toBe(false); + expect(state.lastResult).toBeNull(); + expect(state.error).toBeNull(); + }); + + it('fetchTemplates populates templates on success', async () => { + listPromptTemplatesMock.mockResolvedValue([ + { id: 't1', slug: 'summarize', name: 'Summarize' }, + ]); + + await usePromptStore.getState().fetchTemplates(); + + const state = usePromptStore.getState(); + expect(state.templates).toHaveLength(1); + expect(state.templates[0].slug).toBe('summarize'); + expect(state.isLoading).toBe(false); + }); + + it('fetchTemplates sets error on failure', async () => { + listPromptTemplatesMock.mockRejectedValue(new Error('Network error')); + + await usePromptStore.getState().fetchTemplates(); + + const state = usePromptStore.getState(); + expect(state.error).toBe('Network error'); + expect(state.isLoading).toBe(false); + }); + + it('runPrompt stores result on success', async () => { + runPromptMock.mockResolvedValue({ + content: 'Summary text', + templateSlug: 'summarize', + outputType: 'new_note', + }); + + const result = await usePromptStore.getState().runPrompt({ + templateId: 'summarize', + noteId: 'n1', + workspaceId: 'ws1', + }); + + expect(result).toBeTruthy(); + expect(result!.content).toBe('Summary text'); + const state = usePromptStore.getState(); + expect(state.lastResult!.content).toBe('Summary text'); + expect(state.isRunning).toBe(false); + }); + + it('runPrompt sets error on failure', async () => { + runPromptMock.mockRejectedValue(new Error('LLM timeout')); + + const result = await usePromptStore.getState().runPrompt({ + templateId: 'summarize', + noteId: 'n1', + workspaceId: 'ws1', + }); + + expect(result).toBeNull(); + const state = usePromptStore.getState(); + expect(state.error).toBe('LLM timeout'); + expect(state.isRunning).toBe(false); + }); + + it('clearResult resets lastResult', async () => { + runPromptMock.mockResolvedValue({ content: 'Result', templateSlug: 'x', outputType: 'new_note' }); + await usePromptStore.getState().runPrompt({ templateId: 'x', noteId: 'n1', workspaceId: 'ws1' }); + expect(usePromptStore.getState().lastResult).not.toBeNull(); + + usePromptStore.getState().clearResult(); + expect(usePromptStore.getState().lastResult).toBeNull(); + }); + + it('clearError resets error', async () => { + runPromptMock.mockRejectedValue(new Error('fail')); + await usePromptStore.getState().runPrompt({ templateId: 'x', noteId: 'n1', workspaceId: 'ws1' }); + expect(usePromptStore.getState().error).toBe('fail'); + + usePromptStore.getState().clearError(); + expect(usePromptStore.getState().error).toBeNull(); + }); +});