deploy
This commit is contained in:
parent
3fa95819e3
commit
e9a06417ad
32 changed files with 1531 additions and 407 deletions
97
frontend/src/lib/travel-params.test.ts
Normal file
97
frontend/src/lib/travel-params.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue