import { describe, it, expect } from 'vitest'; import { feetToMeters, metersToFeet, formatNumber1dp, dimInFeet, dimInMeters, } from '@/components/yachts/yacht-dimensions'; describe('yacht-dimensions: ft ↔ m conversions', () => { it('feetToMeters: returns null for non-finite inputs', () => { expect(feetToMeters(null)).toBeNull(); expect(feetToMeters(undefined)).toBeNull(); expect(feetToMeters('')).toBeNull(); expect(feetToMeters('abc')).toBeNull(); expect(feetToMeters(NaN)).toBeNull(); }); it('metersToFeet: returns null for non-finite inputs', () => { expect(metersToFeet(null)).toBeNull(); expect(metersToFeet(undefined)).toBeNull(); expect(metersToFeet('')).toBeNull(); expect(metersToFeet('abc')).toBeNull(); expect(metersToFeet(NaN)).toBeNull(); }); it('feetToMeters: matches 1 ft = 0.3048 m within 1e-4', () => { // 1 / 3.28084 = 0.304800061... vs the SI definition 0.3048 m expect(feetToMeters(1)).toBeCloseTo(0.3048, 4); expect(feetToMeters(12.5)).toBeCloseTo(3.81, 2); expect(feetToMeters(50)).toBeCloseTo(15.24, 2); }); it('metersToFeet: produces inverse of feetToMeters within float precision', () => { expect(metersToFeet(0.3048)).toBeCloseTo(1, 4); expect(metersToFeet(3.81)).toBeCloseTo(12.5, 2); expect(metersToFeet(15.24)).toBeCloseTo(50, 2); }); describe('round-trip is lossless within helper precision', () => { const cases = [1, 5, 12.5, 25, 50, 120, 250]; cases.forEach((ft) => { it(`${ft} ft → m → ft preserves value`, () => { const m = feetToMeters(ft)!; const backToFt = metersToFeet(m)!; expect(backToFt).toBeCloseTo(ft, 6); }); }); const mCases = [0.5, 1, 3.81, 7.62, 15.24, 36.58]; mCases.forEach((m) => { it(`${m} m → ft → m preserves value`, () => { const ft = metersToFeet(m)!; const backToM = feetToMeters(ft)!; expect(backToM).toBeCloseTo(m, 6); }); }); }); describe('formatNumber1dp', () => { it('strips trailing .0', () => { expect(formatNumber1dp(12)).toBe('12'); expect(formatNumber1dp(12.5)).toBe('12.5'); expect(formatNumber1dp(3.812)).toBe('3.8'); expect(formatNumber1dp(null)).toBeNull(); }); }); describe('form-shape round-trip (4dp + trimZero, mirrors yacht-form.tsx)', () => { // The form persists string values, so we model the same shape the // input fields see. trimZero strips trailing zeros after the dot. const trimZero = (s: string) => (!s.includes('.') ? s : s.replace(/\.?0+$/, '')); const ftToM = (v: string): string => { const m = feetToMeters(v); return m === null ? '' : trimZero(m.toFixed(4)); }; const mToFt = (v: string): string => { const ft = metersToFeet(v); return ft === null ? '' : trimZero(ft.toFixed(4)); }; it('round-trip 1 ft → m → ft is lossless', () => { const m = ftToM('1'); expect(mToFt(m)).toBe('1'); }); it('round-trip 12.5 ft ↔ 3.81 m ↔ 12.5 ft', () => { const m = ftToM('12.5'); const ft = mToFt(m); // 12.5 / 3.28084 = 3.80999961... → "3.81" → 3.81 * 3.28084 = 12.4999... → "12.5" expect(ft).toBe('12.5'); }); it('round-trip 50 ft → 15.24 m → 50 ft', () => { expect(mToFt(ftToM('50'))).toBe('50'); }); it('round-trip 0.5 m → ft → 0.5 m', () => { expect(ftToM(mToFt('0.5'))).toBe('0.5'); }); it('empty string returns empty', () => { expect(ftToM('')).toBe(''); expect(mToFt('')).toBe(''); }); it('non-numeric returns empty', () => { expect(ftToM('abc')).toBe(''); expect(mToFt('abc')).toBe(''); }); }); describe('dimInFeet / dimInMeters: derive missing unit from sibling', () => { it('returns ft value when set', () => { expect(dimInFeet({ ft: 12.5, m: 3.81 })).toBe('12.5'); }); it('derives ft from m when ft is null', () => { expect(dimInFeet({ ft: null, m: 3.81 })).toBe('12.5'); }); it('returns m value when set', () => { expect(dimInMeters({ ft: 12.5, m: 3.81 })).toBe('3.8'); }); it('derives m from ft when m is null', () => { expect(dimInMeters({ ft: 12.5, m: null })).toBe('3.8'); }); it('returns null when both null', () => { expect(dimInFeet({ ft: null, m: null })).toBeNull(); expect(dimInMeters({ ft: null, m: null })).toBeNull(); }); }); });