Files
ridebus-pwa/test/routing.test.mjs
Zakhar Timoshenko 6fe039cefa feat: add StopsMap and WalkingDirections components for enhanced map functionality
- Implemented StopsMap component to display bus stops on a map using MapLibre GL.
- Integrated walking directions feature in WalkingDirections component to calculate routes from user location to selected bus stop.
- Added geo utility functions for coordinate validation and distance calculation.
- Created route grouping and stop filtering logic in routeGroups module.
- Developed custom hooks for managing stop routes with React Query.
- Added tests for routing and stop functionalities to ensure reliability.
2026-10-08 00:18:10 +03:00

59 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRideBusClient, normalizeRoutes, normalizeRouteStops, parseRequest } from '../server/ridebus.mjs';
import { normalizeWalking } from '../server/walking.mjs';
import { groupRoutes, stopsForRoutes } from '../src/lib/routeGroups.ts';
const geometry = { type: 'LineString', coordinates: [[28.783, 55.486], [28.7855, 55.4865]] };
const foot = { code: 'Ok', routes: [{ distance: 230, duration: 165.4, geometry, legs: [{ private: 'unneeded' }] }] };
test('route and walking parameters require bounded coordinates and fixed identifiers', () => {
assert.deepEqual(parseRequest(new URLSearchParams('resource=routes&cityId=1')), { resource: 'routes', cityId: 1 });
assert.deepEqual(parseRequest(new URLSearchParams('resource=routeStops&cityId=1&routeId=10')), { resource: 'routeStops', cityId: 1, routeId: 10 });
assert.deepEqual(parseRequest(new URLSearchParams('resource=walk&cityId=1&stopId=28&lat=0&lon=0')), { resource: 'walk', cityId: 1, stopId: 28, lat: 0, lon: 0 });
for (const params of ['resource=routeStops&cityId=1&routeId=../10', 'resource=walk&cityId=1&stopId=28&lat=91&lon=28', 'resource=walk&cityId=1&stopId=28&lat=55&lon=-181', 'resource=walk&cityId=1&lat=55&lon=28', 'resource=routes&cityId=-1']) assert.throws(() => parseRequest(new URLSearchParams(params)), error => error.status === 400);
});
test('route numbers group directions but distinct boarding points retain their identities', () => {
const routes = normalizeRoutes([{ id: 10, route_number: '4', name: 'А → Б' }, { id: 11, route_number: '№4', name: 'Б → А' }, { id: 12, route_number: '10', name: 'В → Г' }, { id: 13, route_number: '2', name: 'А → Г', is_active: false }]);
assert.deepEqual(groupRoutes(routes).map(group => [group.key, group.routes.map(route => route.id)]), [['4', [10, 11]], ['10', [12]]]);
const detail = normalizeRouteStops({ id: 10, route_number: '4', name: 'А → Б', stops: [
{ id: 1, name: 'Вокзал', description: 'В центр', sequence: 2, latitude: 55, longitude: 28 },
{ id: 2, name: 'Вокзал', description: 'Обратно', sequence: 1, latitude: 55.001, longitude: 28 },
] });
assert.deepEqual(detail.stops.map(stop => stop.id), [2, 1]);
const filtered = stopsForRoutes([{ ...detail.stops[0], direction: 'Уточнённое направление' }], [detail, detail]);
assert.deepEqual(filtered.map(stop => stop.id), [2, 1]);
assert.equal(filtered[0].direction, 'Уточнённое направление');
assert.equal(filtered[1].latitude, 55);
});
test('walking geometry and metrics are validated without inventing a path on failure', () => {
assert.deepEqual(normalizeWalking(foot), { geometry, distance: 230, duration: 165.4 });
assert.throws(() => normalizeWalking({ code: 'NoRoute' }), error => error.status === 404 && error.code === 'walking_not_found');
for (const route of [{ ...foot.routes[0], distance: null }, { ...foot.routes[0], duration: -1 }, { ...foot.routes[0], geometry: { type: 'LineString', coordinates: [[28, 55], [999, 55]] } }, { ...foot.routes[0], geometry: { type: 'LineString', coordinates: [[28, 55]] } }]) assert.throws(() => normalizeWalking({ code: 'Ok', routes: [route] }), error => error.status === 502);
});
test('walking goes through the verified foot endpoint and never enters origin caches', async () => {
const calls = [];
const client = createRideBusClient({ apiKey: 'private-key', fetchData: async (url, options) => {
calls.push([url, options]);
return { ok: true, json: async () => url.pathname.startsWith('/routing/') ? foot : { id: 28, latitude: 55.4869, longitude: 28.7856 } };
} });
const input = { resource: 'walk', cityId: 1, stopId: 28, lat: 55.486, lon: 28.783 };
await client.get(input); await client.get(input); await client.get({ ...input, lat: 55.487 });
const paths = calls.filter(([url]) => url.pathname.startsWith('/routing/'));
assert.equal(paths.length, 3);
assert.equal(paths[0][0].pathname, '/routing/foot/route/v1/foot/28.783000,55.486000;28.785600,55.486900');
assert.equal(paths[0][0].searchParams.get('geometries'), 'geojson');
assert.equal(paths[0][1].headers['X-City-Id'], '1');
assert.equal(paths[0][1].headers['X-API-Key'], 'private-key');
assert.equal(calls.filter(([url]) => url.pathname.startsWith('/api/v1/stops/')).length, 1);
});
test('a no-segment HTTP response maps to a safe walking error', async () => {
const client = createRideBusClient({ apiKey: 'key', fetchData: async url => url.pathname.startsWith('/routing/')
? { ok: false, status: 400, json: async () => ({ code: 'NoSegment', message: 'internal details' }) }
: { ok: true, json: async () => ({ latitude: 55, longitude: 28 }) } });
await assert.rejects(client.get({ resource: 'walk', cityId: 1, stopId: 28, lat: 55, lon: 28 }), error => error.code === 'walking_not_found' && !error.message.includes('internal'));
});