Skip to content

Commit a473cc9

Browse files
authored
docs: update testing with routes and islands (#3385)
closes #3383
1 parent 9581d7e commit a473cc9

1 file changed

Lines changed: 113 additions & 19 deletions

File tree

docs/latest/testing/index.md

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,35 +104,129 @@ Deno.test("MyLayout - renders heading and content", async () => {
104104
});
105105
```
106106

107-
## Testing with file routes and islands
107+
## Testing routes and handlers
108108

109-
[File routes](/docs/concepts/file-routing) are collected by the
110-
[`Builder`](/docs/advanced/builder) class and not just by
111-
[`App`](/docs/concepts/app) alone. We can generate a snapshot and re-use it for
112-
many app instances in our test suite.
109+
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:
113112

114-
```ts my-app.test.ts
113+
```ts my-routes.test.ts
114+
import { expect } from "@std/expect";
115+
import { App } from "fresh";
116+
117+
// Import your route handlers
118+
import { handler as indexHandler } from "./routes/index.ts";
119+
import { handler as apiHandler } from "./routes/api/users.ts";
120+
121+
Deno.test("Index route returns homepage", async () => {
122+
const app = new App().get("/", indexHandler);
123+
const handler = app.handler();
124+
125+
const response = await handler(new Request("http://localhost/"));
126+
const text = await response.text();
127+
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: [] });
139+
});
140+
```
141+
142+
## Testing islands
143+
144+
Testing islands requires different approaches for server-side and client-side
145+
behavior:
146+
147+
### Server-side rendering of islands
148+
149+
You can test that your islands render correctly on the server using the same
150+
[`App`](/docs/concepts/app) pattern. Note: this requires a `.tsx` file extension
151+
to use JSX:
152+
153+
```tsx island-ssr.test.tsx
154+
import { expect } from "@std/expect";
155+
import { App } from "fresh";
156+
import Counter from "./islands/Counter.tsx";
157+
158+
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();
168+
169+
const response = await handler(new Request("http://localhost/counter"));
170+
const html = await response.text();
171+
172+
// Verify the island's initial HTML is present
173+
expect(html).toContain('class="counter"');
174+
expect(html).toContain("count: 0");
175+
});
176+
```
177+
178+
### Client-side island interactivity
179+
180+
For testing client-side island behavior (clicks, state changes, etc.), you need
181+
a full build and browser environment. You can use the approach similar to
182+
Fresh's own tests:
183+
184+
```tsx island-client.test.tsx
185+
import { expect } from "@std/expect";
115186
import { createBuilder } from "vite";
116-
// Best to do this once instead of for every test case for
117-
// performance reasons.
187+
import * as path from "@std/path";
188+
189+
// Create a production build
118190
const builder = await createBuilder({
119-
root: "./path/to/app",
120-
build: {
121-
emptyOutDir: true,
191+
logLevel: "error",
192+
root: "./",
193+
build: { emptyOutDir: true },
194+
environments: {
195+
ssr: { build: { outDir: path.join("_fresh", "server") } },
196+
client: { build: { outDir: path.join("_fresh", "client") } },
122197
},
123198
});
124-
await builder.build();
199+
await builder.buildApp();
125200

126-
const { app } = await import("./path/to/app/_fresh/server.js");
201+
const app = await import("./_fresh/server.js");
127202

128-
Deno.test("My Test", async () => {
129-
const handler = app.handler();
203+
Deno.test("Counter island renders correctly", async () => {
204+
// Start production server
205+
const server = Deno.serve({
206+
port: 0,
207+
handler: app.default.fetch,
208+
});
130209

131-
const response = await handler(new Request("http://localhost"));
132-
const text = await response.text();
210+
const { port } = server.addr as Deno.NetAddr;
211+
const address = `http://localhost:${port}`;
212+
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();
133217

134-
if (text !== "hello") {
135-
throw new Error("fail");
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();
136226
}
137227
});
138228
```
229+
230+
**Note:** For most applications, testing the server-side rendering is
231+
sufficient. Only test client-side interactivity if you have complex island logic
232+
that needs verification.

0 commit comments

Comments
 (0)