Skip to content

Commit bbb69e9

Browse files
authored
feat: FixtureEndpoint & renderRestHook resolverFixtures (#1027)
1 parent 2a2fe3a commit bbb69e9

File tree

11 files changed

+229
-83
lines changed

11 files changed

+229
-83
lines changed

__tests__/new.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export class FutureArticleResource extends CoolerArticleResource {
249249
fetch(body: Partial<AbstractInstanceType<T>>) {
250250
return instanceFetch(this.url(), this.getFetchInit(body));
251251
},
252-
url: this.listUrl.bind(this),
252+
url: () => this.listUrl({}),
253253
update: (newid: string) => ({
254254
[this.list().key({})]: (existing: string[] = []) => [
255255
newid,

docs/api/MockResolver.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ title: <MockResolver />
55
```typescript
66
function MockResolver({
77
children,
8-
results,
8+
fixtures,
99
}: {
1010
children: React.ReactNode;
11-
results: Fixture[];
11+
fixtures: Fixture[];
1212
}): JSX.Element;
1313
```
1414

@@ -21,21 +21,44 @@ This is useful for [storybook](../guides/storybook.md) as well as component test
2121
### fixtures
2222

2323
```typescript
24+
export interface SuccessFixtureEndpoint<
25+
E extends EndpointInterface = EndpointInterface,
26+
> {
27+
endpoint: E;
28+
args: Parameters<E>;
29+
response: ResolveType<E>;
30+
error?: false;
31+
}
32+
33+
/** @deprecated */
2434
export interface SuccessFixture {
25-
request: ReadShape<Schema, object>;
26-
params: object;
35+
request: FetchShape<Schema, any>;
36+
params?: any;
37+
body?: any;
2738
result: object | string | number;
2839
error?: false;
2940
}
3041

42+
export interface ErrorFixtureEndpoint<
43+
E extends EndpointInterface = EndpointInterface,
44+
> {
45+
endpoint: E;
46+
args: Parameters<E>;
47+
response: any;
48+
error: true;
49+
}
50+
51+
/** @deprecated */
3152
export interface ErrorFixture {
32-
request: ReadShape<Schema, object>;
33-
params: object;
53+
request: FetchShape<Schema, any>;
54+
params?: any;
55+
body?: any;
3456
result: Error;
3557
error: true;
3658
}
3759

38-
export type Fixture = SuccessFixture | ErrorFixture;
60+
export type FixtureEndpoint = SuccessFixtureEndpoint | ErrorFixtureEndpoint;
61+
export type Fixture = SuccessFixture | ErrorFixture | FixtureEndpoint;
3962
```
4063

4164
This prop specifies the fixtures to use data from. Each item represents a fetch defined by the
@@ -59,9 +82,9 @@ import MyComponentToTest from 'components/MyComponentToTest';
5982

6083
const results = [
6184
{
62-
request: ArticleResource.list(),
63-
params: { maxResults: 10 },
64-
result: [
85+
endpoint: ArticleResource.list(),
86+
args: [{ maxResults: 10 }] as const,
87+
response: [
6588
{
6689
id: 5,
6790
content: 'have a merry christmas',

docs/api/makeRenderRestHook.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ type RenderRestHookFunction = {
3939
callback: (props: P) => R,
4040
options?: {
4141
initialProps?: P;
42-
results?: Fixture[];
42+
initialFixtures?: FixtureEndpoint[];
43+
resolverFixtures?: FixtureEndpoint[];
4344
wrapper?: React.ComponentType;
45+
// @deprecated
46+
results?: Fixture[];
4447
},
4548
): {
4649
readonly result: {
@@ -64,13 +67,19 @@ to `renderRestHook()` will result in a completely fresh cache state as well as m
6467

6568
Hooks to run inside React. Return value will become available in `result`
6669

67-
#### options.results
70+
#### options.initialFixtures
6871

6972
Can be used to prime the cache if test expects cache values to already be filled. Takes same
7073
[array of fixtures as MockResolver](../api/MockResolver#fixtures)
7174

7275
This has the same effect as initializing [<CacheProvider />](../api/CacheProvider) with [mockInitialState()](../api/mockInitialState)
7376

77+
### options.resolverFixtures
78+
79+
These fixtures are used to resolve any new requests. This is most useful for mocking imperative fetches like mutations, but can also allow testing suspending states or transitions.
80+
81+
Wrrks by adding [MockResolver](../api/MockResolver) as a wrapper.
82+
7483
#### options.initialProps
7584

7685
The initial values to pass to the callback function

docs/guides/storybook.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ existing so loading fallback is shown.
5959
export default {
6060
full: [
6161
{
62-
request: ArticleResource.list(),
63-
params: { maxResults: 10 },
64-
result: [
62+
endpoint: ArticleResource.list(),
63+
args: [{ maxResults: 10 }] as const,
64+
response: [
6565
{
6666
id: 5,
6767
content: 'have a merry christmas',
@@ -77,9 +77,9 @@ export default {
7777
],
7878
},
7979
{
80-
request: ArticleResource.update(),
81-
params: { id: 532 },
82-
result: {
80+
endpoint: ArticleResource.update(),
81+
args: [{ id: 532 }] as const,
82+
response: {
8383
id: 532,
8484
content: 'updated "never again"',
8585
author: 23,
@@ -89,16 +89,16 @@ export default {
8989
],
9090
empty: [
9191
{
92-
request: ArticleResource.list(),
93-
params: { maxResults: 10 },
94-
result: [],
92+
endpoint: ArticleResource.list(),
93+
args: [{ maxResults: 10 }] as const,
94+
response: [],
9595
},
9696
],
9797
error: [
9898
{
99-
request: ArticleResource.list(),
100-
params: { maxResults: 10 },
101-
result: { message: 'Bad request', status: 400, name: 'Not Found' },
99+
endpoint: ArticleResource.list(),
100+
args: [{ maxResults: 10 }] as const,
101+
response: { message: 'Bad request', status: 400, name: 'Not Found' },
102102
error: true,
103103
},
104104
],

docs/guides/unit-testing-components.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ with said results.
1414
export default {
1515
full: [
1616
{
17-
request: ArticleResource.list(),
18-
params: { maxResults: 10 },
19-
result: [
17+
endpoint: ArticleResource.list(),
18+
args: [{ maxResults: 10 }] as const,
19+
response: [
2020
{
2121
id: 5,
2222
content: 'have a merry christmas',
@@ -32,9 +32,9 @@ export default {
3232
],
3333
},
3434
{
35-
request: ArticleResource.update(),
36-
params: { id: 532 },
37-
result: {
35+
endpoint: ArticleResource.update(),
36+
args: [{ id: 532 }] as const,
37+
response: {
3838
id: 532,
3939
content: 'updated "never again"',
4040
author: 23,
@@ -44,16 +44,16 @@ export default {
4444
],
4545
empty: [
4646
{
47-
request: ArticleResource.list(),
48-
params: { maxResults: 10 },
49-
result: [],
47+
endpoint: ArticleResource.list(),
48+
args: [{ maxResults: 10 }] as const,
49+
response: [],
5050
},
5151
],
5252
error: [
5353
{
54-
request: ArticleResource.list(),
55-
params: { maxResults: 10 },
56-
result: { message: 'Bad request', status: 400, name: 'Not Found' },
54+
endpoint: ArticleResource.list(),
55+
args: [{ maxResults: 10 }] as const,
56+
response: { message: 'Bad request', status: 400, name: 'Not Found' },
5757
error: true,
5858
},
5959
],

packages/core/src/react-integration/__tests__/useMeta-endpoint.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('useMeta()', () => {
1616
});
1717

1818
it('should contain error', () => {
19-
const results = [
19+
const results: Fixture[] = [
2020
{
2121
request: TypedArticleResource.detail().bind(undefined, payload),
2222
result: new Error('broken'),

packages/experimental/src/__tests__/useFetcher.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { FutureArticleResource } from '__tests__/new';
22
import nock from 'nock';
3-
import { useCache, useResource } from '@rest-hooks/core';
3+
import { useCache, useResource, ResolveType } from '@rest-hooks/core';
44

55
// relative imports to avoid circular dependency in tsconfig references
66
import {
77
makeRenderRestHook,
88
makeCacheProvider,
99
makeExternalCacheProvider,
10+
FixtureEndpoint,
1011
} from '../../../test';
1112
import useFetcher from '../useFetcher';
1213

@@ -132,16 +133,31 @@ for (const makeProvider of [makeCacheProvider, makeExternalCacheProvider]) {
132133
});
133134

134135
it('should update on create', async () => {
135-
const { result, waitForNextUpdate } = renderRestHook(() => {
136-
const articles = useResource(FutureArticleResource.list(), {});
137-
const fetch = useFetcher();
138-
return { articles, fetch };
139-
});
140-
await waitForNextUpdate();
141-
await result.current.fetch(FutureArticleResource.create(), {
142-
id: 1,
143-
});
144-
expect(result.current.articles.map(({ id }) => id)).toEqual([1, 5, 3]);
136+
const endpoint = FutureArticleResource.create();
137+
const response: ResolveType<typeof endpoint> = createPayload;
138+
const fixtures: FixtureEndpoint[] = [
139+
{
140+
endpoint,
141+
args: [{ id: 1 }],
142+
response,
143+
},
144+
];
145+
// use nock, and use resolver
146+
for (const resolverFixtures of [undefined, fixtures]) {
147+
const { result, waitForNextUpdate } = renderRestHook(
148+
() => {
149+
const articles = useResource(FutureArticleResource.list(), {});
150+
const fetch = useFetcher();
151+
return { articles, fetch };
152+
},
153+
{ resolverFixtures },
154+
);
155+
await waitForNextUpdate();
156+
await result.current.fetch(FutureArticleResource.create(), {
157+
id: 1,
158+
});
159+
expect(result.current.articles.map(({ id }) => id)).toEqual([1, 5, 3]);
160+
}
145161
});
146162
});
147163
}

packages/test/src/MockResolver.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Fixture, actionFromFixture } from '@rest-hooks/test/mockState';
1111
type Props = {
1212
children: React.ReactNode;
1313
fixtures: Fixture[];
14+
silenceMissing: boolean;
1415
};
1516

1617
/** Can be used to mock responses based on fixtures provided.
@@ -19,7 +20,11 @@ type Props = {
1920
*
2021
* Place below <CacheProvider /> and above any components you want to mock.
2122
*/
22-
export default function MockResolver({ children, fixtures }: Props) {
23+
export default function MockResolver({
24+
children,
25+
fixtures,
26+
silenceMissing,
27+
}: Props) {
2328
const fetchToReceiveAction = useMemo(() => {
2429
const actionMap: Record<string, ReceiveAction> = {};
2530
for (const fixture of fixtures) {
@@ -49,9 +54,10 @@ export default function MockResolver({ children, fixtures }: Props) {
4954
}, 0);
5055
return Promise.resolve();
5156
}
52-
// This is only a warn because sometimes this is intentional
53-
console.warn(
54-
`<MockResolver/> received a dispatch:
57+
if (!silenceMissing) {
58+
// This is only a warn because sometimes this is intentional
59+
console.warn(
60+
`<MockResolver/> received a dispatch:
5561
${JSON.stringify(action, undefined, 2)}
5662
for which there is no matching fixture.
5763
@@ -67,7 +73,8 @@ export default function MockResolver({ children, fixtures }: Props) {
6773
params: { maxResults: 10 },
6874
result: [],
6975
}`,
70-
);
76+
);
77+
}
7178
} else if (action.type === actionTypes.SUBSCRIBE_TYPE) {
7279
const { key } = action.meta;
7380
if (Object.prototype.hasOwnProperty.call(fetchToReceiveAction, key)) {
@@ -76,7 +83,7 @@ export default function MockResolver({ children, fixtures }: Props) {
7683
}
7784
return dispatch(action);
7885
},
79-
[fetchToReceiveAction, dispatch],
86+
[dispatch, fetchToReceiveAction, silenceMissing],
8087
);
8188

8289
return (
@@ -85,3 +92,7 @@ export default function MockResolver({ children, fixtures }: Props) {
8592
</DispatchContext.Provider>
8693
);
8794
}
95+
96+
MockResolver.defaultProps = {
97+
silenceMissing: false,
98+
};

packages/test/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import mockInitialState from '@rest-hooks/test/mockState';
88
export * from '@rest-hooks/test/managers';
99
export { default as MockResolver } from '@rest-hooks/test/MockResolver';
1010
export type {
11+
FixtureEndpoint,
12+
SuccessFixtureEndpoint,
13+
ErrorFixtureEndpoint,
1114
Fixture,
1215
SuccessFixture,
1316
ErrorFixture,

0 commit comments

Comments
 (0)