diff --git a/craft/src/components/basic/SearchBar.toHtml.test.ts b/craft/src/components/basic/SearchBar.toHtml.test.ts index 9b93acc..b379217 100644 --- a/craft/src/components/basic/SearchBar.toHtml.test.ts +++ b/craft/src/components/basic/SearchBar.toHtml.test.ts @@ -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(/
{ + 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, + }); + }); +}); diff --git a/craft/src/components/basic/SearchBar.tsx b/craft/src/components/basic/SearchBar.tsx index 78030a1..d072b66 100644 --- a/craft/src/components/basic/SearchBar.tsx +++ b/craft/src/components/basic/SearchBar.tsx @@ -1,19 +1,29 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; -import { escapeHtml, escapeAttr } from '../../utils/escape'; +import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape'; interface SearchBarProps { placeholder?: string; buttonText?: string; showButton?: boolean; + /** Where the search GET request is submitted -- a real search-results page + * if the site has one, or '/' (site root) by default. The query is sent + * as `?q=...`, the conventional param name search-results pages look for. */ + action?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } export const SearchBar: UserComponent = ({ placeholder = 'Search...', buttonText = 'Search', showButton = true, + action = '/', style = {}, }) => { const { @@ -27,6 +37,8 @@ export const SearchBar: UserComponent = ({ { if (ref) connect(drag(ref)); }} role="search" + action={action} + method="GET" onSubmit={(e) => e.preventDefault()} style={{ display: 'flex', @@ -51,6 +63,7 @@ export const SearchBar: UserComponent = ({ /> true, @@ -117,6 +136,7 @@ SearchBar.craft = { placeholder = 'Search...', buttonText = 'Search', showButton = true, + action = '/', style = {}, } = props; @@ -133,11 +153,19 @@ SearchBar.craft = { ? `` : ''; + // F2: previously a purely decorative -- no action/method/input + // name at all, so submitting did nothing. A real GET to `action` with the + // query in the conventional `q` param makes this a functioning search + // form on publish (routes to a real search-results page if the site has + // one, or reloads '/' with ?q=... by default). `safeUrl` blocks + // javascript:/vbscript:/data:text/html breakout via the action attribute. + const actionAttr = escapeAttr(safeUrl(action) || '/'); + return { - html: ` + html: `
- +
${btnHtml} `, diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts index 42cd974..d29b243 100644 --- a/craft/src/components/forms/ContactForm.toHtml.test.ts +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -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('*'); + const labelFor = html.match(/