diff --git a/package.json b/package.json index bfbd82a..3260832 100644 --- a/package.json +++ b/package.json @@ -27,19 +27,19 @@ "use-promise" ], "engines": { - "node": ">=8", - "npm": ">=5" + "node": ">=20" }, "main": "dist/index.js", "module": "dist/react-async-hook.esm.js", "typings": "dist/index.d.ts", + "types": "dist/index.d.ts", "files": [ "dist" ], "scripts": { "start": "tsdx watch", "build": "tsdx build", - "test": "tsdx test --env=jsdom" + "test": "tsdx test" }, "peerDependencies": { "react": ">=16.8" @@ -69,17 +69,18 @@ "@testing-library/jest-dom": "^4.1.2", "@testing-library/react": "^9.3.0", "@testing-library/react-hooks": "^3.1.0", - "@types/jest": "^24.0.12", "@types/react": "^16.9.9", "@types/react-dom": "^16.9.2", + "bunchee": "^7.0.0", "husky": "^2.2.0", + "jsdom": "^30.0.1", "prettier": "^1.17.0", "pretty-quick": "^1.10.0", "react": "^16.10.2", "react-dom": "^16.10.2", "react-test-renderer": "^16.10.2", - "tsdx": "^0.7.2", - "tslib": "^1.9.3", - "typescript": "^3.4.5" + "tsdx": "^2.0.0", + "typescript": "^6.0.2", + "vitest": "^4.1.10" } } diff --git a/test/useAsync.test.ts b/test/useAsync.test.ts index 7e163c3..65a8bec 100644 --- a/test/useAsync.test.ts +++ b/test/useAsync.test.ts @@ -1,5 +1,6 @@ -import { useAsync } from '../src'; +import { useAsync, useAsyncAbortable } from '../src'; import { renderHook } from '@testing-library/react-hooks'; +import { describe, expect, it, vi } from 'vitest'; const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -30,8 +31,8 @@ describe('useAync', () => { }); it('should resolve a successful resolved promise', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate } = renderHook(() => useAsync( @@ -58,8 +59,8 @@ describe('useAync', () => { }); it('should resolve a successful real-world request + handle params update', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate, rerender } = renderHook( ({ pageSize }: { pageSize: number }) => @@ -114,8 +115,8 @@ describe('useAync', () => { }); it('should resolve a successful real-world requests with potential race conditions', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate, rerender } = renderHook( ({ pageSize, delay }: { pageSize: number; delay: number }) => @@ -165,10 +166,10 @@ describe('useAync', () => { // This test ensures better testability of user code // See https://github.com/slorber/react-async-hook/issues/24 it('should resolve a successful Jest mocked resolved value', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); - const asyncFunction = jest.fn().mockResolvedValue(fakeResults); + const asyncFunction = vi.fn().mockResolvedValue(fakeResults); const { result, waitForNextUpdate } = renderHook(() => useAsync(asyncFunction, [], { @@ -190,8 +191,8 @@ describe('useAync', () => { // TODO legacy: should we remove this behavior? it('should resolve a successful synchronous request', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate } = renderHook(() => useAsync( @@ -217,8 +218,8 @@ describe('useAync', () => { }); it('should set error detail for unsuccessful request', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate } = renderHook(() => useAsync( @@ -244,8 +245,8 @@ describe('useAync', () => { }); it('should set error detail for error thrown synchronously (like when preparing/formatting a payload)', async () => { - const onSuccess = jest.fn(); - const onError = jest.fn(); + const onSuccess = vi.fn(); + const onError = vi.fn(); const { result, waitForNextUpdate } = renderHook(() => useAsync( @@ -270,3 +271,81 @@ describe('useAync', () => { expect(onError).toHaveBeenCalled(); }); }); + +describe('useAsyncAbortable', () => { + const fakeResults = generateFakeResults(); + + it('should resolve and pass a non-aborted signal', async () => { + let receivedSignal: AbortSignal | undefined; + + const { result, waitForNextUpdate } = renderHook(() => + useAsyncAbortable(async signal => { + receivedSignal = signal; + return fakeResults; + }, []) + ); + + await waitForNextUpdate(); + + expect(result.current.result).toEqual(fakeResults); + expect(result.current.error).toBeUndefined(); + expect(receivedSignal!.aborted).toBe(false); + }); + + it('should abort the previous call when params change', async () => { + const signals: AbortSignal[] = []; + + const { waitForNextUpdate, rerender } = renderHook( + ({ query }) => + useAsyncAbortable( + async signal => { + signals.push(signal); + await sleep(50); + return query; + }, + [query] + ), + { initialProps: { query: 'first' } } + ); + + rerender({ query: 'second' }); + + // the first signal is aborted synchronously, before the second call resolves + expect(signals).toHaveLength(2); + expect(signals[0].aborted).toBe(true); + expect(signals[1].aborted).toBe(false); + + await waitForNextUpdate(); + + expect(signals[1].aborted).toBe(false); + }); + + it('should set error detail for error thrown synchronously', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAsyncAbortable(() => { + throw new Error('something went wrong'); + }, []) + ); + + await waitForNextUpdate(); + + expect(result.current.error).toBeDefined(); + expect(result.current.error!.message).toBe('something went wrong'); + expect(result.current.result).toBeUndefined(); + }); + + it('should resolve a function that does not return a promise', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAsyncAbortable( + // @ts-ignore: not allowed by TS on purpose, but still allowed at runtime + () => fakeResults, + [] + ) + ); + + await waitForNextUpdate(); + + expect(result.current.result).toEqual(fakeResults); + expect(result.current.error).toBeUndefined(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 8e5b21c..793e4ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,9 @@ "include": ["src", "types"], "exclude": ["test"], "compilerOptions": { - "target": "es5", - "module": "esnext", + "target": "ES2022", + "module": "ESNext", "lib": ["dom", "esnext"], - "importHelpers": true, "declaration": true, "sourceMap": true, "rootDir": "./", @@ -20,7 +19,7 @@ "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "baseUrl": "./", "paths": { "*": ["src/*", "node_modules/*"] diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..5270893 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +});