2026-09-08 15:59:26 +02:00
|
|
|
// 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'
|
|
|
|
|
|
2026-09-08 19:55:20 +02:00
|
|
|
import html from '@uhhm/buuh-html'
|
|
|
|
|
import raw from '@uhhm/buuh-html/raw'
|
|
|
|
|
import choo, { Choo } from '@uhhm/buuh'
|
2026-09-08 15:59:26 +02:00
|
|
|
|
2026-09-08 19:55:20 +02:00
|
|
|
test('should render on the server with @uhhm/buuh-html', () => {
|
2026-09-08 15:59:26 +02:00
|
|
|
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>`
|
|
|
|
|
}
|
|
|
|
|
})
|