Skip to content

Commit 999b895

Browse files
authored
docs: fix testing docs (#3419)
This fixes the testing page in the docs for real this time. Every test is hand checked now. Thanks to a certain button, which appeared recently. Optionally I can also split this page up in separate pages: - Middlewars - AppWrappers and Layouts - Routing and Handlers - SSR and Islands
1 parent 399cbc3 commit 999b895

1 file changed

Lines changed: 98 additions & 75 deletions

File tree

docs/latest/testing/index.md

Lines changed: 98 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: |
3-
Add a global app wrapper to provide common meta tags or context for application routes.
3+
Learn how to test Fresh applications using Deno's built-in test runner.
44
---
55

66
To ensure that your application works as expected we can write tests. Any aspect
@@ -11,21 +11,25 @@ write tests.
1111
## Testing middlewares
1212

1313
To test [middlewares](/docs/concepts/middleware) we're going to create a dummy
14-
app and return the relevant info we want to check in a custom `/` handler.
14+
app and return the relevant info we want to check in a custom `/` handler. This
15+
test assumes the `State` object in `utils.ts` has `text` property.
1516

16-
```ts middleware.test.ts
17+
```ts tests/middleware.test.ts
1718
import { expect } from "@std/expect";
1819
import { App } from "fresh";
20+
import { define, type State } from "../utils.ts";
1921

2022
const middleware = define.middleware((ctx) => {
2123
ctx.state.text = "middleware text";
2224
return ctx.next();
2325
});
2426

2527
Deno.test("My middleware - sets ctx.state.text", async () => {
26-
const handler = new App()
28+
const handler = new App<State>()
2729
.use(middleware)
28-
.get("/", (ctx) => new Response(ctx.state.text))
30+
.get("/", (ctx) => {
31+
return new Response(ctx.state.text || "");
32+
})
2933
.handler();
3034

3135
const res = await handler(new Request("http://localhost"));
@@ -43,11 +47,12 @@ that adds a header to the returned response, you can assert against that too.
4347
Both the [app wrapper](/docs/advanced/app-wrapper) component and
4448
[layouts](/docs/advanced/layouts) can be tested in the same way.
4549

46-
```tsx routes/_app.test.tsx
50+
```tsx tests/appWrapper.test.tsx
4751
import { expect } from "@std/expect";
4852
import { App } from "fresh";
53+
import { define, type State } from "../utils.ts";
4954

50-
function AppWrapper({ Component }) {
55+
const AppWrapper = define.layout(function AppWrapper({ Component }) {
5156
return (
5257
<html lang="en">
5358
<head>
@@ -59,10 +64,10 @@ function AppWrapper({ Component }) {
5964
</body>
6065
</html>
6166
);
62-
}
67+
});
6368

6469
Deno.test("App Wrapper - renders title and content", async () => {
65-
const handler = new App()
70+
const handler = new App<State>()
6671
.appWrapper(AppWrapper)
6772
.get("/", (ctx) => ctx.render(<h1>hello</h1>))
6873
.handler();
@@ -71,71 +76,64 @@ Deno.test("App Wrapper - renders title and content", async () => {
7176
const text = await res.text();
7277

7378
expect(text).toContain("My App");
74-
expect(text).toContain("Hello");
79+
expect(text).toContain("hello");
7580
});
7681
```
7782

7883
Same can be done for layouts.
7984

80-
```tsx routes/_layout.test.tsx
85+
```tsx tests/layout.test.tsx
8186
import { expect } from "@std/expect";
8287
import { App } from "fresh";
88+
import { define, type State } from "../utils.ts";
8389

84-
function MyLayout({ Component }) {
90+
const MyLayout = define.layout(function MyLayout({ Component }) {
8591
return (
8692
<div>
8793
<h1>My Layout</h1>
8894
<Component />
8995
</div>
9096
);
91-
}
97+
});
9298

9399
Deno.test("MyLayout - renders heading and content", async () => {
94-
const handler = new App()
95-
.layout("*", MyLayout)
100+
const handler = new App<State>()
101+
.appWrapper(MyLayout)
96102
.get("/", (ctx) => ctx.render(<h1>hello</h1>))
97103
.handler();
98104

99105
const res = await handler(new Request("http://localhost"));
100106
const text = await res.text();
101107

102108
expect(text).toContain("My Layout");
103-
expect(text).toContain("Hello");
109+
expect(text).toContain("hello");
104110
});
105111
```
106112

107113
## Testing routes and handlers
108114

109115
For testing your route handlers and business logic, you can use the same
110-
[`App`](/docs/concepts/app) pattern shown above. Fresh 2.0 makes it easy to test
111-
individual routes without needing a full build process:
116+
[`App`](/docs/concepts/app) pattern shown above. Fresh makes it easy to test
117+
individual routes without needing a full build process, as long as they export a
118+
handler:
112119

113-
```ts my-routes.test.ts
120+
```ts tests/routes.test.ts
114121
import { expect } from "@std/expect";
115122
import { App } from "fresh";
123+
import { type State } from "../utils.ts";
116124

117-
// Import your route handlers
118-
import { handler as indexHandler } from "./routes/index.ts";
119-
import { handler as apiHandler } from "./routes/api/users.ts";
125+
// Import actual route handlers
126+
import { handler as apiHandler } from "../routes/api/[name].tsx";
120127

121-
Deno.test("Index route returns homepage", async () => {
122-
const app = new App().get("/", indexHandler);
123-
const handler = app.handler();
128+
Deno.test("API route returns name", async () => {
129+
const app = new App<State>()
130+
.get("/api/:name", apiHandler.GET)
131+
.handler();
124132

125-
const response = await handler(new Request("http://localhost/"));
133+
const response = await app(new Request("http://localhost/api/joe"));
126134
const text = await response.text();
127135

128-
expect(text).toContain("Welcome");
129-
});
130-
131-
Deno.test("API route returns JSON", async () => {
132-
const app = new App().get("/api/users", apiHandler);
133-
const handler = app.handler();
134-
135-
const response = await handler(new Request("http://localhost/api/users"));
136-
const json = await response.json();
137-
138-
expect(json).toEqual({ users: [] });
136+
expect(text).toEqual("Hello, Joe!");
139137
});
140138
```
141139

@@ -150,28 +148,37 @@ You can test that your islands render correctly on the server using the same
150148
[`App`](/docs/concepts/app) pattern. Note: this requires a `.tsx` file extension
151149
to use JSX:
152150

153-
```tsx island-ssr.test.tsx
151+
```tsx tests/island-ssr.test.tsx
154152
import { expect } from "@std/expect";
155153
import { App } from "fresh";
156-
import Counter from "./islands/Counter.tsx";
154+
import { useSignal } from "@preact/signals";
155+
import { type State } from "../utils.ts";
156+
import Counter from "../islands/Counter.tsx";
157+
158+
function CounterPage() {
159+
const count = useSignal(3);
160+
return (
161+
<div class="p-8">
162+
<h1>Counter Test Page</h1>
163+
<Counter count={count} />
164+
</div>
165+
);
166+
}
157167

158168
Deno.test("Counter page renders island", async () => {
159-
const app = new App().get("/counter", (ctx) => {
160-
return ctx.render(
161-
<div className="p-8">
162-
<h1>Counter Test Page</h1>
163-
<Counter />
164-
</div>,
165-
);
166-
});
167-
const handler = app.handler();
169+
const app = new App<State>()
170+
.get("/counter", (ctx) => {
171+
return ctx.render(<CounterPage />);
172+
})
173+
.handler();
168174

169-
const response = await handler(new Request("http://localhost/counter"));
175+
const response = await app(new Request("http://localhost/counter"));
170176
const html = await response.text();
171177

172178
// Verify the island's initial HTML is present
173-
expect(html).toContain('class="counter"');
174-
expect(html).toContain("count: 0");
179+
expect(html).toContain('class="flex gap-8 py-6"');
180+
expect(html).toContain("Counter Test Page");
181+
expect(html).toContain("3");
175182
});
176183
```
177184

@@ -181,27 +188,56 @@ For testing client-side island behavior (clicks, state changes, etc.), you need
181188
a full build and browser environment. You can use the approach similar to
182189
Fresh's own tests:
183190

184-
```tsx island-client.test.tsx
191+
```tsx tests/island-client.test.tsx
185192
import { expect } from "@std/expect";
186-
import { createBuilder } from "vite";
193+
import { buildFreshApp, startTestServer } from "./test-utils.ts";
194+
195+
const app = await buildFreshApp();
196+
197+
Deno.test("Counter island renders correctly", async () => {
198+
const { server, address } = startTestServer(app);
199+
200+
try {
201+
// Basic smoke test: verify the island HTML is served
202+
const response = await fetch(`${address}/`);
203+
const html = await response.text();
204+
205+
expect(html).toContain('class="flex gap-8 py-6"');
206+
expect(html).toContain("3");
207+
} finally {
208+
await server.shutdown();
209+
}
210+
});
211+
```
212+
213+
```tsx tests/test-utils.ts
214+
import { createBuilder, type InlineConfig } from "vite";
187215
import * as path from "@std/path";
188216

189-
// Create a production build
190-
const builder = await createBuilder({
217+
// Default Fresh build configuration
218+
export const FRESH_BUILD_CONFIG: InlineConfig = {
191219
logLevel: "error",
192220
root: "./",
193221
build: { emptyOutDir: true },
194222
environments: {
195223
ssr: { build: { outDir: path.join("_fresh", "server") } },
196224
client: { build: { outDir: path.join("_fresh", "client") } },
197225
},
198-
});
199-
await builder.buildApp();
226+
};
200227

201-
const app = await import("./_fresh/server.js");
228+
// Helper function to create and build the Fresh app
229+
export async function buildFreshApp(config: InlineConfig = FRESH_BUILD_CONFIG) {
230+
const builder = await createBuilder(config);
231+
await builder.buildApp();
232+
return await import("../_fresh/server.js");
233+
}
202234

203-
Deno.test("Counter island renders correctly", async () => {
204-
// Start production server
235+
// Helper function to start a test server
236+
export function startTestServer(app: {
237+
default: {
238+
fetch: (req: Request) => Promise<Response>;
239+
};
240+
}) {
205241
const server = Deno.serve({
206242
port: 0,
207243
handler: app.default.fetch,
@@ -210,21 +246,8 @@ Deno.test("Counter island renders correctly", async () => {
210246
const { port } = server.addr as Deno.NetAddr;
211247
const address = `http://localhost:${port}`;
212248

213-
try {
214-
// Basic smoke test: verify the island HTML is served
215-
const response = await fetch(`${address}/counter`);
216-
const html = await response.text();
217-
218-
expect(html).toContain('class="counter"');
219-
expect(html).toContain("count: 0");
220-
221-
// For full browser interactivity testing, you would need:
222-
// - Browser automation tools (Puppeteer, Playwright)
223-
// - withBrowser utility from Fresh's test suite
224-
} finally {
225-
await server.shutdown();
226-
}
227-
});
249+
return { server, address };
250+
}
228251
```
229252

230253
**Note:** For most applications, testing the server-side rendering is

0 commit comments

Comments
 (0)