Skip to content

Commit bb956a4

Browse files
committed
fix: improve readme regarding type safety
Also add local implementation fo described helper and migrate to it where applicable.
1 parent 3c72221 commit bb956a4

File tree

6 files changed

+69
-57
lines changed

6 files changed

+69
-57
lines changed

README.md

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,22 @@ regarding the testing environment configuration. Since you're using this library
9494
testing environment, making this error redundant.
9595

9696
```ts filename="useState.test.ts"
97-
import {expect, test} from 'vitest';
9897
import {act, renderHook} from '@ver0/react-hooks-testing';
98+
import {useState} from 'react';
99+
import {expect, test} from 'vitest';
100+
import {expectResultValue} from './test-helpers.test.js';
99101

100102
test('should use setState value', async () => {
101103
const {result} = await renderHook(() => useState('foo'));
102104

103-
expect(result.value).toBe('foo');
104-
expect(result.error).toBe(undefined);
105-
106-
if (result.value === undefined) {
107-
return;
108-
}
105+
const value = expectResultValue(result);
106+
expect(value[0]).toBe('foo');
109107

110108
await act(async () => {
111-
result.value[1]('bar');
109+
value[1]('bar');
112110
});
113111

114-
expect(result.value[0]).toBe('bar');
112+
expect(expectResultValue(result)[0]).toBe('bar');
115113
});
116114
```
117115

@@ -150,13 +148,44 @@ each render.
150148
Another notable difference is the `error` property. This property captures any error thrown during the hook’s rendering,
151149
thanks to an Error Boundary component wrapped around the hook’s harness component.
152150

153-
Each result object is immutable and contains either a `value` or an `error` property—but never both. The hook’s result
154-
object follows the same principle. Although the `value` and `error` properties are implemented as getters and always
155-
exist, the values they return correspond to the most recent render result from the `all` array.
151+
Each result object is immutable and has either a `value` or an `error` property set. The hook's result object follows
152+
the same principle. Although the `value` and `error` properties are implemented as getters and always exist, the values
153+
they return correspond to the most recent render result from the `all` array.
154+
155+
> Though it is possible for both values to become `undefined` simultaneously, when a hook returns `undefined` as a valid
156+
> value, it is developer's responsibility to check error prior to using the value. The other theoretical possibility is
157+
> `undefined` being thrown, but that is considered a bad practice and thus not handled specifically.
156158
157159
Otherwise, the API is similar to `@testing-library/react`. Note that the `waitForNextUpdate` function is not provided,
158160
as modern testing frameworks include their own `waitFor` function, which serves the same purpose.
159161

162+
**Type-Safety Helper**
163+
164+
While the discriminated union type provides strong type safety, manually checking for errors in every test can be
165+
annoyingly verbose. To ease usage while ensuring both type-safety and test coverage, you can introduce a helper
166+
function:
167+
168+
```ts filename="test-helpers.test.ts"
169+
import type {ResultValue} from '@ver0/react-hooks-testing';
170+
import {expect} from 'vitest';
171+
172+
/**
173+
* Helper to assert that a hook result is successful and extract its value in a type-safe way.
174+
*/
175+
export function expectResultValue<T>(result: ResultValue<T>) {
176+
expect(result.error).toBeUndefined();
177+
178+
if (result.error) {
179+
throw new Error('result has unexpected error');
180+
}
181+
182+
return result.value;
183+
}
184+
```
185+
186+
This helper both asserts that no error occurred and narrows the TypeScript type, allowing you to work with
187+
`result.value` directly, as demonstrated in the example above.
188+
160189
#### Testing server-side hooks
161190

162191
The primary purpose of this package is to provide out-of-the-box SSR (Server-Side Rendering) support through the

src/create-hook-renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function newResults<T>() {
5858
results.push(Object.freeze({value, error: undefined}));
5959
},
6060
setError(error: Error) {
61-
results.push(Object.freeze({value: undefined, error}));
61+
results.push(Object.freeze({error, value: undefined}));
6262
},
6363
};
6464
}

src/tests/result-history.ssr.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {describe, expect, test} from 'vitest';
22
import {renderHookServer} from '../index.js';
3+
import {expectResultValue} from './test-helpers.test.js';
34

