@shaferllc/keel 0.66.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,376 @@
1
+ # Testing
2
+
3
+ Test your app by **injecting requests** — no server, no port, no network — and
4
+ asserting on the response. `testClient()` wraps your app's Hono instance (which
5
+ already does fetch-style injection) with verb helpers and fluent assertions, the
6
+ way Fastify's `inject()` works.
7
+
8
+ ## The client
9
+
10
+ Build a client from an `Application` and fire requests:
11
+
12
+ ```ts
13
+ import { test } from "node:test";
14
+ import { Application, Router, json, testClient } from "@shaferllc/keel/core";
15
+
16
+ async function makeApp() {
17
+ const app = new Application();
18
+ await app.boot([], { discoverConfig: false, config: { app: {} } });
19
+ app.make(Router).get("/health", () => json({ ok: true }));
20
+ return app;
21
+ }
22
+
23
+ test("health check", async () => {
24
+ const client = testClient(await makeApp());
25
+ const res = await client.get("/health");
26
+ res.assertStatus(200).assertJson({ ok: true });
27
+ });
28
+ ```
29
+
30
+ `testClient()` accepts an **`Application`** (built through a fresh kernel), an
31
+ **`HttpKernel`** (use this if you need global middleware registered with
32
+ `kernel.use(...)`), or anything with a `request()` (a built Hono instance).
33
+
34
+ ## Requests
35
+
36
+ Verb helpers cover the common methods; `post` / `put` / `patch` take a body that's
37
+ JSON-encoded automatically:
38
+
39
+ ```ts
40
+ await client.get("/users");
41
+ await client.get("/users?active=true");
42
+ await client.post("/users", { email: "a@b.com", name: "Ada" }); // sends JSON
43
+ await client.put("/users/1", { name: "Grace" });
44
+ await client.delete("/users/1");
45
+
46
+ // full control — pass a RequestInit for headers, custom bodies, etc.
47
+ await client.request("/users", { method: "POST", headers: { authorization: "Bearer x" }, body });
48
+ ```
49
+
50
+ ## The response
51
+
52
+ Every call resolves to a `TestResponse`. The body is **pre-buffered**, so reads
53
+ are synchronous and repeatable (no "body already consumed"):
54
+
55
+ ```ts
56
+ const res = await client.get("/user");
57
+ res.status; // 200
58
+ res.header("content-type");
59
+ res.text(); // the raw body
60
+ res.json<User>(); // parsed (sync — the body is already read)
61
+ ```
62
+
63
+ ## Assertions
64
+
65
+ Assertions are chainable and throw a descriptive error (including the body) on
66
+ mismatch:
67
+
68
+ ```ts
69
+ res.assertStatus(201);
70
+ res.assertOk(); // any 2xx
71
+ res.assertJson({ id: 1, email }); // deep-equals the JSON body
72
+ res.assertText("pong");
73
+ res.assertHeader("content-type", "application/json");
74
+ res.assertRedirect("/login"); // 3xx (+ optional Location)
75
+
76
+ // chain them:
77
+ (await client.post("/users", body)).assertStatus(201).assertJson({ id: 2, ...body });
78
+ ```
79
+
80
+ ## Testing with middleware
81
+
82
+ When your test needs global middleware (sessions, request logging, auth), build
83
+ the kernel yourself and hand it to `testClient`:
84
+
85
+ ```ts
86
+ const app = await makeApp();
87
+ const kernel = new HttpKernel(app);
88
+ kernel.use(sessionMiddleware());
89
+ kernel.use(requestLogger());
90
+ const client = testClient(kernel);
91
+ ```
92
+
93
+ ## Authenticated requests
94
+
95
+ The client's `withX` methods return a **copy**, so a client configured once can be
96
+ reused without leaking into other tests:
97
+
98
+ ```ts
99
+ const authed = client.withToken("tok_123"); // Authorization: Bearer tok_123
100
+
101
+ await authed.get("/me");
102
+ await client.get("/me"); // still anonymous
103
+ ```
104
+
105
+ | Method | Sends |
106
+ |--------|-------|
107
+ | `withToken(token)` | `Authorization: Bearer <token>` |
108
+ | `withBasicAuth(user, pass)` | `Authorization: Basic <base64>` |
109
+ | `withHeader(name, value)` / `withHeaders({…})` | any header |
110
+ | `withCookie(name, value)` / `withCookies({…})` | a `Cookie` header |
111
+ | `acceptJson()` | `Accept: application/json` |
112
+
113
+ ## Forms and uploads
114
+
115
+ ```ts
116
+ await client.form("/login", { email: "a@b.com", password: "s3cret" }); // url-encoded
117
+ await client.multipart("/avatar", { file: new Blob([png]), name: "ada" }); // file upload
118
+ ```
119
+
120
+ ## More response assertions
121
+
122
+ ```ts
123
+ res.assertOk(); // 2xx
124
+ res.assertCreated(); // 201
125
+ res.assertNoContent(); // 204
126
+ res.assertUnauthorized(); // 401
127
+ res.assertForbidden(); // 403
128
+ res.assertNotFound(); // 404
129
+ res.assertUnprocessable(); // 422
130
+ res.assertServerError(); // 5xx
131
+ ```
132
+
133
+ **`assertJsonContains` is a subset match** — the one you usually want. It pins the
134
+ fields the test is about and ignores the rest, so adding a field to a response
135
+ doesn't break twenty tests:
136
+
137
+ ```ts
138
+ res.assertJsonContains({ user: { email: "a@b.com" } });
139
+ ```
140
+
141
+ `assertJson` still deep-equals the whole body, when that's what you mean.
142
+
143
+ ```ts
144
+ res.assertSee("Welcome back"); // body contains
145
+ res.assertDontSee("Sign up");
146
+
147
+ res.assertHeader("content-type", "application/json");
148
+ res.assertHeaderMissing("x-debug");
149
+
150
+ res.assertCookie("session"); // was set
151
+ res.assertCookie("session", "abc123"); // ...with this value
152
+ res.assertCookieMissing("admin");
153
+
154
+ res.dump(); // print status, headers, body — when you're stuck
155
+ ```
156
+
157
+ ### Validation
158
+
159
+ A failed `validate()` returns a 422 with per-field errors, so a test can assert on
160
+ the field rather than the message:
161
+
162
+ ```ts
163
+ const res = await client.post("/users", { email: "nope" });
164
+
165
+ res.assertValidationErrors("email", "password");
166
+ res.assertNoValidationError("name");
167
+ ```
168
+
169
+ ## Test doubles
170
+
171
+ Keel's fakes swap out a real backend for a recording one, so a test can assert
172
+ that something *would* have happened without it actually happening — no email
173
+ sent, no card charged, no file uploaded.
174
+
175
+ | Fake | Replaces | Assertions |
176
+ |------|----------|------------|
177
+ | [`fakeMail()`](./mail.md#in-tests) | the mailer | `assertSent`, `assertQueued`, … |
178
+ | [`fakeQueue()`](./queues.md#in-tests) | the queue | `assertPushed`, `assertNothingPushed`, … |
179
+ | [`fakeDisk()`](./storage.md#testing) | a storage disk | `assertExists`, `assertContents`, … |
180
+ | [`events().fake()`](./events.md#testing) | the emitter | `assertEmitted`, `assertNotEmitted`, … |
181
+ | [`hash.fake()`](./hashing.md) | PBKDF2 | — (just makes it fast) |
182
+
183
+ ```ts
184
+ const mailer = fakeMail();
185
+ const queue = fakeQueue();
186
+
187
+ await registerUser({ email: "ada@example.com" });
188
+
189
+ mailer.assertQueued((m) => m.subject === "Welcome");
190
+ queue.assertPushed(SendWelcome);
191
+ ```
192
+
193
+ For anything else, `swap()` replaces a container binding:
194
+
195
+ ```ts
196
+ swap(PaymentGateway, () => new FakeGateway());
197
+ ```
198
+
199
+ ### Spies
200
+
201
+ The smallest double: a function that records how it was called.
202
+
203
+ ```ts
204
+ import { spy, spyOn, restoreSpies } from "@shaferllc/keel/core";
205
+
206
+ const send = spy<[string], void>();
207
+ notify(send);
208
+
209
+ assert.equal(send.callCount, 1);
210
+ assert.ok(send.calledWith("hello"));
211
+ ```
212
+
213
+ `spyOn` replaces a method on an object. It **calls through** by default — so you're
214
+ observing, not stubbing — until you tell it otherwise:
215
+
216
+ ```ts
217
+ const charge = spyOn(gateway, "charge"); // still really charges
218
+ charge.returns(receipt); // now it doesn't
219
+
220
+ restoreSpies(); // put every spied method back
221
+ ```
222
+
223
+ ## Controlling time
224
+
225
+ Testing "this token expires in an hour" shouldn't take an hour.
226
+
227
+ ```ts
228
+ import { freezeTime, timeTravel, restoreTime } from "@shaferllc/keel/core";
229
+
230
+ freezeTime("2026-07-11T12:00:00Z");
231
+
232
+ const token = await jwt.sign({ sub: "1" }, { expiresIn: "1h" });
233
+ assert.ok(await jwt.verify(token)); // valid now
234
+
235
+ timeTravel(61 * 60 * 1000); // an hour and a minute later
236
+ assert.equal(await jwt.verify(token), null); // expired
237
+
238
+ restoreTime();
239
+ ```
240
+
241
+ `freezeTime()` mocks `Date` and `Date.now()`. It does **not** mock timers — a
242
+ `setTimeout` still fires on the real clock — and `new Date("2020-01-01")` still
243
+ parses normally. Only "what time is it *now*" is frozen.
244
+
245
+ ## Resetting state between tests
246
+
247
+ Keel's fakes, disks, queues, and cache are process-global, so one test can leak
248
+ into the next. `resetState()` puts it all back:
249
+
250
+ ```ts
251
+ import { resetState } from "@shaferllc/keel/core";
252
+
253
+ afterEach(() => resetState());
254
+ ```
255
+
256
+ It restores every fake (mail, queue, disk, hash), unfreezes the clock, drops event
257
+ listeners, empties the cache, and gives you a fresh lock store. It does **not**
258
+ touch the database.
259
+
260
+ For that, `truncate()`:
261
+
262
+ ```ts
263
+ afterEach(() => truncate("comments", "posts", "users")); // children before parents
264
+ ```
265
+
266
+ It deletes rows rather than rolling back a transaction, so it works on every driver
267
+ (D1, Postgres, libSQL) instead of only the ones with savepoints.
268
+
269
+ ## Database assertions
270
+
271
+ Assert against the database directly, rather than through an endpoint:
272
+
273
+ ```ts
274
+ import { assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount } from "@shaferllc/keel/core";
275
+
276
+ await client.post("/users", { email: "ada@example.com" });
277
+
278
+ await assertDatabaseHas("users", { email: "ada@example.com" });
279
+ await assertDatabaseHas("users", { active: 1 }, 1); // exactly one match
280
+ await assertDatabaseMissing("users", { email: "deleted@example.com" });
281
+ await assertDatabaseCount("users", 1);
282
+ await assertDatabaseEmpty("sessions");
283
+ ```
284
+
285
+ A failure tells you what it looked for and how many rows the table actually holds.
286
+
287
+ ## Console tests
288
+
289
+ Run a command in-process — no subprocess, so it's fast and you can assert on it:
290
+
291
+ ```ts
292
+ import { runCommand } from "@shaferllc/keel/core";
293
+ import { run } from "../bin/keel"; // your app's console entry point
294
+
295
+ const result = await runCommand(() => run(["node", "keel", "routes"]));
296
+
297
+ result
298
+ .assertSucceeded() // exit code 0
299
+ .assertOutputContains("GET /users")
300
+ .assertOutputMatches(/POST\s+\/users/);
301
+ ```
302
+
303
+ You pass the command **in**, because the console entry point belongs to your app —
304
+ it's the thing that knows how to build your application — not to the core. Anything
305
+ that prints and sets an exit code works, so it's equally good for testing a
306
+ function you wrote yourself.
307
+
308
+ `console.log`/`warn` are captured as stdout and `console.error` as stderr; a
309
+ command that *throws* is recorded as a failure rather than blowing up the test.
310
+
311
+ `assertFailed()`, `assertExitCode(n)`, and `assertErrorContains(text)` cover the
312
+ rest. `result.stdout`, `result.stderr`, and `result.exitCode` are there if you'd
313
+ rather assert by hand.
314
+
315
+ ## Browser tests
316
+
317
+ Keel doesn't ship a browser driver — that's [Playwright](https://playwright.dev)'s
318
+ job, and wrapping it would only put a thinner API in front of a better one.
319
+
320
+ The test client injects requests *without a server*, which is what makes it fast;
321
+ a browser needs a real one. Start the app on a port, point Playwright at it, and
322
+ tear it down:
323
+
324
+ ```ts
325
+ import { serve } from "@hono/node-server";
326
+ import { chromium } from "playwright";
327
+
328
+ const server = serve({ fetch: new HttpKernel(app).build().fetch, port: 3001 });
329
+ const browser = await chromium.launch();
330
+
331
+ const page = await browser.newPage();
332
+ await page.goto("http://localhost:3001/login");
333
+ await page.fill("[name=email]", "ada@example.com");
334
+ await page.click("button[type=submit]");
335
+ await page.waitForURL("**/dashboard");
336
+
337
+ await browser.close();
338
+ server.close();
339
+ ```
340
+
341
+ Everything else on this page — the fakes, `freezeTime`, `resetState`, the database
342
+ assertions — works the same in a browser test, because it's the same process.
343
+
344
+ ## API reference
345
+
346
+ ### `testClient(target)`
347
+
348
+ `testClient(target: Application | HttpKernel | { request(...) }): TestClient`
349
+
350
+ Builds a `TestClient`. An `Application` is built through a fresh `HttpKernel`; pass
351
+ a kernel to register global middleware first.
352
+
353
+ ### `TestClient`
354
+
355
+ | Method | Signature |
356
+ |--------|-----------|
357
+ | `get` / `delete` | `(path, init?) => Promise<TestResponse>` |
358
+ | `post` / `put` / `patch` | `(path, body?, init?) => Promise<TestResponse>` — body JSON-encoded |
359
+ | `request` | `(path, init?) => Promise<TestResponse>` — the low-level form |
360
+
361
+ ### `TestResponse`
362
+
363
+ Body pre-buffered; reads are synchronous.
364
+
365
+ | Member | Notes |
366
+ |--------|-------|
367
+ | `status` | the response status |
368
+ | `header(name)` | a response header, or `null` |
369
+ | `text()` / `json<T>()` | the body (raw / parsed) |
370
+ | `assertStatus(n)` / `assertOk()` | status is `n` / any 2xx |
371
+ | `assertJson(v)` | JSON body deep-equals `v` |
372
+ | `assertText(s)` / `assertHeader(n, v)` | exact body / header match |
373
+ | `assertRedirect(location?)` | 3xx, optionally to `location` |
374
+ | `raw` | the underlying `Response` |
375
+
376
+ All assertions return `this` (chainable) and throw on mismatch.