import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { nextTick } from 'vue'; import type { FetchError } from 'ofetch'; import type { ApiError } from '~/types/api/error'; import { useApi } from './useApi'; // Mock dependencies vi.mock('#app', () => ({ useRuntimeConfig: vi.fn(() => ({ public: { apiBase: 'http://localhost:3100/api', }, })), })); // Mock $fetch globally global.$fetch = vi.fn(); describe('useApi', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('GET requests', () => { it('should make a GET request and populate data on success', async () => { const mockData = { id: 1, name: 'Test' }; vi.mocked($fetch).mockResolvedValueOnce(mockData); const api = useApi(); const result = api.get('/test'); // Should start loading await nextTick(); expect(result.loading.value).toBe(false); // Immediate execution completes quickly // Wait for the async operation await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData)); expect($fetch).toHaveBeenCalledWith('/test', { baseURL: 'http://localhost:3100/api', method: 'GET', body: undefined, }); expect(result.data.value).toEqual(mockData); expect(result.error.value).toBeNull(); expect(result.loading.value).toBe(false); }); it('should handle GET request with custom options', async () => { const mockData = { result: 'success' }; vi.mocked($fetch).mockResolvedValueOnce(mockData); const api = useApi(); const result = api.get('/test', { headers: { 'X-Custom': 'header' } }); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData)); expect($fetch).toHaveBeenCalledWith('/test', { baseURL: 'http://localhost:3100/api', method: 'GET', headers: { 'X-Custom': 'header' }, body: undefined, }); }); it('should not execute immediately when immediate is false', async () => { const api = useApi(); const result = api.get('/test', {}, false); expect($fetch).not.toHaveBeenCalled(); expect(result.data.value).toBeNull(); expect(result.loading.value).toBe(false); }); it('should execute when run() is called manually', async () => { const mockData = { manual: true }; vi.mocked($fetch).mockResolvedValueOnce(mockData); const api = useApi(); const result = api.get('/test', {}, false); expect($fetch).not.toHaveBeenCalled(); await result.run(); expect($fetch).toHaveBeenCalledWith('/test', { baseURL: 'http://localhost:3100/api', method: 'GET', body: undefined, }); expect(result.data.value).toEqual(mockData); }); }); describe('DELETE requests', () => { it('should make a DELETE request', async () => { const mockData = { deleted: true }; vi.mocked($fetch).mockResolvedValueOnce(mockData); const api = useApi(); const result = api.del('/test/1'); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData)); expect($fetch).toHaveBeenCalledWith('/test/1', { baseURL: 'http://localhost:3100/api', method: 'DELETE', body: undefined, }); expect(result.data.value).toEqual(mockData); }); }); describe('POST requests', () => { it('should make a POST request with body', async () => { const mockResponse = { id: 1, created: true }; const requestBody = { name: 'New Item' }; vi.mocked($fetch).mockResolvedValueOnce(mockResponse); const api = useApi(); const result = api.post( '/test', {}, true, requestBody, ); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse)); expect($fetch).toHaveBeenCalledWith('/test', { baseURL: 'http://localhost:3100/api', method: 'POST', body: requestBody, }); expect(result.data.value).toEqual(mockResponse); }); it('should allow run() to be called with a different body', async () => { const mockResponse = { success: true }; vi.mocked($fetch).mockResolvedValueOnce(mockResponse); const api = useApi(); const result = api.post('/test', {}, false); const body = { data: 'runtime-data' }; await result.run(body); expect($fetch).toHaveBeenCalledWith('/test', { baseURL: 'http://localhost:3100/api', method: 'POST', body, }); expect(result.data.value).toEqual(mockResponse); }); }); describe('PUT requests', () => { it('should make a PUT request with body', async () => { const mockResponse = { updated: true }; const requestBody = { name: 'Updated Item' }; vi.mocked($fetch).mockResolvedValueOnce(mockResponse); const api = useApi(); const result = api.put( '/test/1', {}, true, requestBody, ); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse)); expect($fetch).toHaveBeenCalledWith('/test/1', { baseURL: 'http://localhost:3100/api', method: 'PUT', body: requestBody, }); }); }); describe('PATCH requests', () => { it('should make a PATCH request with body', async () => { const mockResponse = { patched: true }; const requestBody = { field: 'value' }; vi.mocked($fetch).mockResolvedValueOnce(mockResponse); const api = useApi(); const result = api.patch( '/test/1', {}, true, requestBody, ); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse)); expect($fetch).toHaveBeenCalledWith('/test/1', { baseURL: 'http://localhost:3100/api', method: 'PATCH', body: requestBody, }); }); }); describe('Error handling', () => { it('should handle fetch errors with ApiError response', async () => { const apiError: ApiError = { message: 'backend.errors.not_found', success: false, }; const fetchError: Partial = { message: 'Fetch Error', response: { _data: apiError, } as any, }; vi.mocked($fetch).mockRejectedValueOnce(fetchError); const api = useApi(); const result = api.get('/test'); await vi.waitFor(() => expect(result.error.value).not.toBeNull()); expect(result.data.value).toBeNull(); expect(result.error.value).toEqual(apiError); expect(result.loading.value).toBe(false); }); it('should handle fetch errors without ApiError response', async () => { const fetchError: Partial = { message: 'Network Error', response: undefined, }; vi.mocked($fetch).mockRejectedValueOnce(fetchError); const api = useApi(); const result = api.get('/test'); await vi.waitFor(() => expect(result.error.value).not.toBeNull()); expect(result.error.value).toEqual({ message: 'Network Error', success: false, }); }); it('should use default error message when fetch error has no message', async () => { const fetchError: Partial = { message: '', }; vi.mocked($fetch).mockRejectedValueOnce(fetchError); const api = useApi(); const result = api.get('/test'); await vi.waitFor(() => expect(result.error.value).not.toBeNull()); expect(result.error.value).toEqual({ message: 'backend.errors.unknown', success: false, }); }); it('should clear previous errors on new request', async () => { const fetchError: Partial = { message: 'First Error', }; const mockData = { success: true }; // First request fails vi.mocked($fetch).mockRejectedValueOnce(fetchError); const api = useApi(); const result = api.get('/test', {}, false); await result.run(); await vi.waitFor(() => expect(result.error.value).not.toBeNull()); expect(result.error.value?.message).toBe('First Error'); // Second request succeeds vi.mocked($fetch).mockResolvedValueOnce(mockData); await result.run(); await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData)); expect(result.error.value).toBeNull(); }); }); describe('Loading state', () => { it('should set loading to true during request', async () => { let resolvePromise: (value: any) => void; const promise = new Promise((resolve) => { resolvePromise = resolve; }); vi.mocked($fetch).mockReturnValueOnce(promise as any); const api = useApi(); const result = api.get('/test', {}, false); expect(result.loading.value).toBe(false); const runPromise = result.run(); // Should be loading await nextTick(); expect(result.loading.value).toBe(true); // Resolve the request resolvePromise!({ done: true }); await runPromise; expect(result.loading.value).toBe(false); }); it('should set loading to false after error', async () => { let rejectPromise: (error: any) => void; const promise = new Promise((_, reject) => { rejectPromise = reject; }); vi.mocked($fetch).mockReturnValueOnce(promise as any); const api = useApi(); const result = api.get('/test', {}, false); const runPromise = result.run(); await nextTick(); expect(result.loading.value).toBe(true); rejectPromise!({ message: 'Error' }); await runPromise; expect(result.loading.value).toBe(false); }); }); describe('Return type structure', () => { it('should return QueryResult with correct structure', async () => { vi.mocked($fetch).mockResolvedValueOnce({ test: 'data' }); const api = useApi(); const result = api.get('/test', {}, false); expect(result).toHaveProperty('data'); expect(result).toHaveProperty('error'); expect(result).toHaveProperty('loading'); expect(result).toHaveProperty('run'); expect(typeof result.run).toBe('function'); }); }); });