45
describe('result history SSR', () => {
56
function useValue(value: number) {
@@ -13,7 +14,7 @@ describe('result history SSR', () => {
1314
test('should capture all renders states of hook with hydration', async () => {
1415
const {result} = await renderHookServer((value) => useValue(value), {initialProps: 0});
1516

16-
expect(result.value).toEqual(0);
17+
expect(expectResultValue(result)).toEqual(0);
1718
expect(result.all).toEqual([{value: 0}]);
1819
});
1920
});

src/tests/test-helpers.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {expect} from 'vitest';
2+
import type {ResultValue} from '../index.js';
3+
4+
/**
5+
* Helper to assert that a hook result is successful and extract its value in a type-safe way.
6+
*/
7+
export function expectResultValue<T>(result: ResultValue<T>) {
8+
expect(result.error).toBeUndefined();
9+
10+
if (result.error) {
11+
throw new Error('result has unexpected error');
12+
}
13+
14+
return result.value;
15+
}

src/tests/use-state.dom.test.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useState} from 'react';
22
import {describe, expect, it} from 'vitest';
33
import {act, renderHook, renderHookServer} from '../index.js';
4+
import {expectResultValue} from './test-helpers.test.js';
45

56
describe('useState on the client', () => {
67
it('should use setState value', async () => {
@@ -10,14 +11,7 @@ describe('useState on the client', () => {
1011
return {state, setState};
1112
});
1213

13-
expect(result.value).not.toBe(undefined);
14-
expect(result.error).toBe(undefined);
15-
16-
if (result.value === undefined) {
17-
return;
18-
}
19-
20-
expect(result.value.state).toBe('foo');
14+
expect(expectResultValue(result).state).toBe('foo');
2115
});
2216

2317
it('should update state', async () => {
@@ -27,18 +21,11 @@ describe('useState on the client', () => {
2721
return {state, setState};
2822
});
2923

30-
expect(result.value).not.toBe(undefined);
31-
expect(result.error).toBe(undefined);
32-
33-
if (result.value === undefined) {
34-
return;
35-
}
36-
3724
await act(async () => {
38-
result.value.setState('bar');
25+
expectResultValue(result).setState('bar');
3926
});
4027

41-
expect(result.value.state).toBe('bar');
28+
expect(expectResultValue(result).state).toBe('bar');
4229
});
4330
});
4431

@@ -50,25 +37,18 @@ describe('useState SSR hydrated', () => {
5037
return {state, setState};
5138
});
5239

53-
expect(result.value).not.toBe(undefined);
54-
expect(result.error).toBe(undefined);
55-
56-
if (result.value === undefined) {
57-
return;
58-
}
59-
6040
await act(async () => {
61-
result.value.setState('bar');
41+
expectResultValue(result).setState('bar');
6242
});
6343

6444
expect(result.all.length).toBe(1);
6545

6646
await hydrate();
6747

6848
await act(async () => {
69-
result.value.setState('bar');
49+
expectResultValue(result).setState('bar');
7050
});
7151

72-
expect(result.value.state).toBe('bar');
52+
expect(expectResultValue(result).state).toBe('bar');
7353
});
7454
});

src/tests/use-state.ssr.test.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useState} from 'react';
22
import {describe, expect, it} from 'vitest';
33
import {act, renderHookServer} from '../index.js';
4+
import {expectResultValue} from './test-helpers.test.js';
45

56
describe('useState SSR, non-hydrated', () => {
67
it('should use setState value', async () => {
@@ -10,14 +11,7 @@ describe('useState SSR, non-hydrated', () => {
1011
return {state, setState};
1112
});
1213

13-
expect(result.value).not.toBe(undefined);
14-
expect(result.error).toBe(undefined);
15-
16-
if (result.value === undefined) {
17-
return;
18-
}
19-
20-
expect(result.value.state).toBe('foo');
14+
expect(expectResultValue(result).state).toBe('foo');
2115
});
2216

2317
it('should not update state without hydration', async () => {
@@ -27,15 +21,8 @@ describe('useState SSR, non-hydrated', () => {
2721
return {state, setState};
2822
});
2923

30-
expect(result.value).not.toBe(undefined);
31-
expect(result.error).toBe(undefined);
32-
33-
if (result.value === undefined) {
34-
return;
35-
}
36-
3724
await act(async () => {
38-
result.value.setState('bar');
25+
expectResultValue(result).setState('bar');
3926
});
4027

4128
expect(result.all.length).toBe(1);

0 commit comments

Comments
 (0)