203 lines
6.9 KiB
JavaScript
203 lines
6.9 KiB
JavaScript
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();
|
|
});
|
|
});
|