Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gcpolicy-unref-interval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/core': patch
---

GCPolicy interval no longer blocks Node.js process exit

Call `.unref()` on GCPolicy's `setInterval` in Node.js environments, preventing the GC sweep timer from keeping Jest workers or other Node.js processes alive.
16 changes: 16 additions & 0 deletions .changeset/test-auto-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@data-client/test': patch
---

Add automatic cleanup after each test

[renderDataHook()](/docs/api/renderDataHook) and `makeRenderDataClient()` now register an `afterEach` hook at import time that automatically cleans up all active managers. Manual `renderDataHook.cleanup()` calls in `afterEach` are no longer needed.

```ts
// Before: ❌
afterEach(() => {
renderDataHook.cleanup();
});

// After: ✓ (no afterEach needed — cleanup is automatic)
```
36 changes: 18 additions & 18 deletions .cursor/skills/data-client-react-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ it('useSuspense() should render the response', async () => {
**Return values:**
- Inherits all `renderHook()` return values from `@testing-library/react`
- `controller` - Controller instance for manual actions
- `cleanup()` - Cleanup function
- `allSettled()` - Wait for all async operations to complete

**Cleanup is automatic** -- an `afterEach` hook is registered at module load time that drains all active cleanups. No manual `renderDataHook.cleanup()` calls are needed.

## Fixtures and Interceptors

**Success Fixture:**
Expand Down Expand Up @@ -119,30 +120,28 @@ it('should handle fetch errors', async () => {

## Testing Components

Use `MockResolver` to provide fixture data when rendering components with `DataProvider`:

```typescript
import { render } from '@testing-library/react';
import { DataProvider } from '@data-client/react';

const renderWithProvider = (component, options = {}) => {
return render(
<DataProvider {...options}>
{component}
</DataProvider>
);
};
import { MockResolver } from '@data-client/test';

it('should render todo list', async () => {
const { getByText } = renderWithProvider(
<TodoList />,
const fixtures = [
{
initialFixtures: [
{
endpoint: TodoResource.getList,
args: [],
response: [{ id: 1, title: 'Test Todo', completed: false }],
},
],
endpoint: TodoResource.getList,
args: [],
response: [{ id: 1, title: 'Test Todo', completed: false }],
},
];

const { getByText } = render(
<DataProvider>
<MockResolver fixtures={fixtures}>
<TodoList />
</MockResolver>
</DataProvider>,
);

expect(getByText('Test Todo')).toBeInTheDocument();
Expand Down Expand Up @@ -212,6 +211,7 @@ packages/react/src/components/__tests__/DataProvider.test.tsx
- Test mutations and their side effects
- Don't mock @data-client internals directly
- Don't use raw fetch in tests when fixtures are available
- Don't manually call `renderDataHook.cleanup()` in `afterEach` -- cleanup is automatic

## References

Expand Down
27 changes: 12 additions & 15 deletions docs/core/api/renderDataHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ type RenderDataHook = {
options?: waitForOptions,
): Promise<T>;
};
/** @deprecated use per-render cleanup() instead */
/** cleanup is automatic; only needed for ordering (e.g., before jest.useRealTimers()) */
cleanup(): void;
/** @deprecated use per-render allSettled() instead */
allSettled(): Promise<unknown>;
};
```
Expand Down Expand Up @@ -159,25 +158,23 @@ it('should update', async () => {

### cleanup()

Cleans up all managers used in this render. Should be run in `afterEach()` to ensure each test starts fresh.
Cleans up all managers used in this render.
This is especially important when mocking timers, as Reactive Data Client's internals rely on real timers to
avoid race conditions.

```ts
afterEach(() => {
renderDataHook.cleanup();
});
```

`cleanup()` is also available on the return value of each `renderDataHook()` call, which is
preferred when multiple renders occur in a single test:
Cleanup runs automatically after each test via a module-level `afterEach` hook (similar to `@testing-library/react`).
Manual calls are only needed when you must control cleanup ordering within a test body -- for example,
cleaning up before switching from fake timers to real timers:

```ts
const { result, cleanup } = renderDataHook(() => useSuspense(MyResource.get, { id: 5 }), {
initialFixtures: [{ endpoint: MyResource.get, args: [{ id: 5 }], response }],
it('should handle polling', async () => {
jest.useFakeTimers();
const { result } = renderDataHook(/* ... */);
// ... assertions ...
// highlight-next-line
renderDataHook.cleanup(); // must run while fake timers are still active
jest.useRealTimers();
});
// ... assertions ...
cleanup();
```

### allSettled()
Expand Down
3 changes: 1 addition & 2 deletions docs/core/guides/unit-testing-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ that are wrappers around [@testing-library/react-hooks](https://github.com/testi
We want a [renderDataHook()](../api/renderDataHook.md) function that renders in the context of both
a `Provider` and `Suspense` boundary.

These will generally be done during test setup. It's important to call cleanup
upon test completion.
These will generally be done during test setup. Cleanup runs automatically after each test.

:::note

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/state/GCPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class GCPolicy implements GCInterface {
this.intervalId = setInterval(() => {
this.idleCallback(() => this.runSweep(), { timeout: 1000 });
}, this.options.intervalMS);
if (typeof this.intervalId === 'object' && 'unref' in this.intervalId) {
this.intervalId.unref();
}
}

cleanup() {
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/endpoint-types.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ describe('endpoint types', () => {
renderDataClient = makeRenderDataClient(makeProvider);
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/hooks-endpoint.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@ describe('useController().getState', () => {
});

afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});
beforeEach(() => {
Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/__tests__/integration-collections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,6 @@ describe.each([
renderDataClient = makeRenderDataClient(makeProvider);
});

afterEach(() => {
renderDataClient.cleanup();
});

it('should work with unions', async () => {
const prevWarn = global.console.warn;
global.console.warn = jest.fn();
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/integration-endpoint.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ describe.each([
});

afterEach(() => {
renderDataHook.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ describe('indexes', () => {
});

afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/integration-nesting.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ describe.each([
});

afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/__tests__/integration.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ describe('SSR', () => {
renderDataClient = makeRenderDataClient(ExternalDataProvider);
});

afterEach(() => {
renderDataClient.cleanup();
});

it('should update useCache()', async () => {
const { result } = renderDataClient(
() => {
Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/__tests__/optional-members.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ describe(`optional members`, () => {
renderDataClient = makeRenderDataClient(CacheProvider);
});

afterEach(() => {
renderDataClient.cleanup();
});

it('should return all members of list without suspending', () => {
const { result } = renderDataClient(
() => {
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/subscriptions-endpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ describe.each([
renderDataClient = makeRenderDataClient(makeProvider);
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
jest.useRealTimers();
});
Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/__tests__/useCache-endpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ describe('useCache()', () => {
renderDataClient = makeRenderDataClient(CacheProvider);
});

afterEach(() => {
renderDataClient.cleanup();
});

it('should be null with empty state', () => {
const { result } = renderDataClient(() => {
return useCache(CoolerArticleResource.get, { id: payload.id });
Expand Down
4 changes: 0 additions & 4 deletions packages/react/src/__tests__/useError-endpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ describe('useError()', () => {
renderDataClient = makeRenderDataClient(CacheProvider);
});

afterEach(() => {
renderDataClient.cleanup();
});

it('should return undefined when cache not ready and no error in meta', () => {
const initialFixtures = [
{
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/__tests__/useFetch-use.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ describeIf19.each([['DataProvider', DataProvider]] as const)(
});

afterEach(() => {
renderDataHook.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ describe.each([
renderDataClient = makeRenderDataClient(makeProvider);
});
afterEach(() => {
renderDataClient.cleanup();
jest.useRealTimers();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/subscriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ describe.each([
renderDataClient = makeRenderDataClient(makeProvider);
});
afterEach(() => {
renderDataClient.cleanup();
jest.useRealTimers();
});

Expand Down
3 changes: 0 additions & 3 deletions packages/react/src/hooks/__tests__/useCache.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ describe('useCache()', () => {
beforeEach(() => {
renderDataClient = makeRenderDataClient(CacheProvider);
});
afterEach(() => {
renderDataClient.cleanup();
});

it('should be undefined with empty state', () => {
const { result } = renderDataClient(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/useController/fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ describe.each([
});

afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/useController/reset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/useController/set.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ beforeEach(() => {
});
});
afterEach(() => {
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/useDLE.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ describe('useDLE', () => {
});
afterEach(() => {
warnSpy.mockRestore();
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
3 changes: 0 additions & 3 deletions packages/react/src/hooks/__tests__/useDLE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ describe('useDLE()', () => {
beforeEach(() => {
renderDataClient = makeRenderDataClient(CacheProvider);
});
afterEach(() => {
renderDataClient.cleanup();
});

it('should work on good network', async () => {
const { result, waitForNextUpdate } = renderDataClient(() => {
Expand Down
3 changes: 0 additions & 3 deletions packages/react/src/hooks/__tests__/useError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ describe('useError()', () => {
beforeEach(() => {
renderDataClient = makeRenderDataClient(CacheProvider);
});
afterEach(() => {
renderDataClient.cleanup();
});

it('should return undefined when cache not ready and no error in meta', () => {
const initialFixtures = [
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/hooks/__tests__/useFetch.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ describe('useFetch', () => {
});
afterEach(() => {
warnSpy.mockRestore();
renderDataClient.cleanup();
nock.cleanAll();
});

Expand Down
Loading