This commit is contained in:
Andras Schmelczer 2026-05-15 08:17:05 +01:00
parent 3fa95819e3
commit e9a06417ad
32 changed files with 1531 additions and 407 deletions

View file

@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest';
import type { TravelTimeEntry } from '../hooks/useTravelTime';
import { buildTravelParam, dedupeTravelTimeEntries } from './travel-params';
const bankMedian: TravelTimeEntry = {
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [0, 60],
useBest: false,
};
describe('travel-params', () => {
it('deduplicates travel entries by backend key and keeps the tightest range', () => {
const entries = dedupeTravelTimeEntries([
bankMedian,
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank tube station',
timeRange: [15, 45],
useBest: false,
},
{
mode: 'walking',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [0, 20],
useBest: false,
},
]);
expect(entries).toEqual([
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [15, 45],
useBest: false,
},
{
mode: 'walking',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [0, 20],
useBest: false,
},
]);
});
it('serializes deduplicated entries before backend requests', () => {
expect(
buildTravelParam([
bankMedian,
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [10, 50],
useBest: false,
},
])
).toBe('transit:bank-tube-station:10:50');
});
it('keeps duplicate blank entries because they are editable placeholders', () => {
const blank: TravelTimeEntry = {
mode: 'transit',
slug: '',
label: '',
timeRange: null,
useBest: false,
};
expect(dedupeTravelTimeEntries([blank, blank])).toHaveLength(2);
});
it('uses an unbounded range when excluding a deduplicated travel filter', () => {
expect(
buildTravelParam(
[
bankMedian,
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [10, 50],
useBest: false,
},
],
'tt_transit_bank-tube-station',
true
)
).toBe('transit:bank-tube-station:0:1440');
});
});

View file

@ -1,5 +1,44 @@
import type { TravelTimeEntry } from '../hooks/useTravelTime';
function mergeTimeRanges(
current: [number, number] | null,
next: [number, number] | null
): [number, number] | null {
if (!current) return next;
if (!next) return current;
return [Math.max(current[0], next[0]), Math.min(current[1], next[1])];
}
export function dedupeTravelTimeEntries(entries: TravelTimeEntry[]): TravelTimeEntry[] {
const result: TravelTimeEntry[] = [];
const indexByKey = new Map<string, number>();
for (const entry of entries) {
if (!entry.slug) {
result.push(entry);
continue;
}
const key = `${entry.mode}:${entry.slug}`;
const existingIndex = indexByKey.get(key);
if (existingIndex == null) {
indexByKey.set(key, result.length);
result.push({ ...entry });
continue;
}
const existing = result[existingIndex];
result[existingIndex] = {
...existing,
label: existing.label || entry.label,
timeRange: mergeTimeRanges(existing.timeRange, entry.timeRange),
useBest: existing.useBest || entry.useBest,
};
}
return result;
}
export function buildTravelParam(
entries: TravelTimeEntry[],
excludeFieldKey?: string,
@ -7,7 +46,7 @@ export function buildTravelParam(
): string {
const segments: string[] = [];
for (const entry of entries) {
for (const entry of dedupeTravelTimeEntries(entries)) {
if (!entry.slug) continue;
let segment = `${entry.mode}:${entry.slug}`;

View file

@ -99,6 +99,45 @@ describe('url-state', () => {
expect(params.getAll('tt')).toEqual(['bicycle:bank:Bank:5:25']);
});
it('deduplicates travel-time URL params with the tightest range', () => {
window.history.replaceState(
{},
'',
'/?tt=transit:bank-tube-station:Bank:0:60&tt=transit:bank-tube-station:Bank:10:45'
);
const state = parseUrlState();
expect(state.travelTime?.entries).toEqual([
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
timeRange: [10, 45],
useBest: false,
},
]);
const params = stateToParams(null, {}, [], new Set(), 'area', [
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
useBest: false,
timeRange: [0, 60],
},
{
mode: 'transit',
slug: 'bank-tube-station',
label: 'Bank',
useBest: false,
timeRange: [10, 45],
},
]);
expect(params.getAll('tt')).toEqual(['transit:bank-tube-station:Bank:10:45']);
});
it('round-trips an explicitly empty POI selection', () => {
const params = stateToParams(null, {}, [], new Set(), 'area');