template-studio/server/templateEngine.test.js

164 lines
5.8 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { interpolate, renderReportHtml, renderEmailHtml } from './templateEngine.js'
test('interpolates nested values', () => {
assert.equal(interpolate('Hello {{customer.name}}', { customer: { name: 'Kauri' } }), 'Hello Kauri')
})
test('report output escapes injected markup', () => {
const html = renderReportHtml({ blocks: [{ type: 'text', props: { text: '{{value}}' } }] }, { value: '<script>x</script>' })
assert.match(html, /&lt;script&gt;/)
assert.doesNotMatch(html, /<script>x/)
})
test('email renderer produces HTML', () => {
const html = renderEmailHtml({ blocks: [{ type: 'heading', props: { text: 'Hello' } }] }, {})
assert.match(html, /Hello/)
assert.match(html, /doctype/i)
})
test('report renderer applies constrained pack theme styling', () => {
const html = renderReportHtml({
theme: {
backgroundColor: '#f6f1e8',
accentColor: '#8b5e34',
pageBackgroundMode: 'watermark',
pageBackgroundImage: 'https://templates.arrowstack.dev/media/token/bg.webp'
},
blocks: [{
type: 'heading',
props: { text: 'Styled' },
style: { backgroundColor: '#fffaf2', padding: 18, borderRadius: 12 }
}]
}, {})
assert.match(html, /#f6f1e8/)
assert.match(html, /#8b5e34/)
assert.match(html, /bg\.webp/)
assert.match(html, /padding:18px/)
})
test('report renderer constrains logo and hero images for print layout', () => {
const html = renderReportHtml({
blocks: [
{ id: 'logo', type: 'image', props: { url: 'https://example.com/logo.png', alt: 'Company logo' } },
{ id: 'hero', type: 'image', props: { url: 'https://example.com/hero.jpg', alt: 'Hero banner' } }
]
}, {})
assert.match(html, /image-logo/)
assert.match(html, /max-height:72px/)
assert.match(html, /image-hero/)
assert.match(html, /max-height:260px/)
assert.match(html, /object-fit:cover/)
})
test('report blocks avoid page breaks by default and support explicit page controls', () => {
const html = renderReportHtml({
blocks: [{
id: 'title',
type: 'heading',
props: { text: 'Title' },
style: { keepWithNext: true, pageBreakBefore: true }
}]
}, {})
assert.match(html, /break-before:page/)
assert.match(html, /break-after:avoid-page/)
assert.match(html, /page-break-inside:avoid/)
})
test('report renderer applies independent page margins in millimetres', () => {
const html = renderReportHtml({
theme: {
pageMarginTop: 8,
pageMarginRight: 10,
pageMarginBottom: 14,
pageMarginLeft: 16,
pagePadding: 22
},
blocks: [{ type: 'text', props: { text: 'Margins' } }]
}, {})
assert.match(html, /margin:8mm 10mm 14mm 16mm/)
assert.match(html, /min-height:calc\(297mm - 22mm\)/)
assert.match(html, /padding:22px/)
})
test('report page chrome defaults to clean without stacked artwork', () => {
const html = renderReportHtml({
theme: {
pageBackgroundImage: 'https://example.com/paper.webp',
borderImage: 'https://example.com/border.png'
},
blocks: [{ type: 'text', props: { text: 'Clean report' } }]
}, {})
assert.match(html, /background-image:none/)
assert.match(html, /border:0px solid transparent/)
})
test('report page chrome can opt into subtle background and decorative border', () => {
const html = renderReportHtml({
theme: {
pageBackgroundImage: 'https://example.com/paper.webp',
pageBackgroundMode: 'watermark',
pageBackgroundOpacity: 0.1,
borderImage: 'https://example.com/border.png',
decorativeBorder: true
},
blocks: [{ type: 'text', props: { text: 'Styled report' } }]
}, {})
assert.match(html, /paper\.webp/)
assert.match(html, /opacity:0\.1/)
assert.match(html, /border:10px solid transparent/)
})
test('auto density compacts data-heavy reports and groups consecutive KPIs', () => {
const blocks = [
{ type: 'heading', props: { text: 'Dense' } },
{ type: 'kpi', props: { label: 'A', value: '1' } },
{ type: 'kpi', props: { label: 'B', value: '2' } },
{ type: 'kpi', props: { label: 'C', value: '3' } },
...Array.from({ length: 16 }, (_, index) => ({ type: 'text', props: { text: `Row ${index}` } }))
]
const html = renderReportHtml({ blocks }, {})
assert.match(html, /data-density="compact"/)
assert.match(html, /class="kpi-grid"/)
assert.match(html, /grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
})
test('compact density flows past ordinary page breaks but preserves forced breaks', () => {
const html = renderReportHtml({
theme: { reportDensity: 'compact' },
blocks: [
{ type: 'heading', props: { text: 'Auto flow' }, style: { pageBreakBefore: true } },
{ type: 'heading', props: { text: 'Forced' }, style: { pageBreakBefore: true, forcePageBreak: true } }
]
}, {})
assert.match(html, /Auto flow/)
assert.match(html, /break-before:auto/)
assert.match(html, /Forced/)
assert.match(html, /break-before:page/)
})
test('splittable tables repeat headers and use compact table styling', () => {
const html = renderReportHtml({
theme: { reportDensity: 'compact' },
blocks: [{
type: 'table',
props: { rowsPath: 'rows', columns: [{ label: 'Name', key: 'name' }] },
style: { allowPageBreak: true }
}]
}, { rows: [{ name: 'A' }, { name: 'B' }] })
assert.match(html, /class="table-wrap allow-break compact-table"/)
assert.match(html, /thead\{display:table-header-group\}/)
assert.match(html, /page-break-inside:auto/)
})
test('comfortable density preserves explicit section page breaks', () => {
const html = renderReportHtml({
theme: { reportDensity: 'comfortable' },
blocks: [{ type: 'heading', props: { text: 'Section' }, style: { pageBreakBefore: true } }]
}, {})
assert.match(html, /data-density="comfortable"/)
assert.match(html, /break-before:page/)
})