Files
doormilxpress_astryx/tests/lib/distance.test.js
2026-08-27 13:40:48 +05:30

180 lines
7.4 KiB
JavaScript

import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance';
/**
* Distance and charge for a delivery leg.
*
* OSRM is the routed source; the Haversine + 1.3 multiplier is the fallback.
* The behaviour that matters commercially is that the fallback NEVER reports a
* duration — a straight-line estimate has no honest ETA, and showing a made-up
* one to an operator is worse than showing none.
*/
const osrmOk = (distanceMeters, durationSeconds) => ({
ok: true,
json: async () => ({ routes: [{ distance: distanceMeters, duration: durationSeconds }] })
});
const BLR = { latitude: 12.9716, longitude: 77.5946 };
const IND = { latitude: 12.9352, longitude: 77.6245 };
describe('calculateDrivingDistance', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
describe('coordinate validation', () => {
it.each([
['a missing origin latitude', { longitude: 77.5946 }, IND],
['a missing origin longitude', { latitude: 12.9716 }, IND],
['a missing destination latitude', BLR, { longitude: 77.6245 }],
['a missing destination longitude', BLR, { latitude: 12.9352 }],
['a null origin', null, IND],
['an undefined destination', BLR, undefined],
['a null latitude value', { latitude: null, longitude: 77.5946 }, IND]
])('should reject %s', async (_label, origin, destination) => {
await expect(calculateDrivingDistance(origin, destination)).rejects.toThrow('Invalid coordinates');
expect(global.fetch).not.toHaveBeenCalled();
});
it('should accept zero coordinates, which are a real position', async () => {
global.fetch.mockResolvedValue(osrmOk(1000, 120));
await expect(
calculateDrivingDistance({ latitude: 0, longitude: 0 }, { latitude: 0, longitude: 1 })
).resolves.toEqual(expect.any(Number));
});
});
describe('OSRM routed distance', () => {
it('should return the routed distance rounded to whole kilometres', async () => {
global.fetch.mockResolvedValue(osrmOk(8200, 900));
await expect(calculateDrivingDistance(BLR, IND)).resolves.toBe(8);
});
it('should cache the routed duration in whole minutes', async () => {
global.fetch.mockResolvedValue(osrmOk(8200, 900));
await calculateDrivingDistance(BLR, IND);
expect(getLastRouteDurationMin()).toBe(15);
});
it('should request lon,lat order — the order OSRM expects', async () => {
// Swapping to lat,lon silently returns a route between two other places.
global.fetch.mockResolvedValue(osrmOk(8200, 900));
await calculateDrivingDistance(BLR, IND);
const url = global.fetch.mock.calls[0][0];
expect(url).toContain('/route/v1/driving/77.5946,12.9716;77.6245,12.9352');
expect(url).toContain('overview=false');
});
it('should use the public OSRM host by default', async () => {
global.fetch.mockResolvedValue(osrmOk(8200, 900));
await calculateDrivingDistance(BLR, IND);
expect(global.fetch.mock.calls[0][0]).toContain('https://router.project-osrm.org');
});
it('should honour a VITE_OSRM_URL override for a self-hosted router', async () => {
globalThis.__VITE_IMPORT_META__ = { env: { VITE_OSRM_URL: 'http://osrm.internal:5000' } };
global.fetch.mockResolvedValue(osrmOk(8200, 900));
let reloaded;
jest.isolateModules(() => {
// eslint-disable-next-line global-require
reloaded = require('@/lib/distance').calculateDrivingDistance;
});
await reloaded(BLR, IND);
expect(global.fetch.mock.calls[0][0]).toContain('http://osrm.internal:5000');
});
it('should report no duration when OSRM omits one', async () => {
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ routes: [{ distance: 8200 }] }) });
await calculateDrivingDistance(BLR, IND);
expect(getLastRouteDurationMin()).toBeNull();
});
});
describe('Haversine fallback', () => {
it('should fall back when OSRM answers with an error status', async () => {
global.fetch.mockResolvedValue({ ok: false, status: 502, json: async () => ({}) });
const distance = await calculateDrivingDistance(BLR, IND);
expect(distance).toEqual(expect.any(Number));
expect(distance).toBeGreaterThan(0);
});
it('should fall back when the network call throws', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
global.fetch.mockRejectedValue(new Error('ECONNREFUSED'));
await expect(calculateDrivingDistance(BLR, IND)).resolves.toEqual(expect.any(Number));
expect(warn).toHaveBeenCalled();
});
it('should fall back when OSRM returns no routes', async () => {
global.fetch.mockResolvedValue({ ok: true, json: async () => ({ routes: [] }) });
await expect(calculateDrivingDistance(BLR, IND)).resolves.toEqual(expect.any(Number));
});
it('should apply the 1.3 road multiplier to the straight-line distance', async () => {
// One degree of latitude is 111.19 km great-circle; x1.3 = 144.5 -> 145.
global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) });
const distance = await calculateDrivingDistance(
{ latitude: 0, longitude: 0 },
{ latitude: 1, longitude: 0 }
);
expect(distance).toBe(145);
});
it('should return zero for two identical points', async () => {
global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) });
await expect(calculateDrivingDistance(BLR, { ...BLR })).resolves.toBe(0);
});
it('should report NO duration for a fallback estimate', async () => {
// A straight-line guess has no honest ETA. Leaving a stale routed value
// in place would attach a real-looking duration to a fabricated distance.
global.fetch.mockResolvedValueOnce(osrmOk(8200, 900));
await calculateDrivingDistance(BLR, IND);
expect(getLastRouteDurationMin()).toBe(15);
global.fetch.mockResolvedValueOnce({ ok: false, json: async () => ({}) });
await calculateDrivingDistance(BLR, IND);
expect(getLastRouteDurationMin()).toBeNull();
});
it('should be symmetric — the distance does not depend on direction', async () => {
global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) });
const there = await calculateDrivingDistance(BLR, IND);
const back = await calculateDrivingDistance(IND, BLR);
expect(there).toBe(back);
});
});
});
describe('calculateTotalCharge', () => {
it('should charge only the base price below the minimum distance', () => {
expect(calculateTotalCharge(3, 30, 6, 8)).toBe(30);
});
it('should charge only the base price at exactly the minimum distance', () => {
// Boundary: minKm is inclusive of the base fare.
expect(calculateTotalCharge(8, 30, 6, 8)).toBe(30);
});
it('should add the per-km rate beyond the minimum', () => {
expect(calculateTotalCharge(12, 30, 6, 8)).toBe(54);
});
it('should add a partial kilometre proportionally', () => {
expect(calculateTotalCharge(8.5, 30, 6, 8)).toBeCloseTo(33, 6);
});
it('should charge the base price for a zero-distance leg', () => {
expect(calculateTotalCharge(0, 30, 6, 8)).toBe(30);
});
it('should charge the base price when there is no minimum', () => {
expect(calculateTotalCharge(0, 30, 6, 0)).toBe(30);
});
it('should scale linearly for a long leg', () => {
expect(calculateTotalCharge(108, 30, 6, 8)).toBe(630);
});
});