import { beforeEach, describe, expect, it } from 'vitest'; import type { FeatureMeta } from '../types'; import { parseUrlState, stateToParams } from './url-state'; import { INITIAL_VIEW_STATE } from './consts'; import { createSchoolFilterKey } from './school-filter'; import { createSpecificCrimeFilterKey } from './crime-filter'; import { createElectionVoteShareFilterKey } from './election-filter'; import { createEthnicityFilterKey } from './ethnicity-filter'; import { POI_COUNT_2KM_FILTER_NAME, TRANSPORT_DISTANCE_FILTER_NAME, createPoiDistanceFilterKey, createPoiFilterKey, } from './poi-distance-filter'; describe('url-state', () => { beforeEach(() => { window.history.replaceState({}, '', '/'); }); it('parses view, filters, POIs, tab, postcode, and travel-time params', () => { window.history.replaceState( {}, '', '/?lat=51.5074&lon=-0.1278&zoom=12.5&filter=Last%20known%20price:100000:500000&filter=Property%20type:Flat|House&poi=supermarket&tab=properties&pc=SW1A%201AA&tt=transit:kings-cross:Kings%20Cross:b:0:30' ); const state = parseUrlState(); expect(state.viewState).toEqual({ latitude: 51.5074, longitude: -0.1278, zoom: 12.5, pitch: 0, }); expect(state.filters).toEqual({ 'Last known price': [100000, 500000], 'Property type': ['Flat', 'House'], }); expect(state.poiCategories).toEqual(new Set(['supermarket'])); expect(state.tab).toBe('properties'); expect(state.postcode).toBe('SW1A 1AA'); expect(state.travelTime?.entries).toEqual([ { mode: 'transit', slug: 'kings-cross', label: 'Kings Cross', timeRange: [0, 30], useBest: true, noChange: false, noBuses: false, }, ]); }); it('leaves POIs unselected when URL params are omitted', () => { const state = parseUrlState(); expect(state.viewState).toEqual(INITIAL_VIEW_STATE); expect(state.filters).toEqual({}); expect(state.poiCategories).toEqual(new Set()); expect(state.tab).toBe('area'); }); it('serializes map state and active filters into stable URL params', () => { const features: FeatureMeta[] = [ { name: 'Last known price', type: 'numeric' }, { name: 'Property type', type: 'enum', values: ['Flat', 'House'] }, ]; const params = stateToParams( { latitude: 51.50742, longitude: -0.12781, zoom: 12.47 }, { 'Last known price': [100000, 500000], 'Property type': ['Flat', 'House'], }, features, new Set(['supermarket']), 'properties', [ { mode: 'bicycle', slug: 'bank', label: 'Bank', useBest: false, timeRange: [5, 25], }, ] ); expect(params.get('lat')).toBe('51.5074'); expect(params.get('lon')).toBe('-0.1278'); expect(params.get('zoom')).toBe('12.5'); expect(params.getAll('filter')).toEqual([ 'Last known price:100000:500000', 'Property type:Flat|House', ]); expect(params.getAll('poi')).toEqual(['supermarket']); expect(params.get('tab')).toBe('properties'); 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, noChange: false, noBuses: 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'); expect(params.getAll('poi')).toEqual(['__none']); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.poiCategories).toEqual(new Set()); }); it('round-trips overlay selections', () => { const params = stateToParams( null, {}, [], new Set(), 'area', undefined, undefined, new Set(['noise', 'crime-hotspots']) ); expect(params.getAll('overlay')).toEqual(['noise', 'crime-hotspots']); window.history.replaceState({}, '', `/?${params.toString()}&overlay=unknown`); const state = parseUrlState(); expect(state.overlays).toEqual(new Set(['noise', 'crime-hotspots'])); }); it('round-trips repeated school filters with dedicated URL params', () => { const schoolOne = createSchoolFilterKey('primary', 'good', 2, 1); const schoolTwo = createSchoolFilterKey('secondary', 'outstanding', 5, 2); const params = stateToParams( null, { [schoolOne]: [1, 10], [schoolTwo]: [2, 15], }, [], new Set(), 'area' ); expect(params.getAll('school')).toEqual([ 'primary:good:2:1:10', 'secondary:outstanding:5:2:15', ]); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createSchoolFilterKey('primary', 'good', 2, 0)]: [1, 10], [createSchoolFilterKey('secondary', 'outstanding', 5, 1)]: [2, 15], }); }); it('round-trips repeated specific crime filters with dedicated URL params', () => { const burglary = createSpecificCrimeFilterKey('Burglary (avg/yr)', 1); const vehicleCrime = createSpecificCrimeFilterKey('Vehicle crime (avg/yr)', 2); const params = stateToParams( null, { [burglary]: [0, 5], [vehicleCrime]: [1, 10], }, [], new Set(), 'area' ); expect(params.getAll('crime')).toEqual([ 'Burglary (avg/yr):0:5', 'Vehicle crime (avg/yr):1:10', ]); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createSpecificCrimeFilterKey('Burglary (avg/yr)', 0)]: [0, 5], [createSpecificCrimeFilterKey('Vehicle crime (avg/yr)', 1)]: [1, 10], }); }); it('round-trips repeated election vote-share filters with dedicated URL params', () => { const labour = createElectionVoteShareFilterKey('% Labour', 1); const conservative = createElectionVoteShareFilterKey('% Conservative', 2); const params = stateToParams( null, { [labour]: [30, 55], [conservative]: [10, 35], }, [], new Set(), 'area' ); expect(params.getAll('voteShare')).toEqual(['% Labour:30:55', '% Conservative:10:35']); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createElectionVoteShareFilterKey('% Labour', 0)]: [30, 55], [createElectionVoteShareFilterKey('% Conservative', 1)]: [10, 35], }); }); it('round-trips repeated ethnicity filters with dedicated URL params', () => { const white = createEthnicityFilterKey('% White', 3); const southAsian = createEthnicityFilterKey('% South Asian', 4); const params = stateToParams( null, { [white]: [10, 80], [southAsian]: [5, 35], }, [], new Set(), 'area' ); expect(params.getAll('ethnicity')).toEqual(['% White:10:80', '% South Asian:5:35']); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createEthnicityFilterKey('% White', 0)]: [10, 80], [createEthnicityFilterKey('% South Asian', 1)]: [5, 35], }); }); it('round-trips repeated amenity distance filters with dedicated URL params', () => { const park = createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 3); const cafe = createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 4); const params = stateToParams( null, { [park]: [0, 0.4], [cafe]: [0, 1.5], }, [], new Set(), 'area' ); expect(params.getAll('amenityDistance')).toEqual([ 'Distance%20to%20nearest%20amenity%20(Park)%20(km):0:0.4', 'Distance%20to%20nearest%20amenity%20(Caf%C3%A9)%20(km):0:1.5', ]); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createPoiDistanceFilterKey('Distance to nearest amenity (Park) (km)', 0)]: [0, 0.4], [createPoiDistanceFilterKey('Distance to nearest amenity (Café) (km)', 1)]: [0, 1.5], }); }); it('round-trips transport distance filters with dedicated URL params', () => { const busStop = createPoiFilterKey( TRANSPORT_DISTANCE_FILTER_NAME, 'Distance to nearest amenity (Bus stop) (km)', 3 ); const params = stateToParams( null, { [busStop]: [0, 0.3], }, [], new Set(), 'area' ); expect(params.getAll('transportDistance')).toEqual([ 'Distance%20to%20nearest%20amenity%20(Bus%20stop)%20(km):0:0.3', ]); expect(params.getAll('amenityDistance')).toEqual([]); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createPoiFilterKey( TRANSPORT_DISTANCE_FILTER_NAME, 'Distance to nearest amenity (Bus stop) (km)', 0 )]: [0, 0.3], }); }); it('round-trips amenity count filters with dedicated URL params', () => { const cafes = createPoiFilterKey( POI_COUNT_2KM_FILTER_NAME, 'Number of amenities (Cafe) within 2km', 3 ); const params = stateToParams( null, { [cafes]: [2, 8], }, [], new Set(), 'area' ); expect(params.getAll('amenityCount2km')).toEqual([ 'Number%20of%20amenities%20(Cafe)%20within%202km:2:8', ]); expect(params.getAll('filter')).toEqual([]); window.history.replaceState({}, '', `/?${params.toString()}`); const state = parseUrlState(); expect(state.filters).toEqual({ [createPoiFilterKey(POI_COUNT_2KM_FILTER_NAME, 'Number of amenities (Cafe) within 2km', 0)]: [ 2, 8, ], }); }); it('omits the default area tab', () => { const params = stateToParams(null, {}, [], new Set(), 'area'); expect(params.has('tab')).toBe(false); }); });