updated the test
This commit is contained in:
179
tests/lib/distance.test.js
Normal file
179
tests/lib/distance.test.js
Normal file
@@ -0,0 +1,179 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user