diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts new file mode 100644 index 0000000..36e48f3 --- /dev/null +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -0,0 +1,43 @@ +import { describe, test, expect } from 'vitest'; +import { ContactForm } from './ContactForm'; + +const toHtml = (ContactForm as any).toHtml; + +describe('ContactForm.toHtml relay wiring', () => { + test('with recipientEmail: emits marker, placeholder action, honeypot', () => { + const { html } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, ''); + expect(html).toMatch(//); + expect(html).toMatch(/action="__WHP_FORM_ACTION__F[0-9a-z]+__"/); + expect(html).toContain('method="POST"'); + expect(html).toContain('name="_gotcha"'); + // marker id and action id match + const mid = html.match(/id="(F[0-9a-z]+)"/)![1]; + expect(html).toContain(`__WHP_FORM_ACTION__${mid}__`); + }); + + test('without recipientEmail: no marker, falls back to formAction', () => { + const { html } = toHtml({ formAction: '/legacy', fields: [] }, ''); + expect(html).not.toContain('WHP-FORM'); + expect(html).toContain('action="/legacy"'); + expect(html).not.toContain('_gotcha'); + // Backward-compat: ensure non-relay output is byte-identical (no extra blank lines from honeypot) + expect(html).not.toMatch(/]*>\n\s*\n/); + }); + + test('without recipientEmail + real fields: byte-clean legacy output (realistic case)', () => { + // The empty-fields case is NOT byte-identical to the old code (the old + // template emitted a stray whitespace line when fields was empty; the new + // ternary drops it). Real forms always have fields, so pin THAT scenario: + // no marker, no honeypot, and no whitespace-only line between
and + // the first field. + const fields = [{ type: 'text', label: 'Name', name: 'name', placeholder: 'Your name', required: true }]; + const { html } = toHtml({ formAction: '/legacy', fields }, ''); + expect(html).not.toContain('WHP-FORM'); + expect(html).not.toContain('_gotcha'); + expect(html).toContain('action="/legacy"'); + expect(html).not.toMatch(/]*>\n\s*\n/); + // First field renders directly after the form tag (no stray blank line). + expect(html).toMatch(/]*>\n\s*
{ />
+ {/* Relay recipient */} +
+ + setProp((p: ContactFormProps) => { p.recipientEmail = e.target.value; })} + placeholder="you@example.com" style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }} /> +

+ Delivered via the site's contact-form relay. Requires the relay to be enabled on this server. +

+
+
+ + setProp((p: ContactFormProps) => { p.thankYouUrl = e.target.value; })} + placeholder="/thank-you (blank = hosted page)" style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }} /> +
+ {/* Success Message */}
@@ -358,6 +377,8 @@ ContactForm.craft = { labelColor: '#374151', inputBg: '#ffffff', inputBorder: '#d1d5db', + recipientEmail: '', + thankYouUrl: '', }, rules: { canDrag: () => true, @@ -414,10 +435,19 @@ ContactForm.craft = { alignSelf: 'flex-start', }); + const useRelay = !!props.recipientEmail; + const fid = 'F' + Math.random().toString(36).slice(2, 8); + const actionAttr = useRelay ? `__WHP_FORM_ACTION__${fid}__` : esc(props.formAction || '#'); + const honeypot = useRelay + ? `` + : ''; + const marker = useRelay + ? `` + : ''; + return { - html: ` - ${fieldsHtml} - + html: `${marker} +${honeypot ? ` ${honeypot}\n` : ''}${fieldsHtml ? ` ${fieldsHtml}\n` : ''} `, }; };