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
117 changes: 116 additions & 1 deletion src/app.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
import { describe, it, expect, vi } from 'vitest';
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { App } from './app';

type MockSearch = {
wps?: string;
};

type MockWaypoint = {
id: string;
geocodeResults: Array<{
selected?: boolean;
sourcelnglat?: [number, number];
}>;
userInput: string;
};

const mockUseParams = vi.fn(() => ({ activeTab: 'directions' }));
const mockUseSearch = vi.fn<() => MockSearch>(() => ({ wps: undefined }));
const mockRefetchDirections = vi.fn().mockResolvedValue(undefined);
const mockReverseGeocode = vi.fn().mockResolvedValue([]);
let mockMapReady = true;
let mockWaypoints: MockWaypoint[] = [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
];

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

vi.mock('react-map-gl/maplibre', () => ({
MapProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="map-provider">{children}</div>
Expand Down Expand Up @@ -32,7 +60,38 @@ vi.mock('@/components/ui/sonner', () => ({
),
}));

vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: () => ({
refetch: mockRefetchDirections,
}),
useReverseGeocodeDirections: () => ({
reverseGeocode: mockReverseGeocode,
}),
}));

vi.mock('@/stores/common-store', () => ({
useCommonStore: (selector: (state: { mapReady: boolean }) => unknown) =>
selector({ mapReady: mockMapReady }),
}));

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

describe('App', () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseParams.mockReturnValue({ activeTab: 'directions' });
mockUseSearch.mockReturnValue({ wps: undefined });
mockMapReady = true;
mockWaypoints = [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
];
});

it('should render without crashing', () => {
expect(() => render(<App />)).not.toThrow();
});
Expand Down Expand Up @@ -73,4 +132,60 @@ describe('App', () => {
expect(mapProvider).toContainElement(screen.getByTestId('settings-panel'));
expect(mapProvider).toContainElement(screen.getByTestId('toaster'));
});

it('should hydrate waypoints from URL search params on initial load', () => {
mockUseSearch.mockReturnValue({
wps: '13.343067169189455,52.5296422146409,13.33414077758789,52.50901237642168',
});

render(<App />);

expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
expect(mockReverseGeocode).toHaveBeenNthCalledWith(
1,
13.343067169189455,
52.5296422146409,
0,
{ isPermalink: true }
);
expect(mockReverseGeocode).toHaveBeenNthCalledWith(
2,
13.33414077758789,
52.50901237642168,
1,
{ isPermalink: true }
);
});

it('should auto-render directions when URL waypoints already exist in state', () => {
mockUseSearch.mockReturnValue({
wps: '13.343067169189455,52.5296422146409,13.33414077758789,52.50901237642168',
});
mockWaypoints = [
{
id: '0',
geocodeResults: [
{
selected: true,
sourcelnglat: [13.343067169189455, 52.5296422146409],
},
],
userInput: 'Waypoint 1',
},
{
id: '1',
geocodeResults: [
{
selected: true,
sourcelnglat: [13.33414077758789, 52.50901237642168],
},
],
userInput: 'Waypoint 2',
},
];

render(<App />);

expect(mockRefetchDirections).toHaveBeenCalledTimes(1);
});
});
100 changes: 100 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,110 @@
import { useEffect, useMemo, useRef } from 'react';
import { useParams, useSearch } from '@tanstack/react-router';
import { MapProvider } from 'react-map-gl/maplibre';
import { MapComponent } from './components/map';
import { RoutePlanner } from './components/route-planner';
import { SettingsPanel } from './components/settings-panel/settings-panel';
import { Toaster } from '@/components/ui/sonner';
import {
useDirectionsQuery,
useReverseGeocodeDirections,
} from '@/hooks/use-directions-queries';
import { useCommonStore } from '@/stores/common-store';
import { useDirectionsStore } from '@/stores/directions-store';
import { isValidCoordinates } from '@/utils/geom';

