79 lines
3.0 KiB
JavaScript
79 lines
3.0 KiB
JavaScript
// Async holes and streaming in the server tag.
|
|||
|
|
|
||
|
|
import { test } from 'node:test'
|
||
|
|
import assert from 'node:assert'
|
||
|
|
|
||
|
|
import html from '@choojs/html'
|
||
|
|
import raw from '@choojs/html/raw'
|
||
|
|
|
||
|
|
async function collect (chunks) {
|
||
|
|
const out = []
|
||
|
|
for await (const chunk of chunks) out.push(chunk)
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))
|
||
|
|
|
||
|
|
test('sync templates stream as a single chunk equal to toString()', async () => {
|
||
|
|
const res = html`<p>hello ${'world'}</p>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.deepStrictEqual(chunks, ['<p>hello world</p>'])
|
||
|
|
assert.strictEqual(chunks.join(''), res.toString())
|
||
|
|
})
|
||
|
|
|
||
|
|
test('a promise child splits the stream at the hole', async () => {
|
||
|
|
const res = html`<main><h1>shell</h1>${wait(10, 'late')}<footer>end</footer></main>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks[0], '<main><h1>shell</h1>', 'everything before the hole flushes first')
|
||
|
|
assert.strictEqual(chunks.join(''), '<main><h1>shell</h1>late<footer>end</footer></main>')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('toString() on async content throws with streaming guidance', () => {
|
||
|
|
const res = html`<p>${Promise.resolve('x')}</p>`
|
||
|
|
assert.throws(() => res.toString(), /toStream/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('resolved promises can carry templates, arrays, and raw', async () => {
|
||
|
|
const res = html`<ul>${wait(5, [html`<li>a</li>`, html`<li>${'<b>'}</li>`, raw('<li>raw</li>')])}</ul>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks.join(''), '<ul><li>a</li><li><b></li><li>raw</li></ul>')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('nested templates with async holes stream through their parents', async () => {
|
||
|
|
const inner = html`<section>${wait(5, 'inner-late')}</section>`
|
||
|
|
const res = html`<div><p>early</p>${inner}</div>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks[0], '<div><p>early</p><section>', 'parent flushes up to the nested hole')
|
||
|
|
assert.strictEqual(chunks.join(''), '<div><p>early</p><section>inner-late</section></div>')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('async iterable children stream chunk by chunk', async () => {
|
||
|
|
async function * rows () {
|
||
|
|
for (let i = 0; i < 3; i++) yield html`<li>${i}</li>`
|
||
|
|
}
|
||
|
|
const res = html`<ul>${rows()}</ul>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks.join(''), '<ul><li>0</li><li>1</li><li>2</li></ul>')
|
||
|
|
assert.ok(chunks.length >= 4, 'each yielded row is its own chunk')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('multiple async holes resolve in document order', async () => {
|
||
|
|
// the second promise resolves first; output order must follow the document
|
||
|
|
const res = html`<div>${wait(20, 'A')}|${wait(5, 'B')}</div>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks.join(''), '<div>A|B</div>')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('async values in attribute position throw immediately', () => {
|
||
|
|
assert.throws(
|
||
|
|
() => html`<div class=${Promise.resolve('x')}></div>`,
|
||
|
|
/child position/
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('resolved async strings are escaped like any child', async () => {
|
||
|
|
const res = html`<p>${wait(5, '<script>')}</p>`
|
||
|
|
const chunks = await collect(res)
|
||
|
|
assert.strictEqual(chunks.join(''), '<p><script></p>')
|
||
|
|
})
|