From 84ea99c21932daf17962c2ca7e885e89f874c9f7 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 22:15:34 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Add=20unit=20tests=20for=20useAsync?= =?UTF-8?q?State=20composable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/composables/useAsyncState.test.js | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 code/websites/pokedex.online/tests/unit/composables/useAsyncState.test.js diff --git a/code/websites/pokedex.online/tests/unit/composables/useAsyncState.test.js b/code/websites/pokedex.online/tests/unit/composables/useAsyncState.test.js new file mode 100644 index 0000000..d29c4c9 --- /dev/null +++ b/code/websites/pokedex.online/tests/unit/composables/useAsyncState.test.js @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useAsyncState } from '@/composables/useAsyncState'; + +describe('useAsyncState', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('initial state', () => { + it('should have correct initial values', () => { + const { loading, error, data, isSuccess, isError, isIdle } = useAsyncState(); + + expect(loading.value).toBe(false); + expect(error.value).toBe(null); + expect(data.value).toBe(null); + expect(isSuccess.value).toBe(false); + expect(isError.value).toBe(false); + expect(isIdle.value).toBe(true); + }); + + it('should use provided initial data', () => { + const initialData = { test: 'data' }; + const { data } = useAsyncState({ initialData }); + + expect(data.value).toEqual(initialData); + }); + }); + + describe('execute', () => { + it('should execute async function and update data', async () => { + const { execute, loading, data, isSuccess } = useAsyncState(); + const mockData = { result: 'success' }; + const asyncFn = vi.fn().mockResolvedValue(mockData); + + expect(loading.value).toBe(false); + + const promise = execute(asyncFn); + expect(loading.value).toBe(true); + + const result = await promise; + + expect(loading.value).toBe(false); + expect(data.value).toEqual(mockData); + expect(result).toEqual(mockData); + expect(isSuccess.value).toBe(true); + expect(asyncFn).toHaveBeenCalledTimes(1); + }); + + it('should handle errors and update error state', async () => { + const { execute, loading, error, isError } = useAsyncState(); + const mockError = new Error('Test error'); + const asyncFn = vi.fn().mockRejectedValue(mockError); + + await expect(execute(asyncFn)).rejects.toThrow('Test error'); + + expect(loading.value).toBe(false); + expect(error.value).toBe(mockError); + expect(isError.value).toBe(true); + }); + + it('should call onSuccess callback', async () => { + const onSuccess = vi.fn(); + const { execute } = useAsyncState({ onSuccess }); + const mockData = { result: 'success' }; + + await execute(async () => mockData); + + expect(onSuccess).toHaveBeenCalledWith(mockData); + expect(onSuccess).toHaveBeenCalledTimes(1); + }); + + it('should call onError callback', async () => { + const onError = vi.fn(); + const { execute } = useAsyncState({ onError }); + const mockError = new Error('Test error'); + + await expect(execute(async () => { + throw mockError; + })).rejects.toThrow(); + + expect(onError).toHaveBeenCalledWith(mockError); + expect(onError).toHaveBeenCalledTimes(1); + }); + }); + + describe('retry logic', () => { + it('should retry failed operations', async () => { + const { execute } = useAsyncState({ maxRetries: 2, retryDelay: 10 }); + const asyncFn = vi.fn() + .mockRejectedValueOnce(new Error('First failure')) + .mockRejectedValueOnce(new Error('Second failure')) + .mockResolvedValueOnce('Success'); + + const result = await execute(asyncFn); + + expect(result).toBe('Success'); + expect(asyncFn).toHaveBeenCalledTimes(3); // Initial + 2 retries + }); + + it('should fail after max retries exhausted', async () => { + const { execute } = useAsyncState({ maxRetries: 2, retryDelay: 10 }); + const asyncFn = vi.fn().mockRejectedValue(new Error('Persistent failure')); + + await expect(execute(asyncFn)).rejects.toThrow('Persistent failure'); + + expect(asyncFn).toHaveBeenCalledTimes(3); // Initial + 2 retries + }); + + it('should respect custom retry count per execution', async () => { + const { execute } = useAsyncState({ retryDelay: 10 }); + const asyncFn = vi.fn() + .mockRejectedValueOnce(new Error('First')) + .mockResolvedValueOnce('Success'); + + const result = await execute(asyncFn, { retries: 1 }); + + expect(result).toBe('Success'); + expect(asyncFn).toHaveBeenCalledTimes(2); + }); + }); + + describe('cancel', () => { + it('should cancel ongoing operation', async () => { + const { execute, cancel, loading } = useAsyncState(); + const asyncFn = vi.fn(async (signal) => { + await new Promise(resolve => setTimeout(resolve, 100)); + if (signal?.aborted) throw new Error('Aborted'); + return 'Success'; + }); + + const promise = execute(asyncFn); + expect(loading.value).toBe(true); + + cancel(); + + expect(loading.value).toBe(false); + await expect(promise).rejects.toThrow(); + }); + }); + + describe('reset', () => { + it('should reset all state to initial values', async () => { + const initialData = 'initial'; + const { execute, reset, data, error, loading } = useAsyncState({ initialData }); + + await execute(async () => 'new data'); + expect(data.value).toBe('new data'); + + reset(); + + expect(data.value).toBe(initialData); + expect(error.value).toBe(null); + expect(loading.value).toBe(false); + }); + + it('should cancel ongoing operation when reset', async () => { + const { execute, reset, loading } = useAsyncState(); + const asyncFn = vi.fn(async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + return 'Success'; + }); + + const promise = execute(asyncFn); + expect(loading.value).toBe(true); + + reset(); + + expect(loading.value).toBe(false); + await expect(promise).rejects.toThrow(); + }); + }); +});