Skip to content

Commit 2c064de

Browse files
committed
docs(skills): Improve skills
1 parent a0ca35c commit 2c064de

5 files changed

Lines changed: 384 additions & 64 deletions

File tree

.cursor/skills/data-client-schema/SKILL.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ to represent the data expected.
6565

6666
## 3. Entity lifecycle methods
6767

68-
- Normalize order: `process()``pk()`[validate()](references/validation.md)**visit nested schemas** (recurse into `schema` fields) → `mergeWithStore()` which calls `shouldUpdate()` and maybe `shouldReorder()` + `merge()`; metadata via `mergeMetaWithStore()`.
69-
- Denormalize order: `createIfValid()`[validate()](references/validation.md)`fromJS()`**unvisit nested schemas** (recurse into `schema` fields).
68+
- **Normalize** (JSON response → cache): operates on POJOs; output is JSON-serializable plain data stored in the normalized cache. Order: `process()``pk()`[validate()](references/validation.md)**visit nested schemas** (recurse into `schema` fields) → if existing: `mergeWithStore()` which calls `shouldUpdate()` and maybe `shouldReorder()` + `merge()`; metadata via `mergeMetaWithStore()`.
69+
- **Denormalize** (cache → component): creates Entity **class instances** via `fromJS()`, restoring prototype chain so getters, methods, and `schema` processing work. Order: `createIfValid()`[validate()](references/validation.md)`fromJS()`**unvisit nested schemas** (recurse into `schema` fields).
7070

7171
---
7272

