import { coord, coordError, coordsOf, hasCoords, latitude, longitude, } from '@/lib/coords'; /** * Coordinate validation. * * The first block is the whole reason this module exists: the guard it replaces * was `Number.isFinite(Number(value))`, and every case below passed it, because * null, '' and false all coerce to 0 and 0 is finite. A missing pin became * latitude 0 / longitude 0 and was priced as an 11,000 km run. */ const COIMBATORE = { latitude: 11.0168, longitude: 76.9558 }; describe('coord', () => { describe('the empty values that used to pass as zero', () => { it.each([ ['null', null], ['undefined', undefined], ['empty string', ''], ['false', false], ['true', true], ['an empty array', []], ])('rejects %s', (_label, value) => { // Number(value) is 0 for every one of these, and 0 is finite — which is // exactly how they got through before. expect(coord(value, 'lat')).toBeNull(); expect(coord(value, 'lng')).toBeNull(); }); it('confirms the coercion that caused the bug', () => { expect(Number(null)).toBe(0); expect(Number.isFinite(Number(null))).toBe(true); }); }); describe('zero', () => { it('rejects a literal zero on both axes', () => { // (0, 0) is a real point in the Gulf of Guinea. In this product it is // only ever the residue of a failed geocode. expect(coord(0, 'lat')).toBeNull(); expect(coord(0, 'lng')).toBeNull(); expect(coord('0', 'lat')).toBeNull(); }); }); describe('unparseable values', () => { it.each(['abc', 'N/A', '--', 'NaN', {}])('rejects %p', (value) => { expect(coord(value, 'lat')).toBeNull(); }); }); describe('range', () => { it('accepts the poles and the antimeridian exactly', () => { expect(coord(90, 'lat')).toBe(90); expect(coord(-90, 'lat')).toBe(-90); expect(coord(180, 'lng')).toBe(180); expect(coord(-180, 'lng')).toBe(-180); }); it('rejects a latitude past the poles', () => { expect(coord(90.1, 'lat')).toBeNull(); expect(coord(-91, 'lat')).toBeNull(); }); it('rejects a longitude past the antimeridian', () => { expect(coord(180.5, 'lng')).toBeNull(); expect(coord(-181, 'lng')).toBeNull(); }); it('catches a transposition only when the longitude exceeds 90', () => { // The honest limit of a range check. Kolkata is (22.5726, 88.3639) and // Dibrugarh is (27.4728, 94.9120) — transpose the second and the // "latitude" becomes 94.9, past the pole, so it is caught. expect(coord(94.912, 'lat')).toBeNull(); expect(hasCoords(94.912, 27.4728)).toBe(false); }); it('CANNOT catch a transposed Coimbatore pin — documented limitation', () => { // 76.9558 is a perfectly legal latitude (northern Siberia), so a // transposed Coimbatore pin passes every check in this module. Range // validation only catches a swap when one axis leaves the other's // domain, i.e. |longitude| > 90. Anything west of 90°E transposes // silently. // // Pinned as a test so nobody reads `coord` as swap-proof. Catching this // needs a plausibility check against the operating region (see // geocodingService.isWithinIndia), not a bounds check. expect(coord(COIMBATORE.longitude, 'lat')).toBe(76.9558); expect(hasCoords(COIMBATORE.longitude, COIMBATORE.latitude)).toBe(true); }); it('rejects an unknown axis rather than guessing a bound', () => { expect(coord(11.0168, 'elevation')).toBeNull(); }); }); describe('real coordinates', () => { it('passes a Coimbatore pin through unchanged', () => { expect(coord(COIMBATORE.latitude, 'lat')).toBe(11.0168); expect(coord(COIMBATORE.longitude, 'lng')).toBe(76.9558); }); it('parses a numeric string, which is how a sheet cell arrives', () => { expect(coord('11.0168', 'lat')).toBe(11.0168); expect(coord(' 76.9558 ', 'lng')).toBe(76.9558); }); it('accepts southern and western hemispheres', () => { expect(coord(-33.8688, 'lat')).toBe(-33.8688); expect(coord(-74.006, 'lng')).toBe(-74.006); }); }); describe('axis helpers', () => { it('applies the latitude bound', () => { expect(latitude(91)).toBeNull(); expect(latitude(11.0168)).toBe(11.0168); }); it('applies the longitude bound', () => { expect(longitude(76.9558)).toBe(76.9558); expect(longitude(181)).toBeNull(); }); }); }); describe('hasCoords', () => { it('needs both halves', () => { expect(hasCoords(11.0168, 76.9558)).toBe(true); expect(hasCoords(11.0168, null)).toBe(false); expect(hasCoords(null, 76.9558)).toBe(false); expect(hasCoords(11.0168, 0)).toBe(false); }); it('rejects the pair the geocoding shim used to produce', () => { // geocodingService returned 0 from geometry.location.lat() when a place // had no coordinates. This is that pair. expect(hasCoords(0, 0)).toBe(false); }); }); describe('coordsOf', () => { it('reads the API/form shape', () => { expect(coordsOf(COIMBATORE)).toEqual({ latitude: 11.0168, longitude: 76.9558 }); }); it('reads the Leaflet/map-picker shape', () => { expect(coordsOf({ lat: 11.0168, lng: 76.9558 })).toEqual({ latitude: 11.0168, longitude: 76.9558, }); }); it('returns null when either half is unusable', () => { expect(coordsOf({ latitude: 11.0168, longitude: null })).toBeNull(); expect(coordsOf({ latitude: 0, longitude: 0 })).toBeNull(); expect(coordsOf({ latitude: '', longitude: '' })).toBeNull(); }); it('returns null for a non-object', () => { expect(coordsOf(null)).toBeNull(); expect(coordsOf(undefined)).toBeNull(); expect(coordsOf('11.0168,76.9558')).toBeNull(); }); it('prefers the explicit spelling when both are present', () => { expect(coordsOf({ latitude: 11.0168, lat: 99, longitude: 76.9558, lng: 99 })).toEqual({ latitude: 11.0168, longitude: 76.9558, }); }); }); describe('coordError', () => { it('names a missing pin rather than a number problem', () => { expect(coordError('', 'lat')).toBe('Navigate cannot work without a pin'); expect(coordError(null, 'lng')).toBe('Navigate cannot work without a pin'); expect(coordError(0, 'lat')).toBe('Navigate cannot work without a pin'); }); it('names a number problem when it is one', () => { expect(coordError('abc', 'lat')).toBe('Enter a number'); expect(coordError(true, 'lat')).toBe('Enter a number'); }); it('names the axis and its bounds when out of range', () => { expect(coordError(91, 'lat')).toBe('Latitude must be between -90 and 90'); expect(coordError(181, 'lng')).toBe('Longitude must be between -180 and 180'); }); it('is null for a good coordinate', () => { expect(coordError(11.0168, 'lat')).toBeNull(); expect(coordError(76.9558, 'lng')).toBeNull(); }); });