fastify-html
Version:
Generate html in the most natural Fastify way, using template tags, layouts and the plugin system.
948 lines (832 loc) • 22.3 kB
JavaScript
import { test } from 'node:test'
import fastify from 'fastify'
import fastifyHtml from './index.js'
import { strictEqual } from 'node:assert'
import { readFile } from 'node:fs/promises'
import { readFileSync, createReadStream } from 'node:fs'
const README = readFileSync('README.md', 'utf8')
const escapeDictionary = {
'"': '"',
'&': '&',
"'": ''',
'<': '<',
'=': '=',
'>': '>'
}
test('render html', async t => {
const app = fastify()
app.register(fastifyHtml)
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello Matteo</h1>')
}
})
test('render html (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello Matteo</h1>')
}
})
test('one level layout', async t => {
const app = fastify()
await app.register(fastifyHtml)
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Matteo</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('one level layout (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.setErrorHandler(function (error, req, reply) {
console.error(error)
})
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Matteo</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('two levels layout', async t => {
const app = fastify()
await app.register(fastifyHtml)
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.addLayout(function (inner) {
return app.html`
<div>
!${inner}
</div>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello World</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello Matteo</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('two levels layout (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.addLayout(function (inner) {
return app.html`
<div>
!${inner}
</div>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello World</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello Matteo</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('two levels layout with plugins', async t => {
const app = fastify()
await app.register(fastifyHtml)
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
app.register(async function (app) {
app.addLayout(function (inner) {
return app.html`
<div>
!${inner}
</div>
`
})
app.get('/inner', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/inner?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello Matteo</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('two levels layout with plugins (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
app.register(async function (app) {
app.addLayout(function (inner) {
return app.html`
<div>
!${inner}
</div>
`
})
app.get('/inner', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
{
const res = await app.inject({
method: 'GET',
url: '/inner?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>Hello Matteo</h1>
</div>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
}
})
test('use reply in the layout', async t => {
const app = fastify()
await app.register(fastifyHtml)
let _reply
app.addLayout(function (inner, reply) {
strictEqual(reply, _reply)
return app.html`<html lang="en">
<body>
!
${inner}
</body>
</html>`
})
app.get('/', async (req, reply) => {
_reply = reply
strictEqual(reply.html`<h1>Hello World</h1>`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
})
test('use reply in the layout (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
let _reply
app.addLayout(function (inner, reply) {
strictEqual(reply, _reply)
return app.html`<html lang="en">
<body>
!
${inner}
</body>
</html>`
})
app.get('/', async (req, reply) => {
_reply = reply
strictEqual(reply.html`<h1>Hello World</h1>`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body.replaceAll(' ', '').replaceAll('\n', ''), `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello World</h1>
</body>
</html>`.replaceAll(' ', '').replaceAll('\n', ''))
})
test('skip layout with hx-request', async t => {
const app = fastify()
await app.register(fastifyHtml)
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
${inner}
</body>
</html>
`
}, { skipOnHeader: 'hx-request' })
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/',
headers: {
'hx-request': true
}
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
})
test('skip layout with hx-request (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
${inner}
</body>
</html>
`
}, { skipOnHeader: 'hx-request' })
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/',
headers: {
'hx-request': true
}
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
})
test('render arrays', async t => {
const app = fastify()
app.register(fastifyHtml)
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>${['Hello', ' ', name]}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello Matteo</h1>')
}
})
test('render arrays (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
strictEqual(reply.html`<h1>${['Hello', ' ', name]}</h1>`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello World</h1>')
}
{
const res = await app.inject({
method: 'GET',
url: '/?name=Matteo'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '<h1>Hello Matteo</h1>')
}
})
test('renders an empty tag', async t => {
const app = fastify()
app.register(fastifyHtml)
app.get('/', async (_req, reply) => {
strictEqual(reply.html``, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '')
}
})
test('renders an empty tag (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (_req, reply) => {
strictEqual(reply.html``, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '')
}
})
test('does not render null', async t => {
const app = fastify()
app.register(fastifyHtml)
app.get('/', async (_req, reply) => {
strictEqual(reply.html`${null}`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '')
}
})
test('does not render null (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (_req, reply) => {
strictEqual(reply.html`${null}`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '')
}
})
test('renders a number and bigint', async t => {
const app = fastify()
app.register(fastifyHtml)
app.get('/', async (_req, reply) => {
strictEqual(reply.html`${42}${42n}`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '4242')
}
})
test('renders a number and bigint (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (_req, reply) => {
strictEqual(reply.html`${42}${42n}`, reply)
return reply
})
{
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, '4242')
}
})
test('escape', async t => {
const app = fastify()
app.register(fastifyHtml)
app.post('/', async (req, reply) => {
const char = req.body.char
strictEqual(reply.html`<h1>Hello ${char}</h1>`, reply)
return reply
})
for (const char of ['<', '>', '"', '\'', '&']) {
const res = await app.inject({
method: 'POST',
url: '/',
body: { char }
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, `<h1>Hello ${escapeDictionary[char]}</h1>`)
}
})
test('escape (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.post('/', async (req, reply) => {
const char = req.body.char
strictEqual(reply.html`<h1>Hello ${char}</h1>`, reply)
return reply
})
for (const char of ['<', '>', '"', '\'', '&']) {
const res = await app.inject({
method: 'POST',
url: '/',
body: { char }
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, `<h1>Hello ${escapeDictionary[char]}</h1>`)
}
})
test('readFile/promise (async)', async t => {
const app = fastify()
app.register(fastifyHtml, { async: true })
app.get('/', async (req, reply) => {
strictEqual(reply.html`<h1>Hello !${readFile('README.md', 'utf8')}</h1>`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
strictEqual(res.body, `<h1>Hello ${README}</h1>`)
})
test('layout and a list of promises (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
strictEqual(reply.html`
<h1>Hello Matteo</h1>
<ul>
!${[
readFile('README.md', 'utf8'),
readFile('README.md', 'utf8')
].map(p => app.html`<li>!${p}</li>`)}
</ul>
`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
const actualOutput = res.body.replaceAll(' ', '').replaceAll('\n', '')
const expectedOutput = `
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Matteo</h1>
<ul>
<li>${README}</li>
<li>${README}</li>
</ul>
</body>
</html>
`.replaceAll(' ', '').replaceAll('\n', '')
strictEqual(actualOutput, expectedOutput)
})
test('layout and streams as expressions (async)', async t => {
const app = fastify()
await app.register(fastifyHtml, { async: true })
app.setErrorHandler(function (error, req, reply) {
console.error(error)
})
app.addLayout(function (inner) {
return app.html`
<html lang="en">
<body>
!
${inner}
</body>
</html>
`
})
app.get('/', async (req, reply) => {
strictEqual(reply.html`
<h1>Hello Matteo</h1>
<ul>
!${[
createReadStream('README.md', 'utf8')
].map(p => app.html`<li>!${p}</li>`)}
<li>!${createReadStream('README.md', 'utf8')}</li>
</ul>
`, reply)
return reply
})
const res = await app.inject({
method: 'GET',
url: '/'
})
strictEqual(res.statusCode, 200)
strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
const actualOutput = res.body.replaceAll(' ', '').replaceAll('\n', '')
const expectedOutput = `
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Matteo</h1>
<ul>
<li>${README}</li>
<li>${README}</li>
</ul>
</body>
</html>
`.replaceAll(' ', '').replaceAll('\n', '')
strictEqual(actualOutput, expectedOutput)
})