Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
});
});
38 changes: 23 additions & 15 deletions apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
.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,}$/;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -924,9 +937,7 @@ function SoftwareVendorInput({
<div className="space-y-2 max-h-[400px] overflow-y-auto pr-2">
{customVendors.map((vendor) => {
// Strip protocol for display, we'll add it back on save
const displayValue = (vendor.website || '')
.replace(/^https?:\/\//, '')
.replace(/^www\./, '');
const displayValue = cleanDomainInput(vendor.website || '');

const isTouched = touchedUrls.has(vendor.name);
const isValid = isValidDomain(displayValue);
Expand All @@ -953,11 +964,8 @@ function SoftwareVendorInput({
type="text"
value={displayValue}
onChange={(e) => {
// Clean input: remove any protocol, www, and trim
let value = e.target.value
.replace(/^(https?:\/\/)+/gi, '') // Remove one or more https://
.replace(/^(www\.)+/gi, '') // Remove one or more www.
.trim();
// Clean input: remove any protocol, www, trailing slashes, and trim
const value = cleanDomainInput(e.target.value);
const fullUrl = value ? `https://${value}` : '';
handleCustomVendorWebsiteChange(vendor.name, fullUrl);

Expand All @@ -983,9 +991,9 @@ function SoftwareVendorInput({
// Get current value from form state
const currentVendors = (form.watch('customVendors') as CustomVendor[] | undefined) || [];
const currentVendor = currentVendors.find((v) => v.name === vendor.name);
const currentValue = (currentVendor?.website || '')
.replace(/^https?:\/\//, '')
.replace(/^www\./, '');
const currentValue = cleanDomainInput(
currentVendor?.website || '',
);
const isValid = isValidDomain(currentValue);
// Only mark as touched if invalid (to show error)
if (!isValid && currentValue.length > 0) {
Expand Down
Loading