// 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,
<>
{SuspenseScript}
2
>
);
});
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,
<>
{SuspenseScript}
2
>
);
});
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,
<>
{SuspenseScript}
2
>
);
})
);
}
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,
<>
{SuspenseScript}
2
>
);
}
});
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,
<>
{SuspenseScript}
4
5
6
>
);
});
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) => (
))}
{SuspenseScript}
{Array.from({ length: seconds }, (_, i) => (
<>
{i + 1}
>
))}
>
);
}
});
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,
<>
{SuspenseScript}
Inner 0!
Outer 0!
Inner 1!
Outer 1!
Outer 2!
Inner 2!
Outer 4!
Inner 3!
Outer 3!
Inner 4!
>
);
});
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,
<>
{SuspenseScript}
3
>
);
});
});