Repository navigation
RU-T56 Mapping and POI Updates #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { createCachedApiEndpoint } from '../../common/cached-client'; | ||
| import { createApiEndpoint } from '../../common/client'; | ||
| import { getMapIconLegend } from '../mapping'; | ||
|
|
||
| jest.mock('../../common/client', () => { | ||
| const get = jest.fn(); | ||
| return { | ||
| // Each endpoint reports its own path with the call, so a test can tell which endpoint was hit. | ||
| createApiEndpoint: jest.fn((endpoint: string) => ({ get: (...args: unknown[]) => get(endpoint, ...args) })), | ||
| __mockGet: get, | ||
| }; | ||
| }); | ||
|
|
||
| jest.mock('../../common/cached-client', () => ({ | ||
| createCachedApiEndpoint: jest.fn(() => ({ get: jest.fn() })), | ||
| })); | ||
|
|
||
| jest.mock('@/lib/storage/app', () => ({ | ||
| getBaseApiUrl: jest.fn(() => 'https://api.example.test/api/v4'), | ||
| })); | ||
|
|
||
| const { __mockGet: mockGet } = jest.requireMock('../../common/client') as { __mockGet: jest.Mock }; | ||
|
|
||
| // Endpoints are created at module load; capture them before beforeEach clears the mocks. | ||
| const plainPaths = (createApiEndpoint as jest.Mock).mock.calls.map((call) => call[0]); | ||
| const cachedPaths = (createCachedApiEndpoint as jest.Mock).mock.calls.map((call) => call[0]); | ||
|
|
||
| describe('getMapIconLegend', () => { | ||
| beforeEach(() => { | ||
| mockGet.mockReset(); | ||
| }); | ||
|
|
||
| it('targets Mapping/GetMapIconLegend without the response cache, so every open asks the server', () => { | ||
| expect(plainPaths).toContain('/Mapping/GetMapIconLegend'); | ||
| expect(cachedPaths).not.toContain('/Mapping/GetMapIconLegend'); | ||
| }); | ||
|
|
||
| it('GETs the endpoint with no parameters and returns the v4 envelope as sent', async () => { | ||
| const payload = { | ||
| PageSize: 3, | ||
| Status: 'success', | ||
| Environment: 'test', | ||
| RequestId: 'abc', | ||
| Version: 'v4', | ||
| Timestamp: '2026-10-25T00:00:00Z', | ||
| Node: 'n1', | ||
| Data: { StatusStyles: [], PriorityStyles: [], Calls: [{ Id: null, Name: 'Calls with no type', States: [] }], Units: [], Personnel: [], PoiTypes: [] }, | ||
| }; | ||
| mockGet.mockResolvedValueOnce({ data: payload }); | ||
|
|
||
| await expect(getMapIconLegend()).resolves.toBe(payload); | ||
| expect(mockGet).toHaveBeenCalledTimes(1); | ||
| expect(mockGet).toHaveBeenCalledWith('/Mapping/GetMapIconLegend', undefined, undefined); | ||
| }); | ||
|
|
||
| it('forwards the abort signal', async () => { | ||
| const controller = new AbortController(); | ||
| mockGet.mockResolvedValueOnce({ data: { Data: null } }); | ||
|
|
||
| await getMapIconLegend(controller.signal); | ||
|
|
||
| expect(mockGet).toHaveBeenCalledWith('/Mapping/GetMapIconLegend', undefined, controller.signal); | ||
| }); | ||
|
|
||
| it('passes request failures to the caller', async () => { | ||
| mockGet.mockRejectedValueOnce(new Error('Request failed with status code 404')); | ||
|
|
||
| await expect(getMapIconLegend()).rejects.toThrow('404'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { type FeatureCollection } from 'geojson'; | |
|
|
||
| import { getBaseApiUrl } from '@/lib/storage/app'; | ||
| import { type GetMapDataAndMarkersResult } from '@/models/v4/mapping/getMapDataAndMarkersResult'; | ||
| import { type GetMapIconLegendResult } from '@/models/v4/mapping/getMapIconLegendResult'; | ||
| import { type GetMapLayersResult } from '@/models/v4/mapping/getMapLayersResult'; | ||
| import { | ||
| type GetAllActiveLayersResult, | ||
|
|
@@ -23,6 +24,8 @@ import { createApiEndpoint } from '../common/client'; | |
|
|
||
| const getMayLayersApi = createApiEndpoint('/Mapping/GetMayLayers'); | ||
| const getMapDataAndMarkersApi = createApiEndpoint('/Mapping/GetMapDataAndMarkers'); | ||
| // Not cached: the legend follows the department's types, statuses, priorities and uploads, so every open asks again. | ||
| const getMapIconLegendApi = createApiEndpoint('/Mapping/GetMapIconLegend'); | ||
| const getPoiApi = createCachedApiEndpoint('/Mapping/GetPoi', { | ||
| ttl: 5 * 60 * 1000, | ||
| enabled: true, | ||
|
|
@@ -72,6 +75,12 @@ export const getMapDataAndMarkers = async (signal?: AbortSignal) => { | |
| return response.data; | ||
| }; | ||
|
|
||
| /** The department's map icon legend: every call, unit, personnel and POI icon the map can draw, and the colour styles. */ | ||
| export const getMapIconLegend = async (signal?: AbortSignal) => { | ||
| const response = await getMapIconLegendApi.get<GetMapIconLegendResult>(undefined, signal); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The external getMapIconLegendApi.get call can propagate an uncontextualized failure from src/api/mapping/mapping.ts. Wrap it in try/catch and map failures to an application-level error with operation context; the same issue occurs in src/services/location.ts:108, :121, :788, and :795. Kody rule violation: Add try-catch blocks for external calls try {
const response = await getMapIconLegendApi.get<GetMapIconLegendResult>(undefined, signal);
return response.data;
} catch (error) {
throw new Error('Failed to fetch map icon legend', { cause: error });
}Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| return response.data; | ||
| }; | ||
|
|
||
| export const getPoiTypes = async () => { | ||
| const response = await getPoiTypesApi.get<PoiTypesResult>(); | ||
| return response.data; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The awaited getMapIconLegend operation can reject without a deterministic test assertion, causing unhandled promise failures. Guard it with an explicit promise assertion or try/catch; the same issue occurs in src/components/maps/tests/map-screen-legend.test.tsx:156, :170, :186, :194; src/stores/signalr/tests/signalr-store.test.ts:513 and :512; src/services/location.ts:108, :121, :721, :504, :796, :483, :743, :494, :788, :795, and :804; src/api/mapping/mapping.ts:80; and src/components/maps/tests/map-legend-sheet.test.tsx:230, :202, :434, :450, :458, :467, :283, :237, :270, :297, :310, :319, :330, :341, :351, :362, :371, :384, :393, :399, :409, :419, and :486.
Kody rule violation: Handle async operations with proper error handling
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.