diff --git a/lib/exporter/hero.edge-cases.test.ts b/lib/exporter/hero.edge-cases.test.ts new file mode 100644 index 0000000..9606abe --- /dev/null +++ b/lib/exporter/hero.edge-cases.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest' +import { exportPage } from '@/lib/exporter/html' +import { pageSchema, type Page } from '@/lib/schema/page' + +function makePage(overrides: Partial = {}): Page { + const base: Page = { + title: 'Hero Edge', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ + { type: 'hero', props: { heading: 'Welcome', subheading: ' ', imageUrl: 'https://img.example/h.png' } }, + ] as Page['sections'], + form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + analytics: { ga4MeasurementId: '', metaPixelId: '', customHeadHtml: '' }, + seo: { title: 't', description: 'd' }, + } + return pageSchema.parse({ ...base, ...overrides }) +} + +describe('Exporter - hero edge cases', () => { + it('omits subheading when whitespace-only', () => { + const out = exportPage(makePage()) + expect(out.html).toContain('

Welcome

') + expect(out.html).not.toMatch(/
[\s\S]*

\s*<\/p>/) + }) + + it('trims subheading content', () => { + const out = exportPage(makePage({ sections: [ { type: 'hero', props: { heading: 'Welcome', subheading: ' Hello world ', imageUrl: 'https://img.example/h.png' } } ] as Page['sections'] })) + expect(out.html).toContain('

Hello world

') + }) +}) diff --git a/lib/exporter/html.ts b/lib/exporter/html.ts index adb110e..2c6cd0b 100644 --- a/lib/exporter/html.ts +++ b/lib/exporter/html.ts @@ -79,7 +79,7 @@ function renderTestimonials(section: { props: { items: Array<{ author: string; q (it) => `
“${escapeHtml(it.quote)}”
-
— ${escapeHtml(it.author)}
+ ${it.author.length > 0 ? `
— ${escapeHtml(it.author)}
` : ''}
` ) .join('')} @@ -110,7 +110,9 @@ const escapeHtml = (s: string) => .replaceAll('"', '"') function renderHero(section: { props: { heading: string; subheading?: string; imageUrl: string } }, opts?: ExportOptions) { - const { heading, subheading } = section.props + const { heading } = section.props + const subheadingRaw = section.props.subheading + const subheading = subheadingRaw ? subheadingRaw.trim() : '' const am = opts?.assetManager const imageUrlRaw: string = section.props.imageUrl const imageUrl = am ? am.rewriteUrl(imageUrlRaw) : imageUrlRaw @@ -119,7 +121,7 @@ function renderHero(section: { props: { heading: string; subheading?: string; im

${escapeHtml(heading)}

- ${subheading ? `

${escapeHtml(subheading)}

` : ''} + ${subheading.length > 0 ? `

${escapeHtml(subheading)}

` : ''}
${escapeHtml(heading)} @@ -276,8 +278,8 @@ function buildCss(page: Page) { button:focus, a:focus, input:focus, select:focus, textarea:focus{ outline: 3px solid #2563eb; outline-offset:2px } .faq-list{ list-style:none; padding:0; margin:0 } .faq-item{ margin: .75rem 0 } - .faq-q{ font-size: 1rem; font-weight:600; } - .faq-a{ color:#334155 } + .faq-q{ font-size: 1rem; font-weight:600; overflow-wrap:anywhere } + .faq-a{ color:#334155; overflow-wrap:anywhere } .btn-cta{ background: var(--primary); color:#fff; padding:.75rem 1rem; border-radius:.5rem; display:inline-block; text-decoration:none } .features{ background:#f8fafc } .features-list{ list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .75rem } diff --git a/lib/exporter/sections.edge-phase3.test.ts b/lib/exporter/sections.edge-phase3.test.ts new file mode 100644 index 0000000..41a80a5 --- /dev/null +++ b/lib/exporter/sections.edge-phase3.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest' +import { exportPage } from '@/lib/exporter/html' +import { pageSchema, type Page } from '@/lib/schema/page' + +function makePage(overrides: Partial = {}): Page { + const base: Page = { + title: 'Edge P3', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ + { type: 'faq', props: { items: [ { q: 'VeryVeryVeryVeryVeryLongWordWithoutSpaces', a: 'AnotherSuperLongUnbreakableWord' } ] } }, + { type: 'testimonials', props: { items: [ + { author: ' ', quote: 'Great!' }, + { author: 'Alice', quote: 'Nice' }, + ] } }, + ] as Page['sections'], + form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + analytics: { ga4MeasurementId: '', metaPixelId: '', customHeadHtml: '' }, + seo: { title: 't', description: 'd' }, + } + return pageSchema.parse({ ...base, ...overrides }) +} + +describe('Exporter - phase3 edge cases', () => { + it('omits testimonial author line when author is blank', () => { + const out = exportPage(makePage()) + const html = out.html + // First item author blank -> no figcaption for that blockquote + expect(html).toMatch(/
“Great!\”<\/blockquote>[\s\S]*?<\/figure>/) + // Ensure the next item with author renders figcaption + expect(html).toMatch(/
— Alice<\/figcaption>/) + }) + + it('adds word-break/overflow-wrap for FAQ q/a to avoid overflow', () => { + const out = exportPage(makePage()) + const css = out.css + expect(css).toMatch(/\.faq-q\{[\s\S]*(?:overflow-wrap:anywhere|word-break:break-word)/i) + expect(css).toMatch(/\.faq-a\{[\s\S]*(?:overflow-wrap:anywhere|word-break:break-word)/i) + }) +})