Files
buuh/packages/core/test/url.test.js
T
Bendik Aagaard LynghaugandClaude Fable 5 ce1ec9e4a9 rebrand: buuh — a friendly public fork under the uhhm org
Packages renamed to the @uhhm scope (@uhhm/buuh, @uhhm/buuh-html,
@uhhm/buuh-component, @uhhm/buuh-devtools, @uhhm/buuh-migrate,
@uhhm/bankai). Scoping is load-bearing twice over: npm routes registries
per scope so @uhhm/* resolves against project.uhhm.no while everything
else stays on npmjs, and it means this fork never squats upstream's
names anywhere. The codemod now migrates choo v7 apps to the @uhhm
names. README rewritten with the fork framing and full upstream credit;
the choojs RFC moves to docs/upstream-rfc-draft.md, in the drawer for if
this work ever goes home. API unchanged — choo() is still choo().

Also: Gitea Actions CI + release workflows (npm publish to the uhhm
registry on tag push, CDN bundle uploaded as a generic package),
npm run bundle producing dist-cdn/buuh.js (the whole framework as one
minified ES module for import-map use), docs/publishing.md explaining
what Gitea Packages is (a real npm registry) and is not (a CDN — serve
the bundle from a static host with module-safe MIME instead), and
onload.js constructing window.MutationObserver to match its own guard
(surfaced by smoke-testing the bundle outside a full browser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
2026-09-08 19:55:20 +02:00

115 lines
3.1 KiB
JavaScript

// URL normalization: the non-ASCII and malformed-percent cases that made
// v7 routing crash or mismatch. See lib/router.js header for the rules.
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@uhhm/buuh-html'
import choo from '@uhhm/buuh'
function view (state) {
return html`<div></div>`
}
test('a literal % in the path does not crash routing', () => {
const app = choo()
let seen
app.route('/deals/:label', (state) => {
seen = state
return view(state)
})
app.toString('/deals/50%off')
assert.strictEqual(seen.params.label, '50%off', 'raw segment kept when not decodable')
assert.strictEqual(seen.href, '/deals/50%off', 'href kept raw when not decodable')
})
test('percent-encoded UTF-8 params decode exactly once', () => {
const app = choo()
let seen
app.route('/user/:name', (state) => {
seen = state
return view(state)
})
app.toString('/user/%F0%9F%9A%82')
assert.strictEqual(seen.params.name, '🚂', 'param decoded')
assert.strictEqual(seen.href, '/user/🚂', 'href decoded for humans')
})
test('double-encoded input is not double-decoded', () => {
const app = choo()
let seen
app.route('/user/:name', (state) => {
seen = state
return view(state)
})
// %2540 is '%40' encoded once; v7 double-decoded it all the way to '@'
app.toString('/user/%2540')
assert.strictEqual(seen.params.name, '%40', 'decoded exactly once')
})
test('non-ASCII literal routes match their encoded locations', () => {
const app = choo()
let calls = 0
app.route('/café', (state) => {
calls++
return view(state)
})
app.toString('/caf%C3%A9')
assert.strictEqual(calls, 1, 'encoded location matched unencoded route')
})
test('NFD input matches an NFC route definition', () => {
const app = choo()
let calls = 0
app.route('/café', (state) => { // composed é
calls++
return view(state)
})
app.toString('/café') // decomposed e + combining acute
assert.strictEqual(calls, 1, 'unicode-normalized before matching')
})
test('unencoded non-ASCII locations route fine', () => {
const app = choo()
let seen
app.route('/user/:name', (state) => {
seen = state
return view(state)
})
app.toString('/user/日本語')
assert.strictEqual(seen.params.name, '日本語')
})
test('wildcards decode per segment', () => {
const app = choo()
let seen
app.route('/files/*', (state) => {
seen = state
return view(state)
})
app.toString('/files/caf%C3%A9/na%C3%AFve.txt')
assert.strictEqual(seen.params.wildcard, 'café/naïve.txt')
})
test('hash routing survives multiple hashes', () => {
const app = choo({ hash: true })
let calls = 0
app.route('/docs/api/intro', (state) => {
calls++
return view(state)
})
app.toString('/docs#api#intro')
assert.strictEqual(calls, 1, 'every hash became a slash')
})
test('query strings decode + and percent-encoding via URLSearchParams', () => {
const app = choo()
let seen
app.route('/', (state) => {
seen = state
return view(state)
})
app.toString('/?q=caf%C3%A9+au+lait&tags=a&tags=b')
assert.deepStrictEqual(seen.query, { q: 'café au lait', tags: ['a', 'b'] })
})