import { renderToEmailHtml, renderToPreviewHtml } from '../renderer/email-html-renderer' import type { Section, GlobalStyles } from '../types' import { DEFAULT_GLOBAL_STYLES, createSection, createRow } from '../types' // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- function makeSingleSection(overrides: Partial
= {}): Section[] { const section: Section = { id: 'section-1', rows: [ { id: 'row-1', layout: '1', columns: [ [ { id: 'text-1', type: 'text', content: '

Hello email

', }, ], ], }, ], paddingTop: 16, paddingBottom: 16, paddingLeft: 0, paddingRight: 0, ...overrides, } return [section] } const defaultStyles: GlobalStyles = DEFAULT_GLOBAL_STYLES // --------------------------------------------------------------------------- // Valid HTML document structure // --------------------------------------------------------------------------- describe('renderToEmailHtml — document structure', () => { it('starts with ', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html.trimStart()).toMatch(/^/i) }) it('contains root element with lang="en"', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain(' with charset meta tag', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('') }) it('contains viewport meta tag', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('name="viewport"') }) it('contains element', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain(' tag', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html.trimEnd()).toMatch(/<\/html>$/) }) it('contains closing tag', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('') }) it('contains role="presentation" table for email wrapper', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('role="presentation"') }) it('contains email-container class for responsive width', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('class="email-container"') }) it('contains @media query for max-width:620px', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('max-width:620px') }) it('contains MSO conditional comment for Outlook', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('[if mso]') }) }) // --------------------------------------------------------------------------- // Global styles applied to output // --------------------------------------------------------------------------- describe('renderToEmailHtml — global styles', () => { it('uses globalStyles.backgroundColor in body inline style', () => { const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, backgroundColor: '#abcdef' }) expect(html).toContain('background-color:#abcdef') }) it('uses globalStyles.contentWidth for container width attribute', () => { const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, contentWidth: 700 }) expect(html).toContain('width="700"') expect(html).toContain('max-width:700px') }) it('uses globalStyles.fontFamily in body inline style', () => { const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, fontFamily: 'Georgia, serif', }) expect(html).toContain('font-family:Georgia, serif') }) it('defaults contentWidth to 600 when not provided', () => { const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, contentWidth: 0 }) expect(html).toContain('width="600"') }) it('defaults backgroundColor when not provided', () => { const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, backgroundColor: '' }) // Falls back to the empty string default; body still renders expect(html).toContain(' { it('puts subject in tag', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles, { subject: 'Q1 Update' }) expect(html).toContain('<title>Q1 Update') }) it('empty subject results in empty ', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles, { subject: '' }) expect(html).toContain('<title>') }) it('renders preheader div when preheaderText is provided', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles, { preheaderText: 'Read this important update', }) expect(html).toContain('Read this important update') expect(html).toContain('display:none') }) it('does not render preheader div when preheaderText is absent', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).not.toContain('display:none') }) }) // --------------------------------------------------------------------------- // Block content included in output // --------------------------------------------------------------------------- describe('renderToEmailHtml — block content', () => { it('includes text block content in output', () => { const html = renderToEmailHtml(makeSingleSection(), defaultStyles) expect(html).toContain('Hello email') }) it('includes content from header block', () => { const section: Section = { id: 's1', rows: [ { id: 'r1', layout: '1', columns: [ [{ id: 'h1', type: 'header', companyName: 'StartSimpli', alignment: 'center' }], ], }, ], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('StartSimpli') }) it('includes metrics block values', () => { const section: Section = { id: 's1', rows: [ { id: 'r1', layout: '1', columns: [ [ { id: 'met1', type: 'metrics', title: 'My Metrics', metrics: [{ id: 'm1', label: 'Revenue', value: '$42k', changeType: 'positive' }], columns: 2, }, ], ], }, ], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('$42k') expect(html).toContain('Revenue') }) it('includes CTA button text and URL', () => { const section: Section = { id: 's1', rows: [ { id: 'r1', layout: '1', columns: [ [ { id: 'cta1', type: 'cta', text: 'Click Here', url: 'https://start.simpli', alignment: 'center', buttonColor: '#2563eb', textColor: '#ffffff', borderRadius: 6, paddingH: 24, paddingV: 12, }, ], ], }, ], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('Click Here') expect(html).toContain('https://start.simpli') }) it('includes footer company name', () => { const section: Section = { id: 's1', rows: [ { id: 'r1', layout: '1', columns: [ [ { id: 'f1', type: 'footer', companyName: 'Acme Inc', showUnsubscribe: false, alignment: 'center', }, ], ], }, ], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('Acme Inc') }) }) // --------------------------------------------------------------------------- // Multi-section and multi-column layout // --------------------------------------------------------------------------- describe('renderToEmailHtml — layout', () => { it('renders multiple sections (both section backgrounds appear)', () => { const s1: Section = { id: 's1', backgroundColor: '#ff0000', rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'Sec1' }]] }], } const s2: Section = { id: 's2', backgroundColor: '#00ff00', rows: [{ id: 'r2', layout: '1', columns: [[{ id: 't2', type: 'text', content: 'Sec2' }]] }], } const html = renderToEmailHtml([s1, s2], defaultStyles) expect(html).toContain('#ff0000') expect(html).toContain('#00ff00') expect(html).toContain('Sec1') expect(html).toContain('Sec2') }) it('renders multi-column row using nested table with stack-column class', () => { const section: Section = { id: 's1', rows: [ { id: 'r1', layout: '2', columns: [ [{ id: 't1', type: 'text', content: 'Left col' }], [{ id: 't2', type: 'text', content: 'Right col' }], ], }, ], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('class="stack-column"') expect(html).toContain('Left col') expect(html).toContain('Right col') }) it('section separator spacer row is present between sections', () => { const s1: Section = { id: 's1', rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'A' }]] }], } const s2: Section = { id: 's2', rows: [{ id: 'r2', layout: '1', columns: [[{ id: 't2', type: 'text', content: 'B' }]] }], } const html = renderToEmailHtml([s1, s2], defaultStyles) // 4px spacer row between sections expect(html).toContain('height:4px') }) it('section padding is applied in style attribute', () => { const section: Section = { id: 's1', paddingTop: 32, paddingBottom: 24, paddingLeft: 8, paddingRight: 8, rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'X' }]] }], } const html = renderToEmailHtml([section], defaultStyles) expect(html).toContain('padding:32px 8px 24px 8px') }) it('renders empty sections array without crashing', () => { expect(() => renderToEmailHtml([], defaultStyles)).not.toThrow() const html = renderToEmailHtml([], defaultStyles) expect(html).toContain('') }) it('renders section with empty columns without crashing', () => { const section: Section = { id: 's1', rows: [{ id: 'r1', layout: '1', columns: [[]] }], } expect(() => renderToEmailHtml([section], defaultStyles)).not.toThrow() }) }) // --------------------------------------------------------------------------- // renderToPreviewHtml delegates to renderToEmailHtml // --------------------------------------------------------------------------- describe('renderToPreviewHtml', () => { it('returns the same output as renderToEmailHtml', () => { const sections = makeSingleSection() expect(renderToPreviewHtml(sections, defaultStyles)).toBe( renderToEmailHtml(sections, defaultStyles) ) }) it('uses DEFAULT_GLOBAL_STYLES when no styles argument provided', () => { const sections = makeSingleSection() const html = renderToPreviewHtml(sections) expect(html).toContain(`background-color:${DEFAULT_GLOBAL_STYLES.backgroundColor}`) }) })