@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.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
package/docs/testing.md
ADDED
|
@@ -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.
|