davaux 0.8.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/BASELINE.md +169 -0
- package/CLAUDE.md +518 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ROADMAP.md +198 -0
- package/build.mjs +101 -0
- package/client/control.ts +247 -0
- package/client/hydrate.ts +37 -0
- package/client/index.ts +19 -0
- package/client/jsx-runtime.ts +209 -0
- package/client/resource.ts +122 -0
- package/client/signal.ts +211 -0
- package/client/store.ts +110 -0
- package/client/useHead.ts +63 -0
- package/dist/build/config.d.ts +3 -0
- package/dist/build/config.d.ts.map +1 -0
- package/dist/build/config.js +38 -0
- package/dist/build/config.js.map +7 -0
- package/dist/build/index.d.ts +2 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +13 -0
- package/dist/build/index.js.map +7 -0
- package/dist/build/plugins.d.ts +7 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +85 -0
- package/dist/build/plugins.js.map +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/control.d.ts +49 -0
- package/dist/client/control.d.ts.map +1 -0
- package/dist/client/control.js +154 -0
- package/dist/client/control.js.map +7 -0
- package/dist/client/hydrate.d.ts +7 -0
- package/dist/client/hydrate.d.ts.map +1 -0
- package/dist/client/hydrate.js +23 -0
- package/dist/client/hydrate.js.map +7 -0
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +32 -0
- package/dist/client/index.js.map +7 -0
- package/dist/client/jsx-runtime.d.ts +40 -0
- package/dist/client/jsx-runtime.d.ts.map +1 -0
- package/dist/client/jsx-runtime.js +139 -0
- package/dist/client/jsx-runtime.js.map +7 -0
- package/dist/client/resource.d.ts +31 -0
- package/dist/client/resource.d.ts.map +1 -0
- package/dist/client/resource.js +64 -0
- package/dist/client/resource.js.map +7 -0
- package/dist/client/signal.d.ts +90 -0
- package/dist/client/signal.d.ts.map +1 -0
- package/dist/client/signal.js +115 -0
- package/dist/client/signal.js.map +7 -0
- package/dist/client/store.d.ts +26 -0
- package/dist/client/store.d.ts.map +1 -0
- package/dist/client/store.js +63 -0
- package/dist/client/store.js.map +7 -0
- package/dist/client/useHead.d.ts +28 -0
- package/dist/client/useHead.d.ts.map +1 -0
- package/dist/client/useHead.js +33 -0
- package/dist/client/useHead.js.map +7 -0
- package/dist/config.d.ts +182 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +7 -0
- package/dist/create-multisite.d.ts +2 -0
- package/dist/create-multisite.d.ts.map +1 -0
- package/dist/create-multisite.js +291 -0
- package/dist/create-multisite.js.map +7 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +179 -0
- package/dist/create.js.map +7 -0
- package/dist/dev/blueprints.d.ts +11 -0
- package/dist/dev/blueprints.d.ts.map +1 -0
- package/dist/dev/blueprints.js +65 -0
- package/dist/dev/blueprints.js.map +7 -0
- package/dist/dev/components.d.ts +19 -0
- package/dist/dev/components.d.ts.map +1 -0
- package/dist/dev/components.js +87 -0
- package/dist/dev/components.js.map +7 -0
- package/dist/dev/insert.d.ts +11 -0
- package/dist/dev/insert.d.ts.map +1 -0
- package/dist/dev/insert.js +160 -0
- package/dist/dev/insert.js.map +7 -0
- package/dist/dev/remove.d.ts +53 -0
- package/dist/dev/remove.d.ts.map +1 -0
- package/dist/dev/remove.js +518 -0
- package/dist/dev/remove.js.map +7 -0
- package/dist/dev/watch.d.ts +26 -0
- package/dist/dev/watch.d.ts.map +1 -0
- package/dist/dev/watch.js +2905 -0
- package/dist/dev/watch.js.map +7 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +7 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +191 -0
- package/dist/generate.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/island.d.ts +24 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +15 -0
- package/dist/island.js.map +7 -0
- package/dist/jsx-runtime.d.ts +406 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +90 -0
- package/dist/jsx-runtime.js.map +7 -0
- package/dist/link.d.ts +27 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +29 -0
- package/dist/link.js.map +7 -0
- package/dist/oml/fragment.d.ts +16 -0
- package/dist/oml/fragment.d.ts.map +1 -0
- package/dist/oml/fragment.js +26 -0
- package/dist/oml/fragment.js.map +7 -0
- package/dist/oml/index.d.ts +11 -0
- package/dist/oml/index.d.ts.map +1 -0
- package/dist/oml/index.js +21 -0
- package/dist/oml/index.js.map +7 -0
- package/dist/oml/jsx-runtime.d.ts +34 -0
- package/dist/oml/jsx-runtime.d.ts.map +1 -0
- package/dist/oml/jsx-runtime.js +59 -0
- package/dist/oml/jsx-runtime.js.map +7 -0
- package/dist/oml/jsx.d.ts +14 -0
- package/dist/oml/jsx.d.ts.map +1 -0
- package/dist/oml/jsx.js +96 -0
- package/dist/oml/jsx.js.map +7 -0
- package/dist/oml/page.d.ts +7 -0
- package/dist/oml/page.d.ts.map +1 -0
- package/dist/oml/page.js +6 -0
- package/dist/oml/page.js.map +7 -0
- package/dist/oml/render.d.ts +13 -0
- package/dist/oml/render.d.ts.map +1 -0
- package/dist/oml/render.js +117 -0
- package/dist/oml/render.js.map +7 -0
- package/dist/oml/types.d.ts +79 -0
- package/dist/oml/types.d.ts.map +1 -0
- package/dist/oml/types.js +64 -0
- package/dist/oml/types.js.map +7 -0
- package/dist/router/handler.d.ts +53 -0
- package/dist/router/handler.d.ts.map +1 -0
- package/dist/router/handler.js +342 -0
- package/dist/router/handler.js.map +7 -0
- package/dist/router/matcher.d.ts +21 -0
- package/dist/router/matcher.d.ts.map +1 -0
- package/dist/router/matcher.js +28 -0
- package/dist/router/matcher.js.map +7 -0
- package/dist/router/scanner.d.ts +17 -0
- package/dist/router/scanner.d.ts.map +1 -0
- package/dist/router/scanner.js +197 -0
- package/dist/router/scanner.js.map +7 -0
- package/dist/server/index.d.ts +23 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +29 -0
- package/dist/server/index.js.map +7 -0
- package/dist/signal.d.ts +15 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +29 -0
- package/dist/signal.js.map +7 -0
- package/dist/ssg.d.ts +45 -0
- package/dist/ssg.d.ts.map +1 -0
- package/dist/ssg.js +175 -0
- package/dist/ssg.js.map +7 -0
- package/dist/test/actions.test.d.ts +2 -0
- package/dist/test/actions.test.d.ts.map +1 -0
- package/dist/test/body-limits.test.d.ts +2 -0
- package/dist/test/body-limits.test.d.ts.map +1 -0
- package/dist/test/errors.test.d.ts +2 -0
- package/dist/test/errors.test.d.ts.map +1 -0
- package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
- package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_error.d.ts +3 -0
- package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_global.d.ts +3 -0
- package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/action.page.d.ts +6 -0
- package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
- package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/state.page.d.ts +3 -0
- package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
- package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
- package/dist/test/helpers.d.ts +37 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/layouts.test.d.ts +2 -0
- package/dist/test/layouts.test.d.ts.map +1 -0
- package/dist/test/middleware.test.d.ts +2 -0
- package/dist/test/middleware.test.d.ts.map +1 -0
- package/dist/test/multipart.test.d.ts +2 -0
- package/dist/test/multipart.test.d.ts.map +1 -0
- package/dist/test/oml-routing.test.d.ts +2 -0
- package/dist/test/oml-routing.test.d.ts.map +1 -0
- package/dist/test/oml.test.d.ts +2 -0
- package/dist/test/oml.test.d.ts.map +1 -0
- package/dist/test/redirects.test.d.ts +2 -0
- package/dist/test/redirects.test.d.ts.map +1 -0
- package/dist/test/routing.test.d.ts +2 -0
- package/dist/test/routing.test.d.ts.map +1 -0
- package/dist/test/ssg.test.d.ts +2 -0
- package/dist/test/ssg.test.d.ts.map +1 -0
- package/dist/test/web-response.test.d.ts +2 -0
- package/dist/test/web-response.test.d.ts.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +292 -0
- package/dist/types.js.map +7 -0
- package/package.json +103 -0
- package/pka.config.json +32 -0
- package/src/build/config.ts +42 -0
- package/src/build/index.ts +6 -0
- package/src/build/plugins.ts +118 -0
- package/src/cli.ts +502 -0
- package/src/config.ts +197 -0
- package/src/create-multisite.ts +310 -0
- package/src/create.ts +194 -0
- package/src/dev/blueprints.ts +75 -0
- package/src/dev/components.ts +108 -0
- package/src/dev/insert.ts +221 -0
- package/src/dev/remove.ts +677 -0
- package/src/dev/watch.ts +3098 -0
- package/src/env.d.ts +5 -0
- package/src/errors.ts +64 -0
- package/src/generate.ts +228 -0
- package/src/index.ts +67 -0
- package/src/island.ts +47 -0
- package/src/jsx-runtime.d.ts +408 -0
- package/src/jsx-runtime.d.ts.map +1 -0
- package/src/jsx-runtime.ts +536 -0
- package/src/link.ts +49 -0
- package/src/oml/fragment.ts +54 -0
- package/src/oml/index.ts +21 -0
- package/src/oml/jsx-runtime.ts +121 -0
- package/src/oml/jsx.ts +151 -0
- package/src/oml/page.ts +13 -0
- package/src/oml/render.ts +181 -0
- package/src/oml/types.ts +159 -0
- package/src/router/handler.ts +515 -0
- package/src/router/matcher.ts +52 -0
- package/src/router/scanner.ts +272 -0
- package/src/server/index.ts +49 -0
- package/src/signal.ts +39 -0
- package/src/ssg.ts +253 -0
- package/src/test/actions.test.ts +40 -0
- package/src/test/body-limits.test.ts +83 -0
- package/src/test/errors.test.ts +53 -0
- package/src/test/fixtures/routes/[id].page.ts +3 -0
- package/src/test/fixtures/routes/_error.ts +6 -0
- package/src/test/fixtures/routes/_global.ts +8 -0
- package/src/test/fixtures/routes/_layout-template.ts +7 -0
- package/src/test/fixtures/routes/_layout.ts +7 -0
- package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
- package/src/test/fixtures/routes/_middleware.ts +8 -0
- package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
- package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
- package/src/test/fixtures/routes/about.page.ts +6 -0
- package/src/test/fixtures/routes/action.page.ts +11 -0
- package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
- package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
- package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
- package/src/test/fixtures/routes/api/upload.post.ts +14 -0
- package/src/test/fixtures/routes/api/users.get.ts +3 -0
- package/src/test/fixtures/routes/api/xml.get.ts +5 -0
- package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
- package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
- package/src/test/fixtures/routes/index.page.ts +3 -0
- package/src/test/fixtures/routes/oml.page.ts +7 -0
- package/src/test/fixtures/routes/redirect.page.ts +3 -0
- package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
- package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
- package/src/test/fixtures/routes/state.page.ts +4 -0
- package/src/test/fixtures/routes/throw.page.ts +5 -0
- package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
- package/src/test/helpers.ts +132 -0
- package/src/test/layouts.test.ts +76 -0
- package/src/test/middleware.test.ts +69 -0
- package/src/test/multipart.test.ts +91 -0
- package/src/test/oml-routing.test.ts +59 -0
- package/src/test/oml.test.ts +429 -0
- package/src/test/redirects.test.ts +32 -0
- package/src/test/routing.test.ts +118 -0
- package/src/test/ssg.test.ts +273 -0
- package/src/test/web-response.test.ts +33 -0
- package/src/types.ts +670 -0
- package/tsconfig.client.json +17 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
function buildMultipart(
|
|
6
|
+
boundary: string,
|
|
7
|
+
parts: Array<{ name: string; value: string; filename?: string; mimetype?: string }>,
|
|
8
|
+
): string {
|
|
9
|
+
let body = ''
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
body += `--${boundary}\r\n`
|
|
12
|
+
if (part.filename !== undefined) {
|
|
13
|
+
body += `Content-Disposition: form-data; name="${part.name}"; filename="${part.filename}"\r\n`
|
|
14
|
+
body += `Content-Type: ${part.mimetype ?? 'application/octet-stream'}\r\n`
|
|
15
|
+
} else {
|
|
16
|
+
body += `Content-Disposition: form-data; name="${part.name}"\r\n`
|
|
17
|
+
}
|
|
18
|
+
body += `\r\n${part.value}\r\n`
|
|
19
|
+
}
|
|
20
|
+
body += `--${boundary}--\r\n`
|
|
21
|
+
return body
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('ctx.multipart()', () => {
|
|
25
|
+
const app = makeApp({ routes: [route('api/upload.post.ts', '/api/upload', 'post')] })
|
|
26
|
+
const boundary = 'TestBoundary123'
|
|
27
|
+
const ct = `multipart/form-data; boundary=${boundary}`
|
|
28
|
+
|
|
29
|
+
test('parses text fields', async () => {
|
|
30
|
+
const body = buildMultipart(boundary, [
|
|
31
|
+
{ name: 'title', value: 'Hello World' },
|
|
32
|
+
{ name: 'body', value: 'Some content here' },
|
|
33
|
+
])
|
|
34
|
+
const { status, body: res } = await request(app, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
url: '/api/upload',
|
|
37
|
+
headers: { 'content-type': ct },
|
|
38
|
+
body,
|
|
39
|
+
})
|
|
40
|
+
assert.equal(status, 200)
|
|
41
|
+
const data = JSON.parse(res)
|
|
42
|
+
assert.equal(data.fields.title, 'Hello World')
|
|
43
|
+
assert.equal(data.fields.body, 'Some content here')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('parses file uploads', async () => {
|
|
47
|
+
const body = buildMultipart(boundary, [
|
|
48
|
+
{ name: 'avatar', value: 'fake-png-bytes', filename: 'photo.png', mimetype: 'image/png' },
|
|
49
|
+
])
|
|
50
|
+
const { status, body: res } = await request(app, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
url: '/api/upload',
|
|
53
|
+
headers: { 'content-type': ct },
|
|
54
|
+
body,
|
|
55
|
+
})
|
|
56
|
+
assert.equal(status, 200)
|
|
57
|
+
const data = JSON.parse(res)
|
|
58
|
+
assert.equal(data.files.avatar.filename, 'photo.png')
|
|
59
|
+
assert.equal(data.files.avatar.mimetype, 'image/png')
|
|
60
|
+
assert.equal(data.files.avatar.size, Buffer.from('fake-png-bytes').length)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('parses mixed fields and files', async () => {
|
|
64
|
+
const body = buildMultipart(boundary, [
|
|
65
|
+
{ name: 'title', value: 'My Post' },
|
|
66
|
+
{ name: 'image', value: 'imgdata', filename: 'hero.jpg', mimetype: 'image/jpeg' },
|
|
67
|
+
])
|
|
68
|
+
const { status, body: res } = await request(app, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
url: '/api/upload',
|
|
71
|
+
headers: { 'content-type': ct },
|
|
72
|
+
body,
|
|
73
|
+
})
|
|
74
|
+
assert.equal(status, 200)
|
|
75
|
+
const data = JSON.parse(res)
|
|
76
|
+
assert.equal(data.fields.title, 'My Post')
|
|
77
|
+
assert.equal(data.files.image.filename, 'hero.jpg')
|
|
78
|
+
assert.equal(data.files.image.mimetype, 'image/jpeg')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('missing Content-Type boundary returns 500', async () => {
|
|
82
|
+
const body = buildMultipart(boundary, [{ name: 'x', value: 'y' }])
|
|
83
|
+
const { status } = await request(app, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
url: '/api/upload',
|
|
86
|
+
headers: { 'content-type': 'multipart/form-data' },
|
|
87
|
+
body,
|
|
88
|
+
})
|
|
89
|
+
assert.equal(status, 500)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
describe('OML page routing', () => {
|
|
6
|
+
it('renders an OML page handler to HTML', async () => {
|
|
7
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] })
|
|
8
|
+
const { status, body } = await request(app, { url: '/' })
|
|
9
|
+
assert.equal(status, 200)
|
|
10
|
+
assert.ok(body.includes('<div class="oml-page">Hello, World!</div>'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('OML page reads query params at render time', async () => {
|
|
14
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] })
|
|
15
|
+
const { body } = await request(app, { url: '/?name=Davaux' })
|
|
16
|
+
assert.ok(body.includes('Hello, Davaux!'))
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('OML page result is cached in production (isDev=false)', async () => {
|
|
20
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] }, undefined, { include: ['/'] })
|
|
21
|
+
await request(app, { url: '/' })
|
|
22
|
+
assert.equal(app.omlCache.size, 1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('OML page is not cached in dev mode', async () => {
|
|
26
|
+
const { buildApp, dispatch } = await import('../router/handler.js')
|
|
27
|
+
const { makeScan, mockReq, mockRes } = await import('./helpers.js')
|
|
28
|
+
const app = buildApp(makeScan({ routes: [route('oml.page.ts', '/')] }), true)
|
|
29
|
+
const req = mockReq({ url: '/' })
|
|
30
|
+
const { res } = mockRes()
|
|
31
|
+
await dispatch(req, res, app)
|
|
32
|
+
assert.equal(app.omlCache.size, 0)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('second request returns cached HTML without re-rendering', async () => {
|
|
36
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] }, undefined, { include: ['/'] })
|
|
37
|
+
const first = await request(app, { url: '/' })
|
|
38
|
+
assert.equal(app.omlCache.size, 1)
|
|
39
|
+
const second = await request(app, { url: '/' })
|
|
40
|
+
assert.equal(second.body, first.body)
|
|
41
|
+
assert.equal(app.omlCache.size, 1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('cache entry stores both the OmlNode and pre-rendered HTML', async () => {
|
|
45
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] }, undefined, { include: ['/'] })
|
|
46
|
+
await request(app, { url: '/' })
|
|
47
|
+
const [entry] = app.omlCache.values()
|
|
48
|
+
assert.ok(entry.node !== undefined)
|
|
49
|
+
assert.equal(typeof entry.html, 'string')
|
|
50
|
+
assert.ok(entry.html.includes('Hello, World!'))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('different query strings produce separate cache entries', async () => {
|
|
54
|
+
const app = makeApp({ routes: [route('oml.page.ts', '/')] }, undefined, { include: ['/'] })
|
|
55
|
+
await request(app, { url: '/?name=Alice' })
|
|
56
|
+
await request(app, { url: '/?name=Bob' })
|
|
57
|
+
assert.equal(app.omlCache.size, 2)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { clearAllFragments, defineFragment } from '../oml/fragment.js'
|
|
4
|
+
import { blueprintToFile, renderToJsx } from '../oml/jsx.js'
|
|
5
|
+
import { Fragment, jsx } from '../oml/jsx-runtime.js'
|
|
6
|
+
import { renderToHtml } from '../oml/render.js'
|
|
7
|
+
import type { OmlNode } from '../oml/types.js'
|
|
8
|
+
import { parseOml, parseOmlBlueprint } from '../oml/types.js'
|
|
9
|
+
|
|
10
|
+
// ─── jsx factory ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe('OML jsx factory', () => {
|
|
13
|
+
it('produces an element node for HTML tags', async () => {
|
|
14
|
+
const node = await jsx('div', { class: 'foo' })
|
|
15
|
+
assert.deepEqual(node, {
|
|
16
|
+
type: 'element',
|
|
17
|
+
tag: 'div',
|
|
18
|
+
id: undefined,
|
|
19
|
+
props: { class: 'foo' },
|
|
20
|
+
children: [],
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('wraps text children as #text nodes', async () => {
|
|
25
|
+
const node = await jsx('p', { children: 'hello' })
|
|
26
|
+
assert.deepEqual(node, {
|
|
27
|
+
type: 'element',
|
|
28
|
+
tag: 'p',
|
|
29
|
+
id: undefined,
|
|
30
|
+
props: {},
|
|
31
|
+
children: [{ type: '#text', value: 'hello' }],
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('resolves nested element children', async () => {
|
|
36
|
+
const child = jsx('span', { children: 'world' })
|
|
37
|
+
const node = await jsx('div', { children: [child] })
|
|
38
|
+
assert.equal((node as { type: string }).type, 'element')
|
|
39
|
+
const el = node as { children: OmlNode[] }
|
|
40
|
+
assert.equal(el.children.length, 1)
|
|
41
|
+
assert.deepEqual(el.children[0], {
|
|
42
|
+
type: 'element',
|
|
43
|
+
tag: 'span',
|
|
44
|
+
id: undefined,
|
|
45
|
+
props: {},
|
|
46
|
+
children: [{ type: '#text', value: 'world' }],
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('produces a #fragment node for Fragment type', async () => {
|
|
51
|
+
const node = await jsx(Fragment, { children: ['a', 'b'] })
|
|
52
|
+
assert.deepEqual(node, {
|
|
53
|
+
type: '#fragment',
|
|
54
|
+
children: [
|
|
55
|
+
{ type: '#text', value: 'a' },
|
|
56
|
+
{ type: '#text', value: 'b' },
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('produces a #component node for function components', async () => {
|
|
62
|
+
async function Card(props: { title?: unknown; [k: string]: unknown }) {
|
|
63
|
+
return jsx('div', { children: props.title as string })
|
|
64
|
+
}
|
|
65
|
+
const node = await jsx(Card, { title: 'Hello' })
|
|
66
|
+
assert.equal((node as { type: string }).type, '#component')
|
|
67
|
+
const comp = node as { name: string; props: Record<string, unknown>; output: OmlNode }
|
|
68
|
+
assert.equal(comp.name, 'Card')
|
|
69
|
+
assert.deepEqual(comp.props, { title: 'Hello' })
|
|
70
|
+
assert.deepEqual(comp.output, {
|
|
71
|
+
type: 'element',
|
|
72
|
+
tag: 'div',
|
|
73
|
+
id: undefined,
|
|
74
|
+
props: {},
|
|
75
|
+
children: [{ type: '#text', value: 'Hello' }],
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('assigns id from key prop', async () => {
|
|
80
|
+
const node = await jsx('li', {}, 'item-1')
|
|
81
|
+
assert.equal((node as { id: string }).id, 'item-1')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('omits null and boolean children', async () => {
|
|
85
|
+
const node = await jsx('div', { children: [null, false, 'visible'] })
|
|
86
|
+
const el = node as { children: OmlNode[] }
|
|
87
|
+
assert.equal(el.children.length, 1)
|
|
88
|
+
assert.deepEqual(el.children[0], { type: '#text', value: 'visible' })
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// ─── renderToHtml ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe('OML renderToHtml', () => {
|
|
95
|
+
it('renders a simple element', () => {
|
|
96
|
+
const node: OmlNode = {
|
|
97
|
+
type: 'element',
|
|
98
|
+
tag: 'div',
|
|
99
|
+
props: { class: 'box' },
|
|
100
|
+
children: [{ type: '#text', value: 'hi' }],
|
|
101
|
+
}
|
|
102
|
+
assert.equal(renderToHtml(node), '<div class="box">hi</div>')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('renders void elements without closing tag', () => {
|
|
106
|
+
const node: OmlNode = { type: 'element', tag: 'br', props: {}, children: [] }
|
|
107
|
+
assert.equal(renderToHtml(node), '<br>')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('escapes text content', () => {
|
|
111
|
+
const node: OmlNode = { type: '#text', value: '<script>alert("xss")</script>' }
|
|
112
|
+
assert.equal(renderToHtml(node), '<script>alert("xss")</script>')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('renders null as empty string', () => {
|
|
116
|
+
assert.equal(renderToHtml(null), '')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('renders #fragment by joining children', () => {
|
|
120
|
+
const node: OmlNode = {
|
|
121
|
+
type: '#fragment',
|
|
122
|
+
children: [
|
|
123
|
+
{ type: '#text', value: 'a' },
|
|
124
|
+
{ type: '#text', value: 'b' },
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
assert.equal(renderToHtml(node), 'ab')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('renders #component by delegating to its output node', () => {
|
|
131
|
+
const node: OmlNode = {
|
|
132
|
+
type: '#component',
|
|
133
|
+
name: 'Badge',
|
|
134
|
+
props: {},
|
|
135
|
+
output: { type: 'element', tag: 'span', props: { class: 'badge' }, children: [] },
|
|
136
|
+
}
|
|
137
|
+
assert.equal(renderToHtml(node), '<span class="badge"></span>')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('handles dangerouslySetInnerHTML', () => {
|
|
141
|
+
const node: OmlNode = {
|
|
142
|
+
type: 'element',
|
|
143
|
+
tag: 'div',
|
|
144
|
+
props: { dangerouslySetInnerHTML: { __html: '<b>raw</b>' } },
|
|
145
|
+
children: [],
|
|
146
|
+
}
|
|
147
|
+
assert.equal(renderToHtml(node), '<div><b>raw</b></div>')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('maps className to class attribute', async () => {
|
|
151
|
+
const node = await jsx('div', { className: 'foo' })
|
|
152
|
+
assert.equal(renderToHtml(node), '<div class="foo"></div>')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('skips event handler props', async () => {
|
|
156
|
+
const node = await jsx('button', { onClick: () => {} })
|
|
157
|
+
assert.equal(renderToHtml(node), '<button></button>')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ─── round-trip ───────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
describe('OML round-trip: jsx → renderToHtml', () => {
|
|
164
|
+
it('matches equivalent HTML', async () => {
|
|
165
|
+
const node = await jsx('section', {
|
|
166
|
+
class: 'card',
|
|
167
|
+
children: [jsx('h1', { children: 'Title' }), jsx('p', { children: 'Body text' })],
|
|
168
|
+
})
|
|
169
|
+
assert.equal(
|
|
170
|
+
renderToHtml(node),
|
|
171
|
+
'<section class="card"><h1>Title</h1><p>Body text</p></section>',
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// ─── parseOml ─────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe('parseOml', () => {
|
|
179
|
+
it('parses a valid element node from JSON', () => {
|
|
180
|
+
const json = JSON.parse(
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
type: 'element',
|
|
183
|
+
tag: 'div',
|
|
184
|
+
props: { class: 'x' },
|
|
185
|
+
children: [],
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
const node = parseOml(json)
|
|
189
|
+
assert.equal((node as { type: string }).type, 'element')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('parses null as null', () => {
|
|
193
|
+
assert.equal(parseOml(null), null)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('throws on unknown node type', () => {
|
|
197
|
+
assert.throws(() => parseOml({ type: 'unknown' }), /unknown node type/)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('throws on non-object input', () => {
|
|
201
|
+
assert.throws(() => parseOml('bad'), /expected node object/)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('parseOmlBlueprint', () => {
|
|
206
|
+
it('parses a valid blueprint', () => {
|
|
207
|
+
const json = JSON.parse(
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
id: 'bp:button',
|
|
210
|
+
name: 'Button',
|
|
211
|
+
props: { label: { type: 'string', required: true } },
|
|
212
|
+
output: { type: 'element', tag: 'button', props: {}, children: [] },
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
const bp = parseOmlBlueprint(json)
|
|
216
|
+
assert.equal(bp.id, 'bp:button')
|
|
217
|
+
assert.equal(bp.name, 'Button')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('throws if id is missing', () => {
|
|
221
|
+
assert.throws(() => parseOmlBlueprint({ name: 'X', output: null }), /missing id/)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('preserves imports field', () => {
|
|
225
|
+
const bp = parseOmlBlueprint({
|
|
226
|
+
id: 'bp:card',
|
|
227
|
+
name: 'Card',
|
|
228
|
+
props: {},
|
|
229
|
+
output: null,
|
|
230
|
+
imports: { Badge: './Badge.js' },
|
|
231
|
+
})
|
|
232
|
+
assert.deepEqual(bp.imports, { Badge: './Badge.js' })
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// ─── renderToJsx ──────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
describe('renderToJsx', () => {
|
|
239
|
+
it('renders a simple element', () => {
|
|
240
|
+
const node: OmlNode = {
|
|
241
|
+
type: 'element',
|
|
242
|
+
tag: 'div',
|
|
243
|
+
props: { className: 'box' },
|
|
244
|
+
children: [{ type: '#text', value: 'Hello' }],
|
|
245
|
+
}
|
|
246
|
+
assert.equal(renderToJsx(node), '<div className="box">Hello</div>')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('renders a void element as self-closing', () => {
|
|
250
|
+
const node: OmlNode = { type: 'element', tag: 'br', props: {}, children: [] }
|
|
251
|
+
assert.equal(renderToJsx(node), '<br />')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('renders nested elements with indentation', () => {
|
|
255
|
+
const node: OmlNode = {
|
|
256
|
+
type: 'element',
|
|
257
|
+
tag: 'ul',
|
|
258
|
+
props: {},
|
|
259
|
+
children: [
|
|
260
|
+
{ type: 'element', tag: 'li', props: {}, children: [{ type: '#text', value: 'a' }] },
|
|
261
|
+
{ type: 'element', tag: 'li', props: {}, children: [{ type: '#text', value: 'b' }] },
|
|
262
|
+
],
|
|
263
|
+
}
|
|
264
|
+
assert.equal(renderToJsx(node), '<ul>\n <li>a</li>\n <li>b</li>\n</ul>')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('renders a #fragment', () => {
|
|
268
|
+
const node: OmlNode = {
|
|
269
|
+
type: '#fragment',
|
|
270
|
+
children: [
|
|
271
|
+
{ type: '#text', value: 'a' },
|
|
272
|
+
{ type: '#text', value: 'b' },
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
assert.equal(renderToJsx(node), '<>\n a\n b\n</>')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('renders a #component as an invocation, not its output', () => {
|
|
279
|
+
const node: OmlNode = {
|
|
280
|
+
type: '#component',
|
|
281
|
+
name: 'Badge',
|
|
282
|
+
props: { count: 5 },
|
|
283
|
+
output: { type: 'element', tag: 'span', props: {}, children: [] },
|
|
284
|
+
}
|
|
285
|
+
assert.equal(renderToJsx(node), '<Badge count={5} />')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('renders null as empty string', () => {
|
|
289
|
+
assert.equal(renderToJsx(null), '')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('renders boolean props correctly', () => {
|
|
293
|
+
const node: OmlNode = {
|
|
294
|
+
type: 'element',
|
|
295
|
+
tag: 'input',
|
|
296
|
+
props: { disabled: true, required: false },
|
|
297
|
+
children: [],
|
|
298
|
+
}
|
|
299
|
+
assert.equal(renderToJsx(node), '<input disabled required={false} />')
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// ─── blueprintToFile ──────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
describe('blueprintToFile', () => {
|
|
306
|
+
it('generates a component file with no props', () => {
|
|
307
|
+
const output = blueprintToFile({
|
|
308
|
+
id: 'bp:divider',
|
|
309
|
+
name: 'Divider',
|
|
310
|
+
props: {},
|
|
311
|
+
output: { type: 'element', tag: 'hr', props: {}, children: [] },
|
|
312
|
+
})
|
|
313
|
+
assert.ok(output.includes('export default function Divider()'))
|
|
314
|
+
assert.ok(output.includes('<hr />'))
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('generates typed props in the function signature', () => {
|
|
318
|
+
const output = blueprintToFile({
|
|
319
|
+
id: 'bp:button',
|
|
320
|
+
name: 'Button',
|
|
321
|
+
props: {
|
|
322
|
+
label: { type: 'string', required: true },
|
|
323
|
+
disabled: { type: 'boolean', required: false },
|
|
324
|
+
},
|
|
325
|
+
output: { type: 'element', tag: 'button', props: {}, children: [] },
|
|
326
|
+
})
|
|
327
|
+
assert.ok(output.includes('label: string'))
|
|
328
|
+
assert.ok(output.includes('disabled?: boolean'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('generates import statements from blueprint.imports', () => {
|
|
332
|
+
const output = blueprintToFile({
|
|
333
|
+
id: 'bp:card',
|
|
334
|
+
name: 'Card',
|
|
335
|
+
props: {},
|
|
336
|
+
output: {
|
|
337
|
+
type: '#component',
|
|
338
|
+
name: 'Badge',
|
|
339
|
+
props: {},
|
|
340
|
+
output: null,
|
|
341
|
+
},
|
|
342
|
+
imports: { Badge: './Badge.js' },
|
|
343
|
+
})
|
|
344
|
+
assert.ok(output.includes("import Badge from './Badge.js'"))
|
|
345
|
+
assert.ok(output.includes('<Badge />'))
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// ─── defineFragment ───────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
describe('defineFragment', () => {
|
|
352
|
+
it('returns the correct node on first call', async () => {
|
|
353
|
+
const Nav = defineFragment(async () => jsx('nav', { children: 'Home' }))
|
|
354
|
+
const node = await Nav()
|
|
355
|
+
assert.equal(renderToHtml(node), '<nav>Home</nav>')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('calls fn only once — second call returns cached node', async () => {
|
|
359
|
+
let calls = 0
|
|
360
|
+
const Widget = defineFragment(async () => {
|
|
361
|
+
calls++
|
|
362
|
+
return jsx('span', { children: 'hi' })
|
|
363
|
+
})
|
|
364
|
+
await Widget()
|
|
365
|
+
await Widget()
|
|
366
|
+
assert.equal(calls, 1)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('caches per unique props using JSON.stringify by default', async () => {
|
|
370
|
+
let calls = 0
|
|
371
|
+
const Badge = defineFragment(async (props: { count: number }) => {
|
|
372
|
+
calls++
|
|
373
|
+
return jsx('span', { children: String(props.count) })
|
|
374
|
+
})
|
|
375
|
+
await Badge({ count: 1 })
|
|
376
|
+
await Badge({ count: 2 })
|
|
377
|
+
await Badge({ count: 1 })
|
|
378
|
+
assert.equal(calls, 2)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('uses a custom key function when provided', async () => {
|
|
382
|
+
let calls = 0
|
|
383
|
+
const Item = defineFragment(
|
|
384
|
+
async (props: { id: string; ts: number }) => {
|
|
385
|
+
calls++
|
|
386
|
+
return jsx('li', { children: props.id })
|
|
387
|
+
},
|
|
388
|
+
{ key: (p) => p.id },
|
|
389
|
+
)
|
|
390
|
+
await Item({ id: 'a', ts: 1 })
|
|
391
|
+
await Item({ id: 'a', ts: 999 }) // different ts, same key → cache hit
|
|
392
|
+
assert.equal(calls, 1)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('re-runs fn after TTL expires', async () => {
|
|
396
|
+
let calls = 0
|
|
397
|
+
const Stale = defineFragment(
|
|
398
|
+
async () => {
|
|
399
|
+
calls++
|
|
400
|
+
return jsx('div', {})
|
|
401
|
+
},
|
|
402
|
+
{ ttl: 1 },
|
|
403
|
+
)
|
|
404
|
+
await Stale()
|
|
405
|
+
await new Promise((r) => setTimeout(r, 5))
|
|
406
|
+
await Stale()
|
|
407
|
+
assert.equal(calls, 2)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('clearAllFragments invalidates all fragment caches', async () => {
|
|
411
|
+
let aCalls = 0
|
|
412
|
+
let bCalls = 0
|
|
413
|
+
const A = defineFragment(async () => {
|
|
414
|
+
aCalls++
|
|
415
|
+
return jsx('div', {})
|
|
416
|
+
})
|
|
417
|
+
const B = defineFragment(async () => {
|
|
418
|
+
bCalls++
|
|
419
|
+
return jsx('span', {})
|
|
420
|
+
})
|
|
421
|
+
await A()
|
|
422
|
+
await B()
|
|
423
|
+
clearAllFragments()
|
|
424
|
+
await A()
|
|
425
|
+
await B()
|
|
426
|
+
assert.equal(aCalls, 2)
|
|
427
|
+
assert.equal(bCalls, 2)
|
|
428
|
+
})
|
|
429
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { fix, makeApp, request, route } from './helpers.js'
|
|
4
|
+
|
|
5
|
+
describe('redirects', () => {
|
|
6
|
+
test('redirect() in a handler returns 302 with Location header', async () => {
|
|
7
|
+
const app = makeApp({ routes: [route('redirect.page.ts', '/')] })
|
|
8
|
+
const { status, headers } = await request(app, { url: '/' })
|
|
9
|
+
assert.equal(status, 302)
|
|
10
|
+
assert.equal(headers.location, '/about')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('redirect() in app middleware returns 302', async () => {
|
|
14
|
+
const app = makeApp({ routes: [route('index.page.ts', '/')] }, fix('_redirect_mw.ts'))
|
|
15
|
+
const { status, headers } = await request(app, { url: '/' })
|
|
16
|
+
assert.equal(status, 302)
|
|
17
|
+
assert.equal(headers.location, '/login')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('redirect() with custom status code is respected', async () => {
|
|
21
|
+
const app = makeApp({ routes: [route('index.page.ts', '/')] }, fix('_redirect301_mw.ts'))
|
|
22
|
+
const { status, headers } = await request(app, { url: '/' })
|
|
23
|
+
assert.equal(status, 301)
|
|
24
|
+
assert.equal(headers.location, '/new-home')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('redirect does not render a body', async () => {
|
|
28
|
+
const app = makeApp({ routes: [route('redirect.page.ts', '/')] })
|
|
29
|
+
const { body } = await request(app, { url: '/' })
|
|
30
|
+
assert.equal(body, '')
|
|
31
|
+
})
|
|
32
|
+
})
|