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:
@@ -126,3 +126,73 @@ describe('ContactForm.toHtml field type attribute sanitization', () => {
|
||||
expect(html).toContain('type="email"');
|
||||
});
|
||||
});
|
||||
|
||||
// F1: the field editor (FormStylePanel) can now create fields of every type
|
||||
// in sanitizeInputType's allowlist, plus textarea/select. Verify each
|
||||
// renders with the right control, label/for association, and required flag.
|
||||
describe('ContactForm.toHtml renders every configured field type/label/required', () => {
|
||||
const cases: { type: string; tag: string }[] = [
|
||||
{ type: 'text', tag: 'input' },
|
||||
{ type: 'email', tag: 'input' },
|
||||
{ type: 'tel', tag: 'input' },
|
||||
{ type: 'number', tag: 'input' },
|
||||
{ type: 'password', tag: 'input' },
|
||||
{ type: 'url', tag: 'input' },
|
||||
{ type: 'search', tag: 'input' },
|
||||
{ type: 'date', tag: 'input' },
|
||||
{ type: 'checkbox', tag: 'input' },
|
||||
{ type: 'radio', tag: 'input' },
|
||||
];
|
||||
|
||||
test.each(cases)('type=$type renders a sanitized <$tag type="$type"> with label + for/id wiring', ({ type, tag }) => {
|
||||
const fields = [{ type: type as any, label: `Field ${type}`, name: `f_${type}`, placeholder: '', required: true }];
|
||||
const { html } = toHtml({ fields }, '');
|
||||
expect(html).toContain(`<${tag}`);
|
||||
expect(html).toContain(`type="${type}"`);
|
||||
expect(html).toContain(`Field ${type}`);
|
||||
// required renders the input attribute AND the visual asterisk
|
||||
expect(html).toMatch(/ required/);
|
||||
expect(html).toContain('*</span>');
|
||||
const labelFor = html.match(/<label for="([^"]+)"/)![1];
|
||||
expect(html).toContain(`id="${labelFor}"`);
|
||||
});
|
||||
|
||||
test('type=textarea renders a <textarea>, not an <input>', () => {
|
||||
const fields = [{ type: 'textarea' as const, label: 'Message', name: 'message', placeholder: '', required: false }];
|
||||
const { html } = toHtml({ fields }, '');
|
||||
expect(html).toMatch(/<textarea[^>]*name="message"/);
|
||||
expect(html).not.toMatch(/<input[^>]*name="message"/);
|
||||
});
|
||||
|
||||
test('type=select renders a <select> with escaped <option> values from field.options', () => {
|
||||
const fields = [{ type: 'select' as const, label: 'Plan', name: 'plan', placeholder: 'Choose one', required: false, options: ['Basic', 'Pro', '"><script>alert(1)</script>'] }];
|
||||
const { html } = toHtml({ fields }, '');
|
||||
expect(html).toMatch(/<select[^>]*name="plan"/);
|
||||
expect(html).toContain('<option value="Basic">Basic</option>');
|
||||
expect(html).toContain('<option value="Pro">Pro</option>');
|
||||
expect(html).not.toContain('<script>alert(1)</script>');
|
||||
});
|
||||
|
||||
test('a non-required field omits both the required attribute and the asterisk', () => {
|
||||
const fields = [{ type: 'text' as const, label: 'Nickname', name: 'nickname', placeholder: '', required: false }];
|
||||
const { html } = toHtml({ fields }, '');
|
||||
expect(html).not.toMatch(/ required/);
|
||||
expect(html).not.toContain('*</span>');
|
||||
});
|
||||
});
|
||||
|
||||
// Box-model / animation / visibility rollout (common enh-batch pattern):
|
||||
// these are top-level props consumed generically by the export's
|
||||
// buildDataAttrs() -- this just confirms the defaults are present on
|
||||
// craft.props so the panel controls render and the props survive save/load.
|
||||
describe('ContactForm.craft.props includes animation/visibility defaults', () => {
|
||||
test('has blank/false defaults for animation, animationDelay, hideOnDesktop/Tablet/Mobile', () => {
|
||||
expect(ContactForm.craft!.props).toMatchObject({
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
hideOnDesktop: false,
|
||||
hideOnTablet: false,
|
||||
hideOnMobile: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user