// This file is mearly a copy of the original test/suspense.test.tsx from @kitajs/html // original source. // https://github.com/kitajs/html/blob/master/test/suspense.test.tsx // // This was adapted to work inside a fastify route handler. import Html, { type PropsWithChildren } from '@kitajs/html'; import { Suspense, SuspenseScript } from '@kitajs/html/suspense'; import fastify from 'fastify'; import { JSDOM } from 'jsdom'; import assert from 'node:assert'; import { afterEach, describe, test } from 'node:test'; import { setTimeout } from 'node:timers/promises'; import { fastifyKitaHtml } from '..'; async function SleepForMs({ ms, children }: PropsWithChildren<{ ms: number }>) { await setTimeout(ms * 50); return Html.contentsToString([children || String(ms)]); } describe('Suspense', () => { // Detect leaks of pending promises afterEach(() => { assert.equal(SUSPENSE_ROOT.requests.size, 0, 'Suspense root left pending resources'); // Reset suspense root SUSPENSE_ROOT.autoScript = true; SUSPENSE_ROOT.requestCounter = 1; SUSPENSE_ROOT.requests.clear(); }); test('Sync without suspense', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (_, res) => res.html(
)); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.body, '
'); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); }); test('Suspense sync children', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1
}>
2
) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.body, '
2
'); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); }); test('Suspense async children', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1}> ) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
{SuspenseScript} ); }); test('Suspense async children & fallback', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1}> ) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
{SuspenseScript} ); }); test('Suspense async fallback sync children', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1)}>
2
) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.body, '
2
'); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); }); test('Multiple async renders cleanup', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1)}> ) ); const promises = []; for (const _ of Array.from({ length: 100 })) { promises.push( app.inject({ method: 'GET', url: '/' }).then((res) => { assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
{SuspenseScript} ); }) ); } await Promise.all(promises); }); test('Multiple sync renders cleanup', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1)}> ) ); for (let i = 0; i < 10; i++) { const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
{SuspenseScript} ); } }); test('Multiple children', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html(
1
}> 2}> 3}> ) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
2
3
{SuspenseScript} ); }); test('Concurrent renders', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => { const seconds = (req.query as { seconds: number }).seconds; res.header('seconds', seconds); return res.html(
{Array.from({ length: seconds }, (_, i) => ( {seconds - i} loading
}> {seconds - i} ))} ); }); const secondsArray = [9, 4, 7]; const results = await Promise.all( secondsArray.map((seconds) => app.inject({ method: 'GET', url: '/', query: { seconds: seconds.toString() } }) ) ); for (const result of results) { // biome-ignore lint/style/noNonNullAssertion: this is a test const seconds = +result.headers.seconds!; assert.strictEqual(result.statusCode, 200); assert.strictEqual(result.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( result.body, <>
{Array.from({ length: seconds }, (_, i) => (
{seconds - i} loading
))}
{SuspenseScript} {Array.from({ length: seconds }, (_, i) => ( <> ))} ); } }); test('works with parallel deep suspense calls resolving first', async (t) => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html(
{Array.from({ length: 5 }, (_, i) => ( {i} fb outer
}>
Outer {i}!
{i} fb inner!}>
Inner {i}!
))} ) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); await t.test('Html stream', () => { assert.strictEqual( res.body, <>
0 fb outer
1 fb outer
2 fb outer
3 fb outer
4 fb outer
{SuspenseScript} ); }); await t.test('Browser simulation', async () => { // Simulates a browser assert.equal( new JSDOM(res.body, { runScripts: 'dangerously' }).window.document.body.innerHTML, <>
Outer 0!
Inner 0!
Outer 1!
Inner 1!
Outer 2!
Inner 2!
Outer 3!
Inner 3!
Outer 4!
Inner 4!
{SuspenseScript} ); }); }); test('tests suspense without error boundary', async () => { await using app = fastify(); app.register(fastifyKitaHtml); app.get('/', (req, res) => res.html( 1}> {Promise.reject(new Error('component failed'))} ) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.deepStrictEqual(res.json(), { error: 'Internal Server Error', message: 'component failed', statusCode: 500 }); }); test('tests suspense with function error boundary', async () => { await using app = fastify(); app.register(fastifyKitaHtml); const err = new Error('component failed'); app.get('/', (req, res) => res.html( 1} catch={(err2) => { assert.equal(err2, err); return
3
; }} > {Promise.reject(err)}
) ); const res = await app.inject({ method: 'GET', url: '/' }); assert.strictEqual(res.statusCode, 200); assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8'); assert.strictEqual( res.body, <>
1
{SuspenseScript} ); }); });