2026-07-12 14:19:01 -07:00
|
|
|
import { describe, test, expect } from 'vitest';
|
|
|
|
|
import { SearchBar } from './SearchBar';
|
|
|
|
|
|
|
|
|
|
const toHtml = (SearchBar as any).toHtml;
|
|
|
|
|
|
|
|
|
|
describe('SearchBar.toHtml decorative icons (F2.5)', () => {
|
|
|
|
|
test('the input-adjacent search icon is aria-hidden', () => {
|
|
|
|
|
const { html } = toHtml({}, '');
|
|
|
|
|
const icons = html.match(/<i class="fa fa-search"[^>]*>/g) || [];
|
|
|
|
|
expect(icons.length).toBeGreaterThan(0);
|
|
|
|
|
icons.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"'));
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-12 18:03:44 -07:00
|
|
|
|
|
|
|
|
describe('SearchBar.toHtml XSS hardening (placeholder/buttonText/showButton)', () => {
|
|
|
|
|
test('a placeholder value with an attribute-breakout string cannot escape placeholder=""', () => {
|
|
|
|
|
const malicious = 'Search..." onmouseover="alert(1)';
|
|
|
|
|
const { html } = toHtml({ placeholder: malicious }, '');
|
|
|
|
|
expect(html).not.toMatch(/"\s+onmouseover="/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a buttonText value with a script tag is escaped as text content, not raw HTML', () => {
|
|
|
|
|
const malicious = '<script>alert(1)</script>';
|
|
|
|
|
const { html } = toHtml({ buttonText: malicious, showButton: true }, '');
|
|
|
|
|
expect(html).not.toContain('<script>alert(1)</script>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a non-boolean showButton (string "false") still yields fixed, safe border-radius values', () => {
|
|
|
|
|
const { html } = toHtml({ showButton: 'false' as any }, '');
|
|
|
|
|
expect(html).toMatch(/border-radius:(8px 0 0 8px|8px)/);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-14 06:44:21 -07:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|