From 2b62bbdcd8e9a116f117909646a900971e6236f1 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 30 Oct 2024 11:43:57 +0100 Subject: [PATCH 1/4] update AsyncView to work with swr's loading flag and support nullable data payloads --- src/async-data/AsyncView.tsx | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/async-data/AsyncView.tsx b/src/async-data/AsyncView.tsx index 2465666..0673811 100644 --- a/src/async-data/AsyncView.tsx +++ b/src/async-data/AsyncView.tsx @@ -2,16 +2,25 @@ import React, { ReactElement, ReactNode } from 'react' import { isFunction } from '../util' type LoadingFunction = () => ReactNode -type SuccessFunction = (data: NonNullable) => ReactNode +type SuccessFunction = (data: Data) => ReactNode type ErrorFunction = (error: NonNullable) => ReactNode type Props = { data?: Data error?: Error + isLoading: boolean renderLoading?: ReactNode | LoadingFunction - renderSuccess: ReactNode | SuccessFunction renderError?: ReactNode | ErrorFunction -} +} & ( + | { + allowMissingData: true + renderSuccess: ReactNode | SuccessFunction + } + | { + allowMissingData?: false + renderSuccess: ReactNode | SuccessFunction> + } +) const AsyncView = ( props: Props, @@ -20,19 +29,34 @@ const AsyncView = ( const { data, error, + isLoading, renderLoading = null, renderSuccess, renderError = null, + allowMissingData = false, } = props + + if (isLoading) { + return <>{isFunction(renderLoading) ? renderLoading() : renderLoading} + } + if (error !== null && error !== undefined) { return <>{isFunction(renderError) ? renderError(error) : renderError} - } else if (data !== null && data !== undefined) { - return ( - <>{isFunction(renderSuccess) ? renderSuccess(data) : renderSuccess} + } + + if ((data === undefined || data === null) && !allowMissingData) { + throw new Error( + 'Data passed into AsyncView was null or undefined. Use allowMissingData=true if this is intended.', ) - } else { - return <>{isFunction(renderLoading) ? renderLoading() : renderLoading} } + + return ( + <> + {isFunction(renderSuccess) + ? renderSuccess(data as NonNullable) + : renderSuccess} + + ) } export { AsyncView } From 3befdbd4bf47a498f2a0ed3bc00cadfeeb45de38 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 30 Oct 2024 14:06:30 +0100 Subject: [PATCH 2/4] enable tsconfig for tests --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 5a8dc5a..341f0e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,6 @@ "moduleResolution": "node", "skipLibCheck": true }, - "exclude": ["node_modules", "dist", "**/__test__/*", "**/__tests__/*"], + "exclude": ["node_modules", "dist"], "include": ["**/*.ts", "**/*.tsx", "**/*.js"] } From 21305b3a2e65b1fe2969103774de2176e40305a8 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 30 Oct 2024 14:06:49 +0100 Subject: [PATCH 3/4] add additional tests for AsyncView --- src/async-data/__test__/AsyncView.test.tsx | 56 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/async-data/__test__/AsyncView.test.tsx b/src/async-data/__test__/AsyncView.test.tsx index 6abe4e8..e3ba6af 100644 --- a/src/async-data/__test__/AsyncView.test.tsx +++ b/src/async-data/__test__/AsyncView.test.tsx @@ -4,7 +4,7 @@ import { AsyncView } from '../AsyncView' type Data = { greeting: string -} +} | null type Error = { message: string @@ -15,36 +15,76 @@ const Loading: React.FC = () => { } const Success: React.FC<{ data: Data }> = ({ data }) => { - return <>{data.greeting} + return <>{data?.greeting} } const Error: React.FC<{ error: Error }> = ({ error }) => { return <>{error.message} } -function renderAsyncView(data?: Data, error?: Error): RenderResult { +type RenderAsyncViewProps = { + isLoading: boolean + error?: Error + data?: Data + allowMissingData?: boolean +} + +function renderAsyncView({ + isLoading, + data, + error, + allowMissingData = false, +}: RenderAsyncViewProps): RenderResult { return render( - + } - renderSuccess={(data) => } + renderSuccess={(data: unknown) => } renderError={(error) => } />, ) } test('should render loading if asyncState is loading for the first time', function () { - const { getByText } = renderAsyncView() + const { getByText } = renderAsyncView({ isLoading: true }) expect(getByText(/loading/i)).toBeInTheDocument() }) test('should render success if asyncState is successful', function () { - const { getByText } = renderAsyncView({ greeting: 'Hello' }) + const { getByText } = renderAsyncView({ + isLoading: false, + data: { greeting: 'Hello' }, + }) expect(getByText(/hello/i)).toBeInTheDocument() }) +test.each([null, undefined])( + 'should throw asyncState is successful but data is missing', + function (value) { + expect(renderAsyncView({ isLoading: false, data: value })).toThrow() + }, +) + +test.each([null, undefined])( + 'should render success if asyncState is successful but data is missing but allowed', + function (value) { + expect( + renderAsyncView({ + isLoading: false, + data: value, + allowMissingData: true, + }), + ).not.toThrow() + }, +) + test('should render error if asyncState is error', function () { - const { getByText } = renderAsyncView(undefined, { message: 'Error' }) + const { getByText } = renderAsyncView({ + isLoading: false, + error: { message: 'Error' }, + }) expect(getByText(/error/i)).toBeInTheDocument() }) From 507f4a18745388517f44d7c3e3697403c18cbdff Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 30 Oct 2024 14:09:54 +0100 Subject: [PATCH 4/4] fix tests --- src/async-data/__test__/AsyncView.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-data/__test__/AsyncView.test.tsx b/src/async-data/__test__/AsyncView.test.tsx index e3ba6af..9898692 100644 --- a/src/async-data/__test__/AsyncView.test.tsx +++ b/src/async-data/__test__/AsyncView.test.tsx @@ -64,14 +64,14 @@ test('should render success if asyncState is successful', function () { test.each([null, undefined])( 'should throw asyncState is successful but data is missing', function (value) { - expect(renderAsyncView({ isLoading: false, data: value })).toThrow() + expect(() => renderAsyncView({ isLoading: false, data: value })).toThrow() }, ) test.each([null, undefined])( 'should render success if asyncState is successful but data is missing but allowed', function (value) { - expect( + expect(() => renderAsyncView({ isLoading: false, data: value,