feat(builder): FORMS package -- field editor, functional Subscribe/Search, box-model+anim rollout
- FormStylePanel: ContactForm field editor (add/remove/reorder via ArrayPropEditor + manual move up/down) covering label/name/placeholder/ type (full sanitizeInputType allowlist + textarea/select)/required/ options. - SubscribeForm.toHtml: was a dead `<form method="POST">` with no action at all -- wired through the same relayFormWiring contract as ContactForm/ FormContainer so a recipientEmail makes it actually submit (marker + placeholder action + honeypot), falling back to action="#" otherwise. - SearchBar: was purely decorative (no action/method/input name) -- now a real GET form (configurable target, default "/") with input name="q", safeUrl-guarded against javascript:/vbscript: breakout. - Box-model (margin/padding per-side, border, shadow, opacity), entrance animation, and hide-on-device controls added to FormStylePanel and rolled out (blank/false craft.props defaults) across ContactForm, FormContainer, InputField, TextareaField, FormButton, SubscribeForm, SearchBar. No toHtml changes needed for animation/visibility -- html-export.ts's buildDataAttrs() already emits data-animation/ data-hide-* generically from these prop names. - Extended toHtml tests for all 7 components: field-type rendering (incl. textarea/select), type-attribute XSS sanitization, relay/GET functional wiring, box-model style passthrough, craft.props defaults. npx vitest run: 689/689 passed. npm run build: green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,3 +30,46 @@ describe('SearchBar.toHtml XSS hardening (placeholder/buttonText/showButton)', (
|
||||
expect(html).toMatch(/border-radius:(8px 0 0 8px|8px)/);
|
||||
});
|
||||
});
|
||||
|
||||
// F2: SearchBar was purely decorative -- no action/method/input name, so
|
||||
// submitting did nothing. It now emits a real GET form.
|
||||
describe('SearchBar.toHtml is a functional GET search form (not decorative)', () => {
|
||||
test('defaults to a GET form action="/" with the query input named "q"', () => {
|
||||
const { html } = toHtml({}, '');
|
||||
expect(html).toMatch(/<form role="search" action="\/" method="GET"/);
|
||||
expect(html).toContain('<input type="search" name="q"');
|
||||
});
|
||||
|
||||
test('a configured action (real search-results page) is used verbatim', () => {
|
||||
const { html } = toHtml({ action: '/search' }, '');
|
||||
expect(html).toContain('action="/search"');
|
||||
});
|
||||
|
||||
test('a javascript: action is blocked via safeUrl and falls back to "/"', () => {
|
||||
const { html } = toHtml({ action: 'javascript:alert(1)' }, '');
|
||||
expect(html).toContain('action="/"');
|
||||
expect(html).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
test('an empty/whitespace action falls back to "/"', () => {
|
||||
const { html } = toHtml({ action: ' ' }, '');
|
||||
expect(html).toContain('action="/"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchBar.toHtml box-model style passthrough', () => {
|
||||
test('margin/border/box-shadow/opacity flow through via the style prop', () => {
|
||||
const { html } = toHtml({ style: { marginBottom: '14px', border: '1px solid #aaa', boxShadow: '0 1px 4px rgba(0,0,0,.1)', opacity: '0.9' } }, '');
|
||||
expect(html).toContain('margin-bottom:14px');
|
||||
expect(html).toContain('border:1px solid #aaa');
|
||||
expect(html).toContain('opacity:0.9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchBar.craft.props includes animation/visibility defaults', () => {
|
||||
test('has blank/false defaults', () => {
|
||||
expect(SearchBar.craft!.props).toMatchObject({
|
||||
animation: '', animationDelay: '', hideOnDesktop: false, hideOnTablet: false, hideOnMobile: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user