@@ -105,11 +105,20 @@ export const EventResource = resource({
105105

106106
### pk routing
107107

108-
`pk()` uses `nestKey(parent, key)` when nested in an Entity and available; otherwise it uses `argsKey(...args)`, then serializes the result. Without options, it defaults to `argsKey: params => ({ ...params })`, using all endpoint args as the collection key. Provide both `argsKey` and `nestKey` to reuse one Collection definition top-level and nested.
108+
`pk()` uses `nestKey(parent, key)` when nested in an Entity and available; otherwise it uses `argsKey(...args)`, then serializes the result. Without options, it defaults to `argsKey: params => ({ ...params })`, using all endpoint args as the collection key.
109109

110110
- `argsKey` — derive pk from endpoint arguments (default)
111111
- `nestKey` — derive pk from parent entity for nested shared-state collections
112112

113+
Define **both** on the same `Collection` to reuse one definition top-level and nested. When `argsKey(args)` and `nestKey(parent)` produce the same object shape, the top-level fetch and the nested read resolve to the **same (referentially equal) array/map** — push/unshift/assign/move/remove on either updates both:
114+
115+
```ts
116+
const userTodos = new Collection([Todo], {
117+
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
118+
nestKey: (parent: User) => ({ userId: parent.id }),
119+
});
120+
```
121+
113122
### nonFilterArgumentKeys
114123

115124
Default `createCollectionFilter` uses `nonFilterArgumentKeys` (default: keys starting with `'order'`) to exclude non-filter args when matching collections. This affects which existing collections receive new items from `push`/`unshift`/`assign`/`move`.

.cursor/skills/data-client-vue-testing/SKILL.md

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: data-client-vue-testing
3-
description: Test @data-client/vue composables and components - renderDataCompose, mountDataClient, fixtures, jest, nock, Vue 3 reactive props, useSuspense testing
3+
description: Test @data-client/vue composables and components - renderDataCompose, mountDataClient, fixtures, jest, nock HTTP mocking, polling/subscription tests with fake timers, useSuspense, useLive, useSubscription, Vue 3 reactive props
44
license: Apache 2.0
55
---
66

@@ -251,73 +251,63 @@ await nextTick();
251251

252252
## Testing with nock (HTTP Mocking)
253253

254+
Use nock when a test must exercise the real fetch path — verifying URL construction, headers, request bodies, retries, or anything in your `RestEndpoint`/`Resource` networking layer. For pure store/state behavior, prefer `initialFixtures`/`resolverFixtures` (lighter and faster).
255+
256+
Minimal shape:
257+
254258
```typescript
255259
import nock from 'nock';
256260

257261
beforeAll(() => {
258262
nock(/.*/)
259263
.persist()
260-
.defaultReplyHeaders({
261-
'Access-Control-Allow-Origin': '*',
262-
'Content-Type': 'application/json',
263-
})
264-
.options(/.*/)
265-
.reply(200)
266-
.get('/article/5')
267-
.reply(200, { id: 5, title: 'hi ho' });
264+
.defaultReplyHeaders({ 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' })
265+
.options(/.*/).reply(200) // CORS preflight (required in JSDOM)
266+
.get('/article/5').reply(200, { id: 5, title: 'hi ho' });
268267
});
269268

270-
afterAll(() => {
271-
nock.cleanAll();
272-
});
269+
afterAll(() => nock.cleanAll());
273270
```
274271

275-
**Dynamic responses with nock:**
276-
```typescript
277-
const fetchMock = jest.fn(() => payload);
278-
nock(/.*/)
279-
.get(`/article/${payload.id}`)
280-
.reply(200, fetchMock);
272+
For dynamic server state, mutating-closure replies, request spying with `jest.fn()`, error responses, and mixing nock with fixtures, see [references/nock-http-mocking.md](references/nock-http-mocking.md).
281273

282-
// Later verify:
283-
expect(fetchMock).toHaveBeenCalledTimes(1);
284-
```
274+
## Testing Polling and Subscriptions
285275

286-
## Testing Polling/Subscriptions
276+
For composables with `pollFrequency`, `useLive`, or `useSubscription`, use fake timers so polls fire deterministically. Core flow:
277+
278+
1. `jest.useFakeTimers()` **before** mount/render (so the interval is created under fake timers).
279+
2. Render, then `jest.advanceTimersByTime(frequency)` to drive the initial fetch.
280+
3. Mutate the response (e.g. `responseMock.mockReturnValue(...)`), advance time again, `await allSettled()` and `await nextTick()`.
281+
4. Restore real timers in `afterEach`: `jest.useRealTimers()`.
282+
283+
Quick example:
287284

288285
```typescript
289-
it('should poll and update', async () => {
290-
jest.useFakeTimers();
291-
let serverData = { id: 5, title: 'Original' };
286+
jest.useFakeTimers();
287+
const responseMock = jest.fn(() => payload);
292288

293-
nock(/.*/)
294-
.persist()
295-
.get('/article/5')
296-
.reply(200, () => serverData);
297-
298-
const { wrapper } = mountDataClient(PollingComponent);
299-
300-
// Wait for initial render
301-
for (let i = 0; i < 100 && !wrapper.find('h3').exists(); i++) {
302-
await jest.advanceTimersByTimeAsync(frequency / 10);
303-
await nextTick();
304-
}
305-
expect(wrapper.find('h3').text()).toBe('Original');
289+
const { result, allSettled, waitForNextUpdate, cleanup } = await renderDataCompose(
290+
() => useSuspense(PollingArticleResource.get, { id: payload.id }),
291+
{ resolverFixtures: [{ endpoint: PollingArticleResource.get, response: responseMock }] },
292+
);
306293

307-
// Simulate server update
308-
serverData = { id: 5, title: 'Updated' };
294+
jest.advanceTimersByTime(frequency);
295+
await allSettled();
296+
await waitForNextUpdate();
297+
const articleRef = await result;
309298

310-
// Advance timers to trigger poll
311-
for (let i = 0; i < 20 && wrapper.find('h3').text() !== 'Updated'; i++) {
312-
await jest.advanceTimersByTimeAsync(frequency / 10);
313-
await nextTick();
314-
}
315-
expect(wrapper.find('h3').text()).toBe('Updated');
299+
responseMock.mockReturnValue({ ...payload, title: 'updated' });
300+
jest.advanceTimersByTime(frequency);
301+
await allSettled();
302+
await nextTick();
316303

317-
jest.useRealTimers();
318-
});
304+
expect(articleRef!.value.title).toBe('updated');
305+
jest.useRealTimers();
306+
cleanup();
319307
```
320308

309+
For unsubscribe patterns, component-level polling tests, fake-timer-safe `flushUntil`, polling via nock, and common pitfalls, see [references/polling-subscriptions.md](references/polling-subscriptions.md).
310+
321311
## Vue Suspense Behavior
322312

323313
**useSuspense() returns Promise → ComputedRef:**
@@ -380,6 +370,8 @@ For detailed API documentation, see the [references](references/) directory:
380370

381371
- [Fixtures](references/Fixtures.md) - Fixture format reference
382372
- [unit-testing-hooks](references/unit-testing-hooks.md) - Hook/composable testing guide
373+
- [nock-http-mocking](references/nock-http-mocking.md) - Full nock setup, dynamic server state, request spying, errors, pitfalls
374+
- [polling-subscriptions](references/polling-subscriptions.md) - Fake-timer patterns for `useLive`/`useSubscription`/`pollFrequency`, unsubscribe verification, polling via nock
383375

384376
## Common Patterns
385377

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Nock HTTP Mocking (Vue Testing)
2+
3+
Use `nock` instead of fixtures when you want a real fetch round-trip — for example, when verifying URL construction, headers, request bodies, retry logic, or anything that exercises the actual networking layer of your `RestEndpoint`/`Resource` definitions. For pure store/state behavior, prefer `initialFixtures`/`resolverFixtures` (lighter, faster, no global setup).
4+
5+
## Standard Setup
6+
7+
CORS preflight handling and a permissive default header set are required because the test environment runs in JSDOM and `fetch` performs OPTIONS preflights for non-simple requests.
8+
9+
```typescript
10+
import nock from 'nock';
11+
12+
beforeAll(() => {
13+
nock(/.*/)
14+
.persist()
15+
.defaultReplyHeaders({
16+
'Access-Control-Allow-Origin': '*',
17+
'Access-Control-Allow-Headers': 'Access-Token',
18+
'Content-Type': 'application/json',
19+
})
20+
.options(/.*/)
21+
.reply(200)
22+
.get(`/article/${payload.id}`)
23+
.reply(200, payload)
24+
.put(`/article/${payload.id}`)
25+
.reply(200, (uri, requestBody: any) => ({
26+
...payload,
27+
...requestBody,
28+
}));
29+
});
30+
31+
afterAll(() => {
32+
nock.cleanAll();
33+
});
34+
```
35+
36+
Key points:
37+
38+
- `nock(/.*/)` matches any host (matches your `urlPrefix` whether it's `/`, `https://example.com`, etc.).
39+
- `.persist()` keeps the interceptors alive across multiple requests in the test file. Without it, each interceptor fires once and is consumed.
40+
- `.options(/.*/).reply(200)` blanket-handles preflight — without it, mutations like PUT/POST/DELETE will hang or fail in JSDOM.
41+
42+
## Dynamic Server State
43+
44+
To simulate a server whose data changes during the test (mutations, polling, optimistic updates), reply with a function that returns a closure-bound variable. Reassign the variable to "update the server".
45+
46+
```typescript
47+
let currentPayload = { ...payload };
48+
49+
beforeAll(() => {
50+
nock(/.*/)
51+
.persist()
52+
.defaultReplyHeaders({
53+
'Access-Control-Allow-Origin': '*',
54+
'Content-Type': 'application/json',
55+
})
56+
.options(/.*/)
57+
.reply(200)
58+
.get(`/article/${payload.id}`)
59+
.reply(200, () => currentPayload)
60+
.put(`/article/${payload.id}`)
61+
.reply(200, (uri, requestBody: any) => ({
62+
...currentPayload,
63+
...requestBody,
64+
}));
65+
});
66+
67+
it('reflects updated server data on next fetch', async () => {
68+
// ... initial render ...
69+
70+
// Mutate "server" state
71+
currentPayload = { ...currentPayload, title: 'updated' };
72+
73+
// Trigger a refetch (e.g. via controller.fetch, polling, or invalidate)
74+
// ... assert new value ...
75+
});
76+
```
77+
78+
## Spying on Requests
79+
80+
Use `jest.fn()` as the reply handler to assert call counts and inspect request bodies.
81+
82+
```typescript
83+
const fetchMock = jest.fn(() => payload);
84+
85+
nock(/.*/)
86+
.persist()
87+
.defaultReplyHeaders({ 'Access-Control-Allow-Origin': '*' })
88+
.options(/.*/)
89+
.reply(200)
90+
.get(`/article/${payload.id}`)
91+
.reply(200, fetchMock);
92+
93+
// ... run test interactions ...
94+
95+
expect(fetchMock).toHaveBeenCalledTimes(1);
96+
```
97+
98+
For mutations, the second argument to the reply callback is the request body:
99+
100+
```typescript
101+
const updateMock = jest.fn((uri, requestBody) => ({ ...payload, ...requestBody }));
102+
103+
nock(/.*/)
104+
.persist()
105+
.put(`/article/${payload.id}`)
106+
.reply(200, updateMock);
107+
```
108+
109+
## Errors and Status Codes
110+
111+
```typescript
112+
nock(/.*/)
113+
.get('/article/missing')
114+
.reply(404, { detail: 'not found' });
115+
116+
nock(/.*/)
117+
.get('/article/broken')
118+
.replyWithError('network down');
119+
```
120+
121+
When testing error paths in components, also catch the error in your composable or wrap with `AsyncBoundary` so the test doesn't fail with an unhandled rejection.
122+
123+
## Combining nock With Fixtures
124+
125+
You can mix both within the same test file: use `initialFixtures` to pre-populate store state cheaply, and let nock handle any subsequent live requests (refetch, polling, mutation). The store will hydrate from fixtures first, then nock interceptors take over for actual fetches.
126+
127+
## Common Pitfalls
128+
129+
- **Hanging mutations** → missing `.options(/.*/).reply(200)` for CORS preflight.
130+
- **Interceptor consumed after one call** → forgot `.persist()`.
131+
- **Stale data after "server update"** → you reassigned the closure variable but never triggered a new fetch (no poll, no invalidate, no controller.fetch).
132+
- **`Nock: No match for request`** → the URL or method differs from what your endpoint actually sends. Add `nock.recorder.rec()` temporarily, or check `urlPrefix` and `path` template substitution.
133+
- **Cross-test pollution** → always `nock.cleanAll()` in `afterAll` (or `afterEach` for stricter isolation).

0 commit comments

Comments
 (0)