Skip to content

Commit 4c1d5c9

Browse files
committed
Merge branch 'master' into beta
2 parents f93c816 + d6181b3 commit 4c1d5c9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3131
-1008
lines changed

.changeset/chilly-towns-run.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@data-client/vue': minor
3+
---
4+
5+
Never wrap renderDataCompose().result in ref. Just passthrough the return value directly. Always.
6+
7+
### Before
8+
```ts
9+
const { result, cleanup } = await renderDataCompose(() =>
10+
useSuspense(CoolerArticleResource.get, { id: payload.id }),
11+
);
12+
13+
const articleRef = await result.value;
14+
expect(articleRef.value.title).toBe(payload.title);
15+
expect(articleRef.value.content).toBe(payload.content);
16+
```
17+
18+
### After
19+
```ts
20+
const { result, cleanup } = await renderDataCompose(() =>
21+
useSuspense(CoolerArticleResource.get, { id: payload.id }),
22+
);
23+
24+
const articleRef = await result;
25+
expect(articleRef.value.title).toBe(payload.title);
26+
expect(articleRef.value.content).toBe(payload.content);
27+
```

.changeset/clever-geckos-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@data-client/vue': patch
3+
---
4+
5+
Fixed race condition in useSuspense() where args change while initial suspense is not complete

.changeset/giant-cycles-follow.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@data-client/vue': patch
3+
---
4+
5+
renderDataClient -> renderDataCompose
6+
7+
This keeps naming conventions closer to the React version

.changeset/icy-hairs-go.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@data-client/vue': minor
3+
---
4+
5+
`renderDataCompose()` awaits until the composable runs
6+
7+
### Before
8+
9+
```ts
10+
const { result, cleanup } = renderDataCompose(() =>
11+
useCache(CoolerArticleResource.get, { id: payload.id }),
12+
);
13+
14+
// Wait for initial render
15+
await waitForNextUpdate();
16+
17+
expect(result.current).toBeDefined();
18+
```
19+
20+
### After
21+
22+
```ts
23+
const { result, cleanup } = await renderDataCompose(() =>
24+
useCache(CoolerArticleResource.get, { id: payload.id }),
25+
);
26+
27+
expect(result.value).toBeDefined();
28+
```

.changeset/kind-garlics-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@data-client/vue': minor
3+
---
4+
5+
`renderDataCompose().result` is now simply passes the composable result if it's a ref, or wraps it as computable ref

.changeset/tricky-olives-wash.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@data-client/vue': minor
3+
---
4+
5+
Add useDLE()
6+
7+
```ts
8+
const { date, loading, error } = useDLE(
9+
CoolerArticleResource.get,
10+
computed(() => (props.id !== null ? { id: props.id } : null)),
11+
);
12+
```

.circleci/config.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
- run:
4747
name: yarn install
4848
command: |
49-
yarn up @data-client/endpoint@workspace:^ @data-client/react@workspace:^ @data-client/rest@workspace:^
49+
yarn up @data-client/endpoint@workspace:^ @data-client/react@workspace:^ @data-client/rest@workspace:^ @data-client/img@workspace:^ @data-client/graphql@workspace:^
5050
- save_cache:
5151
paths:
5252
- .yarn/cache
@@ -113,6 +113,8 @@ jobs:
113113
elif [ "<< parameters.react-version >>" == "^18" ]; then
114114
yarn up react@<< parameters.react-version >> react-dom@<< parameters.react-version >> react-test-renderer@<< parameters.react-version >>
115115
yarn workspace @data-client/test add --dev @testing-library/react-hooks
116+
elif [ "<< parameters.react-version >>" == "native" ]; then
117+
116118
fi
117119
- run:
118120
name: Running Jest

