Skip to content

Commit d968ba2

Browse files
committed
docs: rewrite README as a TanStack + TWD showcase
Replace the TanStack Start boilerplate README with a project-specific showcase doc covering the TanStack tools wired in (Router + loaders, Query, Form), a minimal TWD test snippet, the SPA-navigation / module-cache testing consideration (with the singleton + clear pattern), project layout, scripts, and CI. Include a sidebar screenshot under docs/.
1 parent f6a88d2 commit d968ba2

2 files changed

Lines changed: 104 additions & 142 deletions

File tree

README.md

Lines changed: 104 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,193 +1,155 @@
1-
Welcome to your new TanStack Start app!
1+
# twd-tanstack-example
22

3-
# Getting Started
3+
A showcase of the [TanStack](https://tanstack.com) ecosystem — Router, Query, and Form — tested **in the browser, while you build**, with [TWD](https://github.com/BRIKEV/twd-js).
44

5-
To run this application:
5+
![TWD sidebar running tests against the Todos page](docs/twd-sidebar.png)
66

7-
```bash
8-
npm install
9-
npm run dev
10-
```
7+
The sidebar on the left is TWD running real assertions against the app on the right. No extra renderer, no jsdom, no separate "test build" — just the dev server and a small panel.
118

12-
# Building For Production
9+
---
1310

14-
To build this application for production:
11+
## What this project demonstrates
1512

16-
```bash
17-
npm run build
18-
```
13+
| TanStack | Where it lives | What it does |
14+
|---|---|---|
15+
| **Router** | `src/routes/*` | File-based routing, code-split routes, root context, navigation with `<Link>`, 404 `notFoundComponent` |
16+
| **Router loaders** | `src/routes/todos.tsx` | `loader` calls `queryClient.ensureQueryData(...)` so the page already has data when it mounts |
17+
| **Query** | `src/api/queries.ts`, `src/query-client.ts` | `queryOptions` shared between loader and component, `useSuspenseQuery` in the view, `useMutation` + `invalidateQueries` for create/delete |
18+
| **Form** | `src/routes/todos.tsx` | `useForm`, per-field validation, `<form.Subscribe>` for submit state |
1919

20-
## Testing
20+
The rest of the stack:
2121

22-
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
22+
- **Vite** dev server on `:3000`, `/api` proxied to a json-server on `:3001`.
23+
- **Tailwind v4** for styling.
24+
- **TWD** (`twd-js` + `twd-relay`) for browser-side tests, **twd-cli** for headless CI runs.
25+
- **vite-plugin-istanbul** + **nyc** for coverage.
26+
- **openapi-mock-validator** (via twd-cli) validating mocks against `contracts/todos-3.0.json` on every CI run.
2327

24-
```bash
25-
npm run test
26-
```
28+
---
2729

28-
## Styling
30+
## Getting started
2931

30-
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
32+
```bash
33+
npm install
34+
npm run serve:dev # json-server on :3001 + Vite dev on :3000
35+
```
3136

32-
### Removing Tailwind CSS
37+
Open <http://localhost:3000>. The TWD sidebar opens with the app — hit **Run All** to run the tests in `src/twd-tests/`.
3338

34-
If you prefer not to use Tailwind CSS:
39+
---
3540

36-
1. Remove the demo pages in `src/routes/demo/`
37-
2. Replace the Tailwind import in `src/styles.css` with your own styles
38-
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
39-
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
41+
## Writing a TWD test — the whole thing
4042

43+
TWD tests are plain `.ts` files next to your code. They run **inside the same browser tab as your app**, so they can import anything the app uses.
4144

45+
```ts
46+
// src/twd-tests/helloWorld.twd.test.ts
47+
import { twd, userEvent, screenDom } from 'twd-js'
48+
import { describe, it, beforeEach } from 'twd-js/runner'
49+
import { queryClient } from '#/query-client'
4250

43-
## Routing
51+
describe('Hello World Page', () => {
52+
beforeEach(() => {
53+
twd.clearRequestMockRules()
54+
queryClient.clear()
55+
})
4456

45-
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
57+
it('counts up when you click', async () => {
58+
await twd.visit('/')
4659

47-
### Adding A Route
60+
const button = await screenDom.findByText('Count is 0')
61+
await userEvent.click(button)
62+
twd.should(button, 'have.text', 'Count is 1')
63+
})
64+
})
65+
```
4866

49-
To add a new route to your application just add a new file in the `./src/routes` directory.
67+
That's it. No render setup, no `MemoryRouter`, no `QueryClientProvider` wrapper in the test. The real router, the real query client, the real DOM.
5068

51-
TanStack will automatically generate the content of the route file for you.
69+
Look at `src/twd-tests/todoList.twd.test.ts` for the data-fetching version: it mocks `/api/todos` with `twd.mockRequest`, waits for the request with `twd.waitForRequest`, and asserts on the resulting DOM.
5270

53-
Now that you have two routes you can use a `Link` component to navigate between them.
71+
---
5472

55-
### Adding Links
73+
## Testing consideration: SPA navigation keeps module-level state alive
5674

57-
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
75+
This is something to keep in mind whenever you write TWD tests against an app that caches data in memory. It will come up in **any** project that uses TanStack Query, Zustand, Valtio, Apollo, or anything else that holds state at the module level — not just this one. If you're not aware of it, your tests will look like they have a network problem when they actually have a state problem.
5876

59-
```tsx
60-
import { Link } from "@tanstack/react-router";
61-
```
77+
**The setup.** `twd.visit('/somewhere')` is a router navigation, not a page reload — same browser tab, same JS runtime, same module instances.
6278

63-
Then anywhere in your JSX you can use it like so:
79+
**The trap.** TanStack Query caches results by key. A loader that calls `ensureQueryData(['todos'])` will fetch the first time, then on every subsequent `twd.visit('/todos')` it will return the cached array **without ever calling fetch**. The MSW mock you set up for that test never matches anything, and you get:
6480

65-
```tsx
66-
<Link to="/about">About</Link>
6781
```
68-
69-
This will create a link that will navigate to the `/about` route.
70-
71-
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
72-
73-
### Using A Layout
74-
75-
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
76-
77-
Here is an example layout that includes a header:
78-
79-
```tsx
80-
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
81-
82-
export const Route = createRootRoute({
83-
head: () => ({
84-
meta: [
85-
{ charSet: 'utf-8' },
86-
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
87-
{ title: 'My App' },
88-
],
89-
}),
90-
shellComponent: ({ children }) => (
91-
<html lang="en">
92-
<head>
93-
<HeadContent />
94-
</head>
95-
<body>
96-
<header>
97-
<nav>
98-
<Link to="/">Home</Link>
99-
<Link to="/about">About</Link>
100-
</nav>
101-
</header>
102-
{children}
103-
<Scripts />
104-
</body>
105-
</html>
106-
),
107-
})
82+
Rule "getTodoList" was not executed within 1000ms.
83+
Executed rules: none
10884
```
10985

110-
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
111-
112-
## Server Functions
86+
The test fails for what looks like a network reason but is really a *state* reason.
11387

114-
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
88+
**The fix.** Export the `QueryClient` as a module-level singleton and call `clear()` between tests:
11589

116-
```tsx
117-
import { createServerFn } from '@tanstack/react-start'
90+
```ts
91+
// src/query-client.ts
92+
import { QueryClient } from '@tanstack/react-query'
11893

119-
const getServerTime = createServerFn({
120-
method: 'GET',
121-
}).handler(async () => {
122-
return new Date().toISOString()
94+
export const queryClient = new QueryClient({
95+
defaultOptions: { queries: { staleTime: 1000 * 30 } },
12396
})
124-
125-
// Use in a component
126-
function MyComponent() {
127-
const [time, setTime] = useState('')
128-
129-
useEffect(() => {
130-
getServerTime().then(setTime)
131-
}, [])
132-
133-
return <div>Server time: {time}</div>
134-
}
13597
```
13698

137-
## API Routes
99+
```ts
100+
// any *.twd.test.ts
101+
import { queryClient } from '#/query-client'
138102

139-
You can create API routes by using the `server` property in your route definitions:
140-
141-
```tsx
142-
import { createFileRoute } from '@tanstack/react-router'
143-
import { json } from '@tanstack/react-start'
144-
145-
export const Route = createFileRoute('/api/hello')({
146-
server: {
147-
handlers: {
148-
GET: () => json({ message: 'Hello, World!' }),
149-
},
150-
},
103+
beforeEach(() => {
104+
twd.clearRequestMockRules()
105+
queryClient.clear()
151106
})
152107
```
153108

154-
## Data Fetching
109+
ESM modules are singletons, so the cache the test clears **is** the cache the app reads from. No `window` globals, no test-only branches. The same pattern works for Zustand (`store.getState().reset()`), Valtio, etc. — anything you can `import`, you can reset.
155110

156-
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
111+
You can see the difference in two of this project's commits: the QueryClient extraction (`refactor: extract QueryClient to a singleton module`) and the test update that added `queryClient.clear()` to `beforeEach`.
157112

158-
For example:
113+
---
159114

160-
```tsx
161-
import { createFileRoute } from '@tanstack/react-router'
115+
## Project layout
162116

163-
export const Route = createFileRoute('/people')({
164-
loader: async () => {
165-
const response = await fetch('https://swapi.dev/api/people')
166-
return response.json()
167-
},
168-
component: PeopleComponent,
169-
})
170-
171-
function PeopleComponent() {
172-
const data = Route.useLoaderData()
173-
return (
174-
<ul>
175-
{data.results.map((person) => (
176-
<li key={person.name}>{person.name}</li>
177-
))}
178-
</ul>
179-
)
180-
}
117+
```
118+
src/
119+
api/
120+
todos.ts # fetch helpers, types derived from contracts/todos-3.0.json
121+
queries.ts # todosQueryOptions (shared by loader + component)
122+
routes/
123+
__root.tsx # nav, Devtools panels, 404 component
124+
index.tsx # Home (counter)
125+
todos.tsx # loader + useSuspenseQuery + useMutation + useForm
126+
twd-tests/ # *.twd.test.ts run in-browser via TWD
127+
mocks/
128+
query-client.ts # singleton QueryClient (importable from tests)
129+
router.tsx # createAppRouter() — wires routeTree + queryClient context
130+
main.tsx # <QueryClientProvider><RouterProvider/></QueryClientProvider>
131+
contracts/
132+
todos-3.0.json # OpenAPI 3.0 spec, validated against mocks in CI
133+
data/
134+
data.json # json-server seed
135+
twd.config.json # twd-cli headless config + contract validation
181136
```
182137

183-
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
138+
---
184139

185-
# Demo files
140+
## Scripts
186141

187-
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
142+
| Command | What it does |
143+
|---|---|
144+
| `npm run dev` | Vite dev server on `:3000` (TWD sidebar opens automatically) |
145+
| `npm run serve` | json-server on `:3001` |
146+
| `npm run serve:dev` | Both in parallel |
147+
| `npm run dev:ci` | Same as `dev` but with `CI=true` (turns on istanbul instrumentation) |
148+
| `npm run test:ci` | Headless run via `twd-cli` (used by GitHub Actions) |
149+
| `npm run collect:coverage:text` | Print coverage to stdout |
188150

189-
# Learn More
151+
---
190152

191-
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
153+
## CI
192154

193-
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).
155+
`.github/workflows/ci.yml` boots `dev:ci`, runs `twd-cli` headless via the official action, validates every mock response against the OpenAPI spec, posts a contract report as a PR comment, and prints coverage. See `twd.config.json` for the contract configuration.

docs/twd-sidebar.png

294 KB
Loading

0 commit comments

Comments
 (0)