Files
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

268 lines
8.5 KiB
JavaScript

// Ported from choo 7.1.0 test/node.js (MIT), tape → node:test.
// The hyperscript case now uses a plain object with outerHTML, which is the
// contract toString actually relies on.
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@uhhm/buuh-html'
import raw from '@uhhm/buuh-html/raw'
import choo, { Choo } from '@uhhm/buuh'
test('should render on the server with @uhhm/buuh-html', () => {
const app = choo()
app.route('/', function (state, emit) {
const strong = '<strong>Hello filthy planet</strong>'
return html`
<p>${raw(strong)}</p>
`
})
const res = app.toString('/')
const exp = '<p><strong>Hello filthy planet</strong></p>'
assert.strictEqual(res.toString().trim(), exp, 'result was OK')
})
test('should render on the server with any view returning outerHTML', () => {
const app = choo()
app.route('/', function (state, emit) {
return { outerHTML: '<p><strong>Hello filthy planet</strong></p>', nodeName: 'P' }
})
const res = app.toString('/')
const exp = '<p><strong>Hello filthy planet</strong></p>'
assert.strictEqual(res.toString().trim(), exp, 'result was OK')
})
test('should expose a public API', () => {
const app = choo()
assert.strictEqual(typeof app.route, 'function', 'app.route prototype method exists')
assert.strictEqual(typeof app.toString, 'function', 'app.toString prototype method exists')
assert.strictEqual(typeof app.start, 'function', 'app.start prototype method exists')
assert.strictEqual(typeof app.mount, 'function', 'app.mount prototype method exists')
assert.strictEqual(typeof app.emitter, 'object', 'app.emitter prototype method exists')
assert.strictEqual(typeof app.emit, 'function', 'app.emit instance method exists')
assert.strictEqual(typeof app.router, 'object', 'app.router instance object exists')
assert.strictEqual(typeof app.state, 'object', 'app.state instance object exists')
})
test('the default export works with and without new, and instances are Choo', () => {
assert.ok(choo() instanceof Choo, 'choo() returns a Choo instance')
assert.ok(new Choo() instanceof Choo, 'new Choo() works')
})
test('should enable history and href by default', () => {
const app = choo()
assert.ok(app._historyEnabled, 'history enabled')
assert.ok(app._hrefEnabled, 'href enabled')
})
test('router should pass state and emit to view', () => {
let calls = 0
const app = choo()
app.route('/', function (state, emit) {
assert.strictEqual(typeof state, 'object', 'state is an object')
assert.strictEqual(typeof emit, 'function', 'emit is a function')
calls++
return html`<div></div>`
})
app.toString('/')
assert.strictEqual(calls, 1, 'view was rendered')
})
test('router should support a default route', () => {
let calls = 0
const app = choo()
app.route('*', function (state, emit) {
calls++
return html`<div></div>`
})
app.toString('/random')
assert.strictEqual(calls, 1, 'default route was rendered')
})
test('enabling hash routing should treat hashes as slashes', () => {
let calls = 0
const app = choo({ hash: true })
app.route('/account/security', function (state, emit) {
calls++
return html`<div></div>`
})
app.toString('/account#security')
assert.strictEqual(calls, 1, 'hash route was rendered')
})
test('router should ignore hashes by default', () => {
let calls = 0
const app = choo()
app.route('/account', function (state, emit) {
calls++
return html`<div></div>`
})
app.toString('/account#security')
assert.strictEqual(calls, 1, 'route was rendered')
})
test('cache should default to 100 instances', () => {
let pruned = 0
const app = choo()
app.route('/', function (state, emit) {
let i
for (i = 0; i <= 100; i++) state.cache(Component, i)
state.cache(Component, 0)
return html`<div></div>`
function Component (id) {
if (id < i) pruned++
}
})
app.toString('/')
assert.strictEqual(pruned, 1, 'oldest instance was pruned when exceeding 100')
})
test('cache option should override number of max instances', () => {
let pruned = 0
const app = choo({ cache: 1 })
app.route('/', function (state, emit) {
let instances = 0
state.cache(Component, instances)
state.cache(Component, instances)
state.cache(Component, 0)
return html`<div></div>`
function Component (id) {
if (id < instances) pruned++
instances++
}
})
app.toString('/')
assert.strictEqual(pruned, 1, 'oldest instance was pruned when exceeding 1')
})
test('cache option should override default LRU cache', () => {
let gets = 0
let sets = 0
const cache = {
get (id) { gets++ },
set (id, el) { sets++ }
}
const app = choo({ cache })
app.route('/', function (state, emit) {
state.cache(Component, 'foo')
return html`<div></div>`
})
app.toString('/')
assert.strictEqual(gets, 1, 'called get')
assert.strictEqual(sets, 1, 'called set')
function Component () {}
})
// built-in state
test('state should include events', () => {
let checked = false
const app = choo()
app.route('/', function (state, emit) {
assert.ok(Object.hasOwn(state, 'events'), 'state has events property')
assert.ok(Object.keys(state.events).length > 0, 'events object has keys')
checked = true
return html`<div></div>`
})
app.toString('/')
assert.ok(checked)
})
test('state should include location on render', () => {
let checked = false
const app = choo()
app.route('/:first/:second/*', function (state, emit) {
const params = { first: 'foo', second: 'bar', wildcard: 'file.txt' }
assert.strictEqual(state.href, '/foo/bar/file.txt', 'state has href')
assert.strictEqual(state.route, ':first/:second/*', 'state has route')
assert.ok(Object.hasOwn(state, 'params'), 'state has params')
assert.deepStrictEqual(state.params, params, 'params match')
assert.ok(Object.hasOwn(state, 'query'), 'state has query')
assert.deepStrictEqual(state.query, { bin: 'baz' }, 'query match')
checked = true
return html`<div></div>`
})
app.toString('/foo/bar/file.txt?bin=baz')
assert.ok(checked)
})
test('state should include location on store init', () => {
let checked = false
const app = choo()
app.use(store)
app.route('/:first/:second/*', function (state, emit) {
return html`<div></div>`
})
app.toString('/foo/bar/file.txt?bin=baz')
assert.ok(checked)
function store (state, emit) {
const params = { first: 'foo', second: 'bar', wildcard: 'file.txt' }
assert.strictEqual(state.href, '/foo/bar/file.txt', 'state has href')
assert.strictEqual(state.route, ':first/:second/*', 'state has route')
assert.ok(Object.hasOwn(state, 'params'), 'state has params')
assert.deepStrictEqual(state.params, params, 'params match')
assert.ok(Object.hasOwn(state, 'query'), 'state has query')
assert.deepStrictEqual(state.query, { bin: 'baz' }, 'query match')
checked = true
}
})
test('state should include cache', () => {
let constructed = 0
const app = choo()
app.route('/', function (state, emit) {
assert.strictEqual(typeof state.cache, 'function', 'state has cache method')
const cached = state.cache(Component, 'foo', 'arg')
assert.strictEqual(cached, state.cache(Component, 'foo'), 'consecutive calls return same instance')
return html`<div></div>`
})
app.toString('/')
assert.strictEqual(constructed, 1, 'component constructed once')
function Component (id, state, emit, arg) {
assert.strictEqual(id, 'foo', 'id was prefixed to constructor args')
assert.strictEqual(typeof state, 'object', 'state was prefixed to constructor args')
assert.strictEqual(typeof emit, 'function', 'emit was prefixed to constructor args')
assert.strictEqual(arg, 'arg', 'constructor args were forwarded')
constructed++
}
})
test('state should not mutate on toString', () => {
const app = choo()
app.use(store)
const routes = ['foo', 'bar']
const states = routes.map(function (route) {
const state = {}
app.route(`/${route}`, view)
app.toString(`/${route}`, state)
return state
})
for (let i = 0; i < routes.length; i++) {
assert.strictEqual(states[i].test, routes[i], 'store was used')
assert.strictEqual(states[i].title, routes[i], 'title was added to state')
}
function store (state, emitter) {
state.test = null
emitter.on('test', function (str) {
assert.strictEqual(state.test, null, 'state has been reset')
state.test = str
})
}
function view (state, emit) {
emit('test', state.route)
emit(state.events.DOMTITLECHANGE, state.route)
return html`<body>Hello ${state.route}</body>`
}
})