.cursor/rules/test-files.mdc

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
description: Testing patterns and conventions for @data-client/test
3+
globs: '**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/__tests__/**/*.ts', '**/__tests__/**/*.tsx'
4+
alwaysApply: false
5+
---
6+
7+
# Testing Patterns (@data-client/test)
8+
9+
## Hook Testing with renderDataHook()
10+
11+
```typescript
12+
import { renderDataHook } from '@data-client/test';
13+
14+
it('useSuspense() should render the response', async () => {
15+
const { result, waitFor } = renderDataHook(
16+
() => useSuspense(ArticleResource.get, { id: 5 }),
17+
{
18+
initialFixtures: [
19+
{
20+
endpoint: ArticleResource.get,
21+
args: [{ id: 5 }],
22+
response: { id: 5, title: 'hi ho', content: 'whatever' },
23+
},
24+
],
25+
},
26+
);
27+
expect(result.current.title).toBe('hi ho');
28+
});
29+
```
30+
31+
**Options:**
32+
- `initialFixtures` - Set up initial state of the store
33+
- `resolverFixtures` - Add interceptors for subsequent requests
34+
- `getInitialInterceptorData` - Simulate changing server state
35+
36+
**Return values:**
37+
- Inherits all `renderHook()` return values from `@testing-library/react`
38+
- `controller` - Controller instance for manual actions
39+
- `cleanup()` - Cleanup function
40+
- `allSettled()` - Wait for all async operations to complete
41+
42+
## Fixtures and Interceptors
43+
44+
**Success Fixture:**
45+
```typescript
46+
interface SuccessFixture {
47+
endpoint;
48+
args;
49+
response;
50+
error?;
51+
delay?;
52+
}
53+
```
54+
55+
**Response Interceptor:**
56+
```typescript
57+
interface ResponseInterceptor {
58+
endpoint;
59+
response(...args);
60+
delay?;
61+
delayCollapse?;
62+
}
63+
```
64+
65+
## Testing Mutations
66+
67+
**Create operations:**
68+
```typescript
69+
it('should create a new todo', async () => {
70+
const { result } = renderDataHook(
71+
() => useController(),
72+
{
73+
initialFixtures: [
74+
{
75+
endpoint: TodoResource.getList,
76+
args: [],
77+
response: [],
78+
},
79+
],
80+
resolverFixtures: [
81+
{
82+
endpoint: TodoResource.getList.push,
83+
response: (newTodo) => ({ ...newTodo, id: 1 }),
84+
},
85+
],
86+
},
87+
);
88+
89+
const newTodo = { title: 'Test Todo', completed: false };
90+
const createdTodo = await result.current.fetch(TodoResource.getList.push, newTodo);
91+
92+
expect(createdTodo.id).toBe(1);
93+
});
94+
```
95+
96+
## Testing Error States
97+
98+
```typescript
99+
it('should handle fetch errors', async () => {
100+
const { result, waitFor } = renderDataHook(
101+
() => useSuspense(TodoResource.get, { id: 1 }),
102+
{
103+
initialFixtures: [
104+
{
105+
endpoint: TodoResource.get,
106+
args: [{ id: 1 }],
107+
response: null,
108+
error: new Error('Not found'),
109+
},
110+
],
111+
},
112+
);
113+
114+
await waitFor(() => {
115+
expect(result.current).toBeUndefined();
116+
});
117+
});
118+
```
119+
120+
## Testing Components
121+
122+
```typescript
123+
import { render } from '@testing-library/react';
124+
import { DataProvider } from '@data-client/react';
125+
126+
const renderWithProvider = (component, options = {}) => {
127+
return render(
128+
<DataProvider {...options}>
129+
{component}
130+
</DataProvider>
131+
);
132+
};
133+
134+
it('should render todo list', async () => {
135+
const { getByText } = renderWithProvider(
136+
<TodoList />,
137+
{
138+
initialFixtures: [
139+
{
140+
endpoint: TodoResource.getList,
141+
args: [],
142+
response: [{ id: 1, title: 'Test Todo', completed: false }],
143+
},
144+
],
145+
},
146+
);
147+
148+
expect(getByText('Test Todo')).toBeInTheDocument();
149+
});
150+
```
151+
152+
## Testing with nock (HTTP Endpoint Testing)
153+
154+
```typescript
155+
import nock from 'nock';
156+
157+
it('should fetch data from API', async () => {
158+
const scope = nock('https://jsonplaceholder.typicode.com')
159+
.get('/todos/1')
160+
.reply(200, { id: 1, title: 'Test', completed: false });
161+
162+
const result = await TodoResource.get({ id: 1 });
163+
164+
expect(result.title).toBe('Test');
165+
scope.done();
166+
});
167+
```
168+
169+
## Testing Managers
170+
171+
```typescript
172+
it('should handle manager middleware', async () => {
173+
const mockManager = {
174+
middleware: (controller) => (next) => async (action) => {
175+
if (action.type === 'FETCH') {
176+
console.log('Fetch action:', action);
177+
}
178+
return next(action);
179+
},
180+
cleanup: jest.fn(),
181+
};
182+
183+
const { controller } = renderDataHook(
184+
() => useController(),
185+
{ managers: [mockManager] },
186+
);
187+
188+
await controller.fetch(TodoResource.get, { id: 1 });
189+
expect(mockManager.cleanup).not.toHaveBeenCalled();
190+
});
191+
```
192+
193+
## Test File Organization
194+
195+
**Keep tests under `packages/*/src/**/__tests__`:**
196+
```
197+
packages/react/src/hooks/__tests__/useSuspense.test.ts
198+
packages/react/src/components/__tests__/DataProvider.test.tsx
199+
```
200+
201+
**Test naming:**
202+
- Node-only: `*.node.test.ts[x]`
203+
- React Native: `*.native.test.ts[x]`
204+
- Regular: `*.test.ts[x]`
205+
206+
## Best Practices
207+
208+
- Use `renderDataHook()` for testing hooks that use @data-client/react hooks
209+
- Use fixtures or interceptors when testing hooks or components
210+
- Use `nock` when testing networking definitions
211+
- Test both success and error scenarios
212+
- Test mutations and their side effects
213+
- Don't mock @data-client internals directly
214+
- Don't use raw fetch in tests when fixtures are available

0 commit comments

Comments
 (0)