diff --git a/apps/app/src/app/(app)/setup/components/OnboardingStepInput.test.ts b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.test.ts new file mode 100644 index 0000000000..36b797910a --- /dev/null +++ b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { cleanDomainInput, isValidDomain } from './OnboardingStepInput'; + +describe('cleanDomainInput', () => { + it('strips a trailing slash', () => { + expect(cleanDomainInput('example.com/')).toBe('example.com'); + }); + + it('strips multiple trailing slashes', () => { + expect(cleanDomainInput('example.com//')).toBe('example.com'); + }); + + it('strips the https:// protocol', () => { + expect(cleanDomainInput('https://example.com')).toBe('example.com'); + }); + + it('strips the http:// protocol', () => { + expect(cleanDomainInput('http://example.com')).toBe('example.com'); + }); + + it('strips a www. prefix', () => { + expect(cleanDomainInput('www.example.com')).toBe('example.com'); + }); + + it('strips protocol, www, and a trailing slash together', () => { + expect(cleanDomainInput('https://www.example.com/')).toBe('example.com'); + }); + + it('trims surrounding whitespace', () => { + expect(cleanDomainInput(' example.com ')).toBe('example.com'); + }); + + it('leaves an already-clean domain unchanged', () => { + expect(cleanDomainInput('example.com')).toBe('example.com'); + }); +}); + +describe('isValidDomain', () => { + it('accepts an empty string (optional field)', () => { + expect(isValidDomain('')).toBe(true); + }); + + it('accepts a plain domain', () => { + expect(isValidDomain('example.com')).toBe(true); + }); + + it('accepts a domain with a trailing slash', () => { + expect(isValidDomain('example.com/')).toBe(true); + }); + + it('accepts a domain with protocol, www, and a trailing slash', () => { + expect(isValidDomain('https://www.example.com/')).toBe(true); + }); + + it('accepts a subdomain', () => { + expect(isValidDomain('sub.example.com')).toBe(true); + }); + + it('rejects a value with no dot', () => { + expect(isValidDomain('example')).toBe(false); + }); + + it('rejects a value with a path', () => { + expect(isValidDomain('example.com/about')).toBe(false); + }); + + it('rejects a value with spaces', () => { + expect(isValidDomain('exa mple.com')).toBe(false); + }); +}); diff --git a/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx index 2a3febbb17..004e435e28 100644 --- a/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx +++ b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx @@ -533,12 +533,25 @@ const normalizeVendorName = (name: string): string => { .trim(); }; +// Helper to strip protocol, www, and trailing slashes from a website URL, +// leaving just the bare domain for display/validation +export const cleanDomainInput = (raw: string): string => { + return raw + .trim() + .replace(/^(https?:\/\/)+/gi, '') + .replace(/^(www\.)+/gi, '') + .trim() + .replace(/\/+$/, ''); +}; + // Helper to validate domain/URL format -const isValidDomain = (domain: string): boolean => { +export const isValidDomain = (domain: string): boolean => { if (!domain || domain.trim() === '') return true; // Empty is valid (optional field) - // Clean the input - const cleaned = domain.trim().toLowerCase(); + // Clean the input (also strips a trailing slash defensively, in case the + // value wasn't run through cleanDomainInput first) + const cleaned = cleanDomainInput(domain).toLowerCase(); + if (!cleaned) return true; // Domain regex: allows subdomains, requires at least one dot and valid TLD const domainRegex = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/; @@ -593,7 +606,7 @@ function SoftwareVendorInput({ if (onTouchedInvalidUrlChange) { const hasTouchedInvalid = customVendorsForCallback.some((vendor) => { if (!touchedUrls.has(vendor.name)) return false; - const url = (vendor.website || '').replace(/^https?:\/\//, '').replace(/^www\./, ''); + const url = cleanDomainInput(vendor.website || ''); return url.length > 0 && !isValidDomain(url); }); onTouchedInvalidUrlChange(hasTouchedInvalid); @@ -924,9 +937,7 @@ function SoftwareVendorInput({