export const App = () => {
const { activeTab } = useParams({ from: '/$activeTab' });
const { wps } = useSearch({ from: '/$activeTab' });
const { refetch: refetchDirections } = useDirectionsQuery();
const { reverseGeocode } = useReverseGeocodeDirections();
const mapReady = useCommonStore((state) => state.mapReady);
const waypoints = useDirectionsStore((state) => state.waypoints);
const initialWpsRef = useRef(wps);
const urlWaypointsLoadedRef = useRef(false);
const urlRouteRenderedRef = useRef(false);

const activeWaypointCount = useMemo(
() =>
waypoints.filter((wp) =>
wp.geocodeResults.some((result) => result.selected)
).length,
[waypoints]
);

useEffect(() => {
const initialWps = initialWpsRef.current;

if (
activeTab !== 'directions' ||
!initialWps ||
urlWaypointsLoadedRef.current
) {
return;
}

urlWaypointsLoadedRef.current = true;
console.info('[App] Hydrating directions from URL search params', {
wps: initialWps,
});

const coordinates = initialWps.split(',').map(Number);
let hasValidWaypoint = false;

for (let i = 0; i < coordinates.length; i += 2) {
const lng = coordinates[i];
const lat = coordinates[i + 1];

if (
lng === undefined ||
lat === undefined ||
Number.isNaN(lng) ||
Number.isNaN(lat) ||
!isValidCoordinates(lat, lng)
) {
continue;
}

hasValidWaypoint = true;

void reverseGeocode(lng, lat, i / 2, { isPermalink: true }).catch(
(error) => {
console.error('[App] Failed to hydrate waypoint from URL', error);
}
);
}

if (!hasValidWaypoint) {
console.debug(
'[App] No valid route coordinates found in URL search params'
);
}
}, [activeTab, reverseGeocode]);

useEffect(() => {
const initialWps = initialWpsRef.current;

if (
activeTab !== 'directions' ||
!initialWps ||
!mapReady ||
urlRouteRenderedRef.current ||
activeWaypointCount < 2
) {
return;
}

urlRouteRenderedRef.current = true;
console.info('[App] Auto-rendering route from URL search params', {
activeWaypointCount,
});

void refetchDirections().catch((error) => {
console.error('[App] Failed to auto-render route from URL', error);
urlRouteRenderedRef.current = false;
});
}, [activeTab, activeWaypointCount, mapReady, refetchDirections]);

return (
<MapProvider>
<MapComponent />
Expand Down
94 changes: 0 additions & 94 deletions src/components/directions/directions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DirectionsControl } from './directions';

const mockNavigate = vi.fn();
const mockRefetchDirections = vi.fn();
const mockReverseGeocode = vi.fn().mockResolvedValue([]);
const mockAddEmptyWaypointToEnd = vi.fn();
const mockClearWaypoints = vi.fn();
const mockClearRoutes = vi.fn();
Expand All @@ -15,10 +14,6 @@ vi.mock('@tanstack/react-router', () => ({
useNavigate: vi.fn(() => mockNavigate),
}));

vi.mock('@/utils/parse-url-params', () => ({
parseUrlParams: vi.fn(() => ({})),
}));

const mockWaypoints = [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
Expand Down Expand Up @@ -63,9 +58,6 @@ vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: vi.fn(() => ({
refetch: mockRefetchDirections,
})),
useReverseGeocodeDirections: vi.fn(() => ({
reverseGeocode: mockReverseGeocode,
})),
}));

vi.mock('@/hooks/use-optimized-route-query', () => ({
Expand Down Expand Up @@ -292,89 +284,3 @@ describe('DirectionsControl', () => {
expect(result.wps).toBe('13.4,52.5,10,48');
});
});

describe('DirectionsControl URL parsing', () => {
beforeEach(() => {
vi.clearAllMocks();
mockResults.data = null;
mockWaypoints.length = 0;
mockWaypoints.push(
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' }
);
});

it('should process URL params with valid coordinates (Berlin)', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '13.365016850476763,52.483706198952575,13.422421655040836,52.49336042169804',
});

render(<DirectionsControl />);

expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
expect(mockReverseGeocode).toHaveBeenCalledWith(
13.365016850476763,
52.483706198952575,
0,
{ isPermalink: true }
);
expect(mockReverseGeocode).toHaveBeenCalledWith(
13.422421655040836,
52.49336042169804,
1,
{ isPermalink: true }
);
});

it('should process URL params with valid coordinates where lng > 90 (Singapore)', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '103.66492937866911,1.4827280571964963,103.66421854954496,1.4840285187178779',
});

render(<DirectionsControl />);

expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
expect(mockReverseGeocode).toHaveBeenCalledWith(
103.66492937866911,
1.4827280571964963,
0,
{ isPermalink: true }
);
expect(mockReverseGeocode).toHaveBeenCalledWith(
103.66421854954496,
1.4840285187178779,
1,
{ isPermalink: true }
);
});

it('should skip truly invalid coordinates from URL', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '999,999',
});

render(<DirectionsControl />);

expect(mockReverseGeocode).not.toHaveBeenCalled();
});

it('should handle coordinates near edge of valid range', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '179.9,89,-179.9,-89',
});

render(<DirectionsControl />);

expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
expect(mockReverseGeocode).toHaveBeenCalledWith(179.9, 89, 0, {
isPermalink: true,
});
expect(mockReverseGeocode).toHaveBeenCalledWith(-179.9, -89, 1, {
isPermalink: true,
});
});
});
Loading