Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/components/map/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ vi.mock('./parts/route-lines', () => ({
RouteLines: vi.fn(() => <div data-testid="route-lines">Route Lines</div>),
}));

vi.mock('./parts/trace-input-line', () => ({
TraceRouteInputLine: vi.fn(() => (
<div data-testid="trace-input-line">Trace Input Line</div>
)),
}));

vi.mock('./parts/trace-route-markers', () => ({
TraceRouteMarkers: vi.fn(() => (
<div data-testid="trace-route-markers">Trace Route Markers</div>
)),
}));

vi.mock('./parts/highlight-segment', () => ({
HighlightSegment: vi.fn(() => (
<div data-testid="highlight-segment">Highlight</div>
Expand Down Expand Up @@ -345,6 +357,12 @@ describe('MapComponent', () => {
expect(screen.getByTestId('route-lines')).toBeInTheDocument();
});

it('should render trace-route input and marker layers', () => {
render(<MapComponent />);
expect(screen.getByTestId('trace-input-line')).toBeInTheDocument();
expect(screen.getByTestId('trace-route-markers')).toBeInTheDocument();
});

it('should render highlight segment component', () => {
render(<MapComponent />);
expect(screen.getByTestId('highlight-segment')).toBeInTheDocument();
Expand Down
4 changes: 4 additions & 0 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import {
useReverseGeocodeIsochrones,
} from '@/hooks/use-isochrones-queries';
import { toast } from 'sonner';
import { TraceRouteMarkers } from './parts/trace-route-markers';
import { TraceRouteInputLine } from './parts/trace-input-line';

const { center, zoom: zoom_initial } = getInitialMapPosition();

Expand Down Expand Up @@ -835,7 +837,9 @@ export const MapComponent = () => {
onStyleChange={handleStyleChange}
onCustomStyleLoaded={handleCustomStyleLoaded}
/>
<TraceRouteInputLine />
<RouteLines />
<TraceRouteMarkers />
<HighlightSegment />
<IsochronePolygons />
<IsochroneLocations />
Expand Down
128 changes: 100 additions & 28 deletions src/components/map/parts/route-lines.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,48 @@ vi.mock('react-map-gl/maplibre', () => ({
}));

const mockUseDirectionsStore = vi.fn();
const mockUseTraceRouteStore = vi.fn();
const mockUseParams = vi.hoisted(() =>
vi.fn(() => ({ activeTab: 'directions' }))
);

vi.mock('@tanstack/react-router', () => ({
useParams: mockUseParams,
}));

vi.mock('@/stores/directions-store', () => ({
useDirectionsStore: (selector: (state: unknown) => unknown) =>
mockUseDirectionsStore(selector),
}));

const createMockState = (overrides = {}) => ({
vi.mock('@/stores/trace-route-store', () => ({
useTraceRouteStore: (selector: (state: unknown) => unknown) =>
mockUseTraceRouteStore(selector),
}));

interface MockRouteGeometry {
decodedGeometry: number[][];
trip: {
summary: {
length: number;
time: number;
};
};
alternates: MockRouteGeometry[];
}

interface MockRouteState {
results: {
data: MockRouteGeometry | null;
show: Record<number, boolean>;
};
successful: boolean;
activeRouteIndex: number;
}

const createMockState = (
overrides: Partial<MockRouteState> = {}
): MockRouteState => ({
results: {
data: {
decodedGeometry: [
Expand All @@ -40,17 +75,38 @@ const createMockState = (overrides = {}) => ({
...overrides,
});

const setupStores = ({
activeTab = 'directions',
directionState = createMockState(),
traceState = createMockState(),
}: {
activeTab?: string;
directionState?: MockRouteState;
traceState?: MockRouteState;
} = {}) => {
mockUseParams.mockReturnValue({ activeTab });
mockUseDirectionsStore.mockImplementation((selector) =>
selector(directionState)
);
mockUseTraceRouteStore.mockImplementation((selector) => selector(traceState));
};

describe('RouteLines', () => {
beforeEach(() => {
mockSource.mockClear();
mockLayer.mockClear();
mockUseDirectionsStore.mockClear();
mockUseTraceRouteStore.mockClear();
mockUseParams.mockReturnValue({ activeTab: 'directions' });
});

it('should render nothing when results data is null', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = { results: { data: null, show: {} }, successful: false };
return selector(state);
setupStores({
directionState: {
results: { data: null, show: {} },
successful: false,
activeRouteIndex: -1,
},
});

const { container } = render(<RouteLines />);
Expand All @@ -59,21 +115,15 @@ describe('RouteLines', () => {
});

it('should render nothing when not successful', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState({ successful: false });
return selector(state);
});
setupStores({ directionState: createMockState({ successful: false }) });

const { container } = render(<RouteLines />);

expect(container.firstChild).toBeNull();
});

it('should render Source when data is valid', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState();
return selector(state);
});
setupStores();

render(<RouteLines />);

Expand All @@ -83,21 +133,15 @@ describe('RouteLines', () => {
});

it('should render two layers (outline and line)', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState();
return selector(state);
});
setupStores();

render(<RouteLines />);

expect(mockLayer).toHaveBeenCalledTimes(2);
});

it('should render outline layer with white color', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState();
return selector(state);
});
setupStores();

render(<RouteLines />);

Expand All @@ -111,10 +155,7 @@ describe('RouteLines', () => {
});

it('should render line layer with dynamic color', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState();
return selector(state);
});
setupStores();

render(<RouteLines />);

Expand All @@ -132,10 +173,7 @@ describe('RouteLines', () => {
});

it('should convert lat/lng to lng/lat format', () => {
mockUseDirectionsStore.mockImplementation((selector) => {
const state = createMockState();
return selector(state);
});
setupStores();

render(<RouteLines />);

Expand All @@ -144,4 +182,38 @@ describe('RouteLines', () => {
expect(coords[0]).toEqual([10, 50]);
expect(coords[1]).toEqual([11, 51]);
});

it('should use trace-route results when active tab is trace-route', () => {
setupStores({
activeTab: 'trace-route',
directionState: {
results: { data: null, show: {} },
successful: false,
activeRouteIndex: -1,
},
traceState: createMockState({
activeRouteIndex: 0,
results: {
data: {
decodedGeometry: [
[1, 2],
[3, 4],
],
trip: { summary: { length: 2, time: 60 } },
alternates: [],
},
show: { 0: true },
},
}),
});

render(<RouteLines />);

const sourceCall = mockSource.mock.calls[0]?.[0];
const coords = sourceCall?.data.features[0].geometry.coordinates;
expect(coords).toEqual([
[2, 1],
[4, 3],
]);
});
});
29 changes: 23 additions & 6 deletions src/components/map/parts/route-lines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,39 @@ import { useDirectionsStore } from '@/stores/directions-store';
import { routeObjects } from '../constants';
import type { Feature, FeatureCollection, LineString } from 'geojson';
import type { ParsedDirectionsGeometry } from '@/components/types';
import { useParams } from '@tanstack/react-router';
import { useTraceRouteStore } from '@/stores/trace-route-store';

export function RouteLines() {
const { activeTab } = useParams({ from: '/$activeTab' });
const isTraceRoute = activeTab === 'trace-route';

const directionResults = useDirectionsStore((state) => state.results);
const directionsSuccessful = useDirectionsStore((state) => state.successful);
const activeRouteIndex = useDirectionsStore(
const activeDirectionRouteIndex = useDirectionsStore(
(state) => state.activeRouteIndex
);

const traceRouteResults = useTraceRouteStore((state) => state.results);
const traceRouteSuccessful = useTraceRouteStore((state) => state.successful);
const traceRouteActiveIndex = useTraceRouteStore(
(state) => state.activeRouteIndex
);

const results = isTraceRoute ? traceRouteResults : directionResults;
const successful = isTraceRoute ? traceRouteSuccessful : directionsSuccessful;
const activeRouteIndex = isTraceRoute
? traceRouteActiveIndex
: activeDirectionRouteIndex;

const data = useMemo(() => {
if (!directionResults.data || !directionsSuccessful) return null;
if (!results.data || !successful) return null;

const hasNoData = Object.keys(directionResults.data).length === 0;
const hasNoData = Object.keys(results.data).length === 0;
if (hasNoData) return null;

const response = directionResults.data;
const showRoutes = directionResults.show || {};
const response = results.data;
const showRoutes = results.show || {};
const features: Feature<LineString>[] = [];

if (response.alternates) {
Expand Down Expand Up @@ -77,7 +94,7 @@ export function RouteLines() {
type: 'FeatureCollection',
features,
} as FeatureCollection;
}, [directionResults, directionsSuccessful, activeRouteIndex]);
}, [results, successful, activeRouteIndex]);

if (!data) return null;

Expand Down
Loading
Loading