@shaferllc/keel 0.66.0 → 0.68.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 +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -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/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -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 +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -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 +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -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/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -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/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/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -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/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -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/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 +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -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/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Templates
|
|
2
|
+
|
|
3
|
+
A string templating engine in the spirit of AdonisJS **Edge** — `{{ }}`
|
|
4
|
+
interpolation and `@`-prefixed tags for logic, includes, layouts, and
|
|
5
|
+
components. Reach for it when you want plain-text templates instead of (or
|
|
6
|
+
alongside) [JSX views](./views.md).
|
|
7
|
+
|
|
8
|
+
Unlike engines that compile a template to a function with `eval` /
|
|
9
|
+
`new Function`, Keel **interprets** templates against a small, safe expression
|
|
10
|
+
evaluator. No dynamic code generation, so the same templates run on Node **and**
|
|
11
|
+
on Cloudflare Workers (where `eval` is forbidden).
|
|
12
|
+
|
|
13
|
+
## Rendering
|
|
14
|
+
|
|
15
|
+
Register a template by name, then render it with a state object:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { templates, render } from "@shaferllc/keel/core";
|
|
19
|
+
|
|
20
|
+
templates().register("greeting", "Hello, {{ name }}!");
|
|
21
|
+
|
|
22
|
+
await render("greeting", { name: "Ada" }); // "Hello, Ada!"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`render()` returns a `Promise<string>` — hand it to a response or a [view](./views.md):
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { html, render } from "@shaferllc/keel/core";
|
|
29
|
+
return html(await render("greeting", { name: "Ada" }));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Register many at once (e.g. a Node loader reads `.html` files and passes them in):
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
templates().registerAll({
|
|
36
|
+
layout: await readFile("views/layout.html", "utf8"),
|
|
37
|
+
home: await readFile("views/home.html", "utf8"),
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Interpolation
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
{{ user.name }} {{-- escaped: HTML-safe --}}
|
|
45
|
+
{{{ post.body }}} {{-- raw: unescaped, for trusted HTML --}}
|
|
46
|
+
{{-- this is a comment; it renders nothing --}}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Escaped `{{ }}` is the default and encodes `& < > " '`. Use raw `{{{ }}}` only for
|
|
50
|
+
HTML you trust. A `null`/`undefined` value renders as an empty string.
|
|
51
|
+
|
|
52
|
+
## Expressions
|
|
53
|
+
|
|
54
|
+
Interpolation and tag conditions accept a practical subset of JavaScript —
|
|
55
|
+
enough for real templates, without `eval`:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
{{ user.name }} {{-- property + index access --}}
|
|
59
|
+
{{ items[0] }}
|
|
60
|
+
{{ title.toUpperCase() }} {{-- method calls --}}
|
|
61
|
+
{{ items.join(", ") }}
|
|
62
|
+
{{ price * qty }} {{-- + - * / % --}}
|
|
63
|
+
{{ n > 3 && n < 10 }} {{-- comparisons, && || ! ?? --}}
|
|
64
|
+
{{ admin ? "Admin" : "User" }} {{-- ternary --}}
|
|
65
|
+
{{ [1, 2, 3].length }} {{-- array / object literals --}}
|
|
66
|
+
{{ { role: "admin" }.role }}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Not supported: assignment, arrow functions, and other statement-level JS. Keep
|
|
70
|
+
logic in your controller and pass results in as state.
|
|
71
|
+
|
|
72
|
+
### Filters
|
|
73
|
+
|
|
74
|
+
Pipe a value through a filter with `|`:
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
{{ name | upper }}
|
|
78
|
+
{{ name | capitalize }}
|
|
79
|
+
{{ items | length }}
|
|
80
|
+
{{ price | currency("USD") }} {{-- filters take arguments --}}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Built-in filters: `upper`, `lower`, `capitalize`, `json`, `length`. Register your
|
|
84
|
+
own on the engine:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
templates().filter("currency", (v, code) =>
|
|
88
|
+
new Intl.NumberFormat("en-US", { style: "currency", currency: String(code) }).format(Number(v)),
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Conditionals
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
@if(user.admin)
|
|
96
|
+
<span>Admin</span>
|
|
97
|
+
@elseif(user.member)
|
|
98
|
+
<span>Member</span>
|
|
99
|
+
@else
|
|
100
|
+
<span>Guest</span>
|
|
101
|
+
@end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Loops
|
|
105
|
+
|
|
106
|
+
`@each` iterates arrays (or the values of an object). A `$loop` variable exposes
|
|
107
|
+
positional info, and you can capture the index:
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<ul>
|
|
111
|
+
@each(post in posts)
|
|
112
|
+
<li>{{ $loop.iteration }}. {{ post.title }}</li>
|
|
113
|
+
@end
|
|
114
|
+
</ul>
|
|
115
|
+
|
|
116
|
+
@each(item, i in items)
|
|
117
|
+
{{ i }}: {{ item }}
|
|
118
|
+
@end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`$loop` fields: `index` (0-based), `iteration` (1-based), `first`, `last`,
|
|
122
|
+
`count`, `even`, `odd`.
|
|
123
|
+
|
|
124
|
+
## Partials
|
|
125
|
+
|
|
126
|
+
Pull one template into another with `@include` — it shares the current state:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
{{-- list.html --}}
|
|
130
|
+
<ul>@each(item in items)@include("row")@end</ul>
|
|
131
|
+
|
|
132
|
+
{{-- row.html --}}
|
|
133
|
+
<li>{{ item }}</li>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`@includeIf(condition, "name")` includes only when the condition is truthy.
|
|
137
|
+
|
|
138
|
+
## Layouts
|
|
139
|
+
|
|
140
|
+
A page declares its layout and fills the layout's `@yield` slots with `@section`:
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
{{-- layout.html --}}
|
|
144
|
+
<!doctype html>
|
|
145
|
+
<title>@yield("title")Keel@end</title>
|
|
146
|
+
<body>@yield("body")@end</body>
|
|
147
|
+
|
|
148
|
+
{{-- page.html --}}
|
|
149
|
+
@layout("layout")
|
|
150
|
+
@section("title"){{ page.title }} · Keel@end
|
|
151
|
+
@section("body")<h1>{{ page.title }}</h1>@end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`@yield("name") … @end` renders the matching section, falling back to the content
|
|
155
|
+
between `@yield` and `@end` when the page defines no such section.
|
|
156
|
+
|
|
157
|
+
## Components
|
|
158
|
+
|
|
159
|
+
Components are reusable templates rendered with props and slots. The content
|
|
160
|
+
between `@component` and its `@end` becomes the `main` slot; `@slot("name")`
|
|
161
|
+
defines named slots. Inside the component, slots arrive as pre-rendered HTML
|
|
162
|
+
strings on a `slots` object:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
{{-- card.html --}}
|
|
166
|
+
<div class="card">
|
|
167
|
+
<header>{{{ slots.header }}}</header>
|
|
168
|
+
<main>{{{ slots.main }}}</main>
|
|
169
|
+
<footer>{{ title }}</footer>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{{-- usage --}}
|
|
173
|
+
@component("card", { title: "Welcome" })
|
|
174
|
+
@slot("header")<h2>Hi</h2>@end
|
|
175
|
+
<p>Body content goes to the main slot.</p>
|
|
176
|
+
@end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Props are any expression evaluating to an object; they become the component's
|
|
180
|
+
state (merged with globals).
|
|
181
|
+
|
|
182
|
+
## Globals
|
|
183
|
+
|
|
184
|
+
Expose values or helpers to every template:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
templates()
|
|
188
|
+
.global("appName", "Keel")
|
|
189
|
+
.global("asset", (path: string) => `/static/${path}`);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<title>{{ appName }}</title>
|
|
194
|
+
<img src="{{ asset('logo.svg') }}" />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Debugging
|
|
198
|
+
|
|
199
|
+
`@dump(value)` renders a `<pre>` of the value's JSON — handy while building a
|
|
200
|
+
template.
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
@dump(user)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Escaping & safety
|
|
207
|
+
|
|
208
|
+
- Escaped `{{ }}` encodes HTML; only use raw `{{{ }}}` for trusted content.
|
|
209
|
+
- The evaluator blocks access to `__proto__`, `constructor`, and `prototype`, so
|
|
210
|
+
template state can't be used to reach the prototype chain.
|
|
211
|
+
- There's no `eval`: a template can't execute arbitrary JavaScript, only the
|
|
212
|
+
expression subset above.
|
|
213
|
+
|
|
214
|
+
## API reference
|
|
215
|
+
|
|
216
|
+
### `templates()`
|
|
217
|
+
|
|
218
|
+
`templates(): TemplateEngine`
|
|
219
|
+
|
|
220
|
+
Returns the default engine — register templates, globals, and filters on it.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
templates().register("home", "…");
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Notes:** module-global and shared. Swap it with `setTemplateEngine()` (e.g. for
|
|
227
|
+
an isolated engine in a test).
|
|
228
|
+
|
|
229
|
+
### `render(name, state?)`
|
|
230
|
+
|
|
231
|
+
`render(name: string, state?: Record<string, unknown>): Promise<string>`
|
|
232
|
+
|
|
233
|
+
Renders a registered template on the default engine.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
await render("home", { user });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Notes:** throws if `name` isn't registered. Async because includes, components,
|
|
240
|
+
and layouts compose asynchronously.
|
|
241
|
+
|
|
242
|
+
### `setTemplateEngine(engine)`
|
|
243
|
+
|
|
244
|
+
`setTemplateEngine(engine: TemplateEngine): TemplateEngine`
|
|
245
|
+
|
|
246
|
+
Replaces the default engine and returns it.
|
|
247
|
+
|
|
248
|
+
**Notes:** the last call wins; useful to reset state between tests.
|
|
249
|
+
|
|
250
|
+
### `escapeHtml(value)`
|
|
251
|
+
|
|
252
|
+
`escapeHtml(value: unknown): string`
|
|
253
|
+
|
|
254
|
+
HTML-escapes a value (`& < > " '`); `null`/`undefined` become `""`. This is what
|
|
255
|
+
`{{ }}` uses internally.
|
|
256
|
+
|
|
257
|
+
### `TemplateEngine`
|
|
258
|
+
|
|
259
|
+
The engine class. Construct your own for isolation, or use `templates()`.
|
|
260
|
+
|
|
261
|
+
#### `register(name, source)`
|
|
262
|
+
|
|
263
|
+
`register(name: string, source: string): this`
|
|
264
|
+
|
|
265
|
+
Parses and registers a template. Chainable.
|
|
266
|
+
|
|
267
|
+
**Notes:** parsing happens here, so a malformed template throws at registration,
|
|
268
|
+
not at render.
|
|
269
|
+
|
|
270
|
+
#### `registerAll(sources)`
|
|
271
|
+
|
|
272
|
+
`registerAll(sources: Record<string, string>): this`
|
|
273
|
+
|
|
274
|
+
Registers many templates at once from a `name → source` map.
|
|
275
|
+
|
|
276
|
+
#### `has(name)`
|
|
277
|
+
|
|
278
|
+
`has(name: string): boolean`
|
|
279
|
+
|
|
280
|
+
Whether a template is registered.
|
|
281
|
+
|
|
282
|
+
#### `global(name, value)`
|
|
283
|
+
|
|
284
|
+
`global(name: string, value: unknown): this`
|
|
285
|
+
|
|
286
|
+
Exposes a value or function to every template as a global variable.
|
|
287
|
+
|
|
288
|
+
#### `filter(name, fn)`
|
|
289
|
+
|
|
290
|
+
`filter(name: string, fn: Filter): this`
|
|
291
|
+
|
|
292
|
+
Registers a `{{ value | name }}` filter. `Filter` is
|
|
293
|
+
`(value: unknown, ...args: unknown[]) => unknown`.
|
|
294
|
+
|
|
295
|
+
#### `render(name, state?)`
|
|
296
|
+
|
|
297
|
+
`render(name: string, state?: Record<string, unknown>): Promise<string>`
|
|
298
|
+
|
|
299
|
+
Renders a registered template. Throws for an unknown template, tag, or filter.
|
|
300
|
+
|
|
301
|
+
### Interfaces & types
|
|
302
|
+
|
|
303
|
+
#### `Filter`
|
|
304
|
+
|
|
305
|
+
`type Filter = (value: unknown, ...args: unknown[]) => unknown`
|
|
306
|
+
|
|
307
|
+
A pipe filter: receives the piped value plus any `filter(arg)` arguments, returns
|
|
308
|
+
the transformed value.
|
|
309
|
+
|
|
310
|
+
#### `RenderContext`
|
|
311
|
+
|
|
312
|
+
`interface RenderContext { sections: Record<string, string>; slots: Record<string, string> }`
|
|
313
|
+
|
|
314
|
+
Internal per-render state threaded through layouts and components — you won't
|
|
315
|
+
construct it directly.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
## API reference
|
|
94
|
+
|
|
95
|
+
### `testClient(target)`
|
|
96
|
+
|
|
97
|
+
`testClient(target: Application | HttpKernel | { request(...) }): TestClient`
|
|
98
|
+
|
|
99
|
+
Builds a `TestClient`. An `Application` is built through a fresh `HttpKernel`; pass
|
|
100
|
+
a kernel to register global middleware first.
|
|
101
|
+
|
|
102
|
+
### `TestClient`
|
|
103
|
+
|
|
104
|
+
| Method | Signature |
|
|
105
|
+
|--------|-----------|
|
|
106
|
+
| `get` / `delete` | `(path, init?) => Promise<TestResponse>` |
|
|
107
|
+
| `post` / `put` / `patch` | `(path, body?, init?) => Promise<TestResponse>` — body JSON-encoded |
|
|
108
|
+
| `request` | `(path, init?) => Promise<TestResponse>` — the low-level form |
|
|
109
|
+
|
|
110
|
+
### `TestResponse`
|
|
111
|
+
|
|
112
|
+
Body pre-buffered; reads are synchronous.
|
|
113
|
+
|
|
114
|
+
| Member | Notes |
|
|
115
|
+
|--------|-------|
|
|
116
|
+
| `status` | the response status |
|
|
117
|
+
| `header(name)` | a response header, or `null` |
|
|
118
|
+
| `text()` / `json<T>()` | the body (raw / parsed) |
|
|
119
|
+
| `assertStatus(n)` / `assertOk()` | status is `n` / any 2xx |
|
|
120
|
+
| `assertJson(v)` | JSON body deep-equals `v` |
|
|
121
|
+
| `assertText(s)` / `assertHeader(n, v)` | exact body / header match |
|
|
122
|
+
| `assertRedirect(location?)` | 3xx, optionally to `location` |
|
|
123
|
+
| `raw` | the underlying `Response` |
|
|
124
|
+
|
|
125
|
+
All assertions return `this` (chainable) and throw on mismatch.
|