2026-07-12 18:03:44 -07:00
|
|
|
import { describe, test, expect } from 'vitest';
|
|
|
|
|
import { SubscribeForm } from './SubscribeForm';
|
|
|
|
|
|
|
|
|
|
const toHtml = (SubscribeForm as any).toHtml;
|
|
|
|
|
|
|
|
|
|
describe('SubscribeForm.toHtml hardcoded attributes stay hardcoded (no raw prop breakout)', () => {
|
|
|
|
|
test('form method is always POST regardless of any injected props', () => {
|
|
|
|
|
const { html } = toHtml({ heading: 'Join us', method: 'GET"><script>alert(1)</script>' } as any, '');
|
2026-07-14 06:44:21 -07:00
|
|
|
expect(html).toMatch(/<form action="[^"]*" method="POST"/);
|
2026-07-12 18:03:44 -07:00
|
|
|
expect(html).not.toContain('<script');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('email input type is always "email" regardless of any injected props', () => {
|
|
|
|
|
const { html } = toHtml({ type: 'text"><img src=x onerror=alert(1)>' } as any, '');
|
|
|
|
|
expect(html).toContain('<input type="email"');
|
|
|
|
|
expect(html).not.toContain('<img');
|
|
|
|
|
expect(html).not.toContain('onerror=');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('layout enum only ever feeds one of two fixed literal style strings, never raw', () => {
|
|
|
|
|
const { html: inlineHtml } = toHtml({ layout: 'inline' }, '');
|
|
|
|
|
const { html: stackedHtml } = toHtml({ layout: 'stacked' }, '');
|
|
|
|
|
expect(inlineHtml).toContain('flex-direction:row');
|
|
|
|
|
expect(stackedHtml).toContain('flex-direction:column');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('malicious layout value cannot inject raw CSS/attribute breakout (falls through the isInline boolean check to the stacked literal)', () => {
|
|
|
|
|
const { html } = toHtml({ layout: '"><script>alert(1)</script>' as any }, '');
|
|
|
|
|
expect(html).not.toContain('<script');
|
|
|
|
|
expect(html).toContain('flex-direction:column');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('normal render still produces expected structure', () => {
|
|
|
|
|
const { html } = toHtml({ heading: 'Subscribe', placeholder: 'you@example.com', buttonText: 'Go' }, '');
|
|
|
|
|
expect(html).toContain('Subscribe');
|
|
|
|
|
expect(html).toContain('placeholder="you@example.com"');
|
|
|
|
|
expect(html).toContain('>Go<');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-14 06:44:21 -07:00
|
|
|
|
|
|
|
|
// F1: SubscribeForm previously emitted `<form method="POST">` with no action
|
|
|
|
|
// at all -- a published subscribe form silently did nothing on submit.
|
|
|
|
|
// Wired through the same relay contract as ContactForm/FormContainer
|
|
|
|
|
// (utils/form-relay-wiring.ts) so setting a recipient makes it functional.
|
|
|
|
|
describe('SubscribeForm.toHtml is functional (not a dead POST)', () => {
|
|
|
|
|
test('without a recipient: still has a real (non-empty) action -- "#" fallback, not a bare method="POST"', () => {
|
|
|
|
|
const { html } = toHtml({}, '');
|
|
|
|
|
expect(html).toMatch(/<form action="#" method="POST"/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('with recipientEmail: emits the relay marker, placeholder action, and honeypot -- a working submission path', () => {
|
|
|
|
|
const { html } = toHtml({ recipientEmail: 'news@example.com', thankYouUrl: '/thanks' }, '', 'node-sub1');
|
|
|
|
|
expect(html).toMatch(/<!--WHP-FORM id="F_[0-9a-z]+" recipient="news@example.com" thankyou="\/thanks"-->/);
|
|
|
|
|
expect(html).toMatch(/action="__WHP_FORM_ACTION__F_[0-9a-z]+__"/);
|
|
|
|
|
expect(html).toContain('name="_gotcha"');
|
|
|
|
|
const mid = html.match(/id="(F_[0-9a-z]+)"/)![1];
|
|
|
|
|
expect(html).toContain(`__WHP_FORM_ACTION__${mid}__`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('the email input keeps its name="email" so the relay receives it', () => {
|
|
|
|
|
const { html } = toHtml({ recipientEmail: 'news@example.com' }, '', 'node-sub2');
|
|
|
|
|
expect(html).toContain('name="email"');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('SubscribeForm.toHtml box-model style passthrough', () => {
|
|
|
|
|
test('margin/border/box-shadow/opacity flow through via the style prop', () => {
|
|
|
|
|
const { html } = toHtml({ style: { marginTop: '16px', border: '1px solid #ddd', boxShadow: '0 2px 6px rgba(0,0,0,.15)', opacity: '0.85' } }, '');
|
|
|
|
|
expect(html).toContain('margin-top:16px');
|
|
|
|
|
expect(html).toContain('border:1px solid #ddd');
|
|
|
|
|
expect(html).toContain('opacity:0.85');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('SubscribeForm.craft.props includes animation/visibility defaults', () => {
|
|
|
|
|
test('has blank/false defaults', () => {
|
|
|
|
|
expect(SubscribeForm.craft!.props).toMatchObject({
|
|
|
|
|
animation: '', animationDelay: '', hideOnDesktop: false, hideOnTablet: false, hideOnMobile: false,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|