# Testing

> Test components and loaders that use raf-query with a fresh client per test, seeded data, an injectable clock and real browser events.

Components that read queries need a query client and handles. In tests, create both directly. There's nothing to mock in raf-query itself: mock your API, or seed the cache. The examples use [Vitest](https://vitest.dev) and [Testing Library](https://testing-library.com/docs/react-testing-library/intro/), but nothing depends on them.

## A client per test

Create a new client for every test, so no cached data leaks between them:

```tsx
let queryClient: QueryClient;

beforeEach(() => {
	queryClient = createQueryClient({ isServer: false });
});

afterEach(() => {
	removeAll(queryClient);
});

function renderWithClient(ui: ReactNode) {
	return act(async () =>
		render(
			<QueryClientProvider client={queryClient}>
				<Suspense fallback="Loading…">{ui}</Suspense>
			</QueryClientProvider>,
		),
	);
}
```

- A client starts with no links and no features: no retries, no refetching on focus, no polling. A failing request fails at once. Add only the parts a test is about. If your app makes its client in a function, let that function take the parts, so tests can leave `retry()` out.
- `removeAll` drops every query and aborts requests in flight, so nothing runs after the test ends. Features stop when the `QueryClientProvider` unmounts, which Testing Library's `cleanup` does after each test.
- `isServer: false` makes it a browser client even where `window` isn't defined. See [Server rendering](#server-rendering).

Render inside an awaited, async [`act`](https://react.dev/reference/react/act). A tree that suspends inside a sync `act`, which `render` on its own uses, is never retried by React once its data lands, with or without a Suspense boundary. The same holds for any `use(promise)`, not only raf-query's.

## Testing a component

A component takes a handle as a prop, so a test can start the query itself with `load`, the same way a loader would:

```tsx
test("shows the post", async () => {
	const [handles] = await load(queryClient, {
		post: prefetch(postQuery, { slug: "hello" }),
	});

	await renderWithClient(<Post handle={handles.post} />);

	expect((await screen.findByRole("heading")).textContent).toBe("Hello");
});
```

## Seeding the cache

To render with known data and no request at all, write it with `setData` before loading:

```ts
const queryClient = createQueryClient({
	isServer: false,
	staleTime: Number.POSITIVE_INFINITY,
});

setData(
	queryClient,
	postQuery,
	{ slug: "hello" },
	{ slug: "hello", title: "Hello", body: "", authorId: "" },
);
const [handles] = await load(queryClient, {
	post: swr(postQuery, { slug: "hello" }),
});
```

`swr` resolves at once when the cache has data. It also refetches stale data in the background, and data written with `setData` is fresh only for `staleTime`, which is `0` by default: give the test client a `staleTime` so the seeded data stays as it is.

## Mocking the network

The `queryFn` is your code, so mock what it calls. With `fetch`, [MSW](https://mswjs.io) intercepts requests without changing application code:

```ts
server.use(
	http.get("/api/posts/:slug", () =>
		HttpResponse.json({ slug: "hello", title: "Hello" }),
	),
);
```

## Testing loaders

A loader is a function of its context. Export it from the route file, call it with a client, and check what it started:

```ts
test("the post loader starts both queries", async () => {
	const queryClient = createQueryClient({ isServer: false });
	await postLoader({
		context: { queryClient },
		params: { slug: "hello" },
	});
	expect(getData(queryClient, postQuery, { slug: "hello" })).toBeDefined();
	expect(isFetching(queryClient, commentsQuery)).toBe(1);
});
```

`getData` reads what an `swr` or `revalidate` entry loaded, and `isFetching(queryClient, target)` counts the requests still in flight, such as a prefetch's.

## Time

Staleness, garbage collection, retry delays, timeouts and polling depend on time. The client reads it from its `clock`: `now()`, `setTimeout` and `clearTimeout`. Pass your own to `createQueryClient({ clock })`, and a test of `gcTime` or `staleTime` runs in milliseconds, without touching globals:

```ts
interface Timer {
	readonly at: number;
	readonly callback: () => void;
}

/** A clock whose timers fire only when the test advances it. */
function fakeClock(): Clock & { advance(ms: number): void } {
	let now = 0;
	let timers: Array<Timer> = [];
	return {
		now: () => now,
		setTimeout(callback, ms) {
			const timer: Timer = { at: now + ms, callback };
			timers.push(timer);
			return timer;
		},
		clearTimeout(handle) {
			timers = timers.filter((timer) => timer !== handle);
		},
		advance(ms) {
			const end = now + ms;
			for (;;) {
				const due = timers
					.filter((timer) => timer.at <= end)
					.toSorted((a, b) => a.at - b.at)[0];
				if (!due) break;
				timers = timers.filter((timer) => timer !== due);
				now = due.at;
				due.callback();
			}
			now = end;
		},
	};
}

test("garbage-collects unread entries", () => {
	const clock = fakeClock();
	const queryClient = createQueryClient({ clock, isServer: false });
	const post: Post = {
		slug: "hello",
		title: "Hello",
		body: "",
		authorId: "ada",
	};

	setData(queryClient, postQuery, { slug: "hello" }, post);
	clock.advance(5 * 60 * 1000); // the default gcTime

	expect(getData(queryClient, postQuery, { slug: "hello" })).toBeUndefined();
});
```

The default clock calls `Date.now()` and `setTimeout` each time, so [`vi.useFakeTimers()`](https://vitest.dev/api/vi.html#vi-usefaketimers) works as well.

## Focus and the network

`focusRefetch()` and `reconnectRefetch()` listen to the page's own events, so a test in a DOM environment fires them. Give the client the feature, render under a `QueryClientProvider` (features only run while one has the client mounted), and dispatch the event:

```tsx
test("refetches on focus", async () => {
	let calls = 0;
	const countQuery = () =>
		queryOptions({ queryKey: count(), queryFn: async () => ++calls });
	function Count(props: { handle: QueryHandle<typeof countQuery> }) {
		const [countThenable] = useQuery(countQuery, props.handle);
		const count = use(countThenable);
		return <p>{count}</p>;
	}

	queryClient = createQueryClient({
		isServer: false,
		features: [focusRefetch()],
	});
	const [handles] = await load(queryClient, {
		count: revalidate(countQuery),
	});
	await renderWithClient(<Count handle={handles.count} />);

	await act(async () => {
		window.dispatchEvent(new Event("focus"));
	});
	expect(calls).toBe(2);
});
```

`focusRefetch()` refetches on `focus` only while `document.visibilityState` is `"visible"`. For reconnecting, dispatch `new Event("online")`. Retries wait for that event too, while `navigator.onLine` is `false`.

## Server rendering

A client created where `window` isn't defined is a server client: no retries, no garbage collection, and cached data is never revalidated. To test browser behavior in Node without a DOM environment, pass `isServer: false`:

```ts
const queryClient = createQueryClient({ isServer: false });
```
