- 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.
59 lines
4.9 KiB
JavaScript
59 lines
4.9 KiB
JavaScript
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'));
|
||
});
|