Skip to content

Commit 65ca9c6

Browse files
authored
Staging (#29)
* test: improve alert handling in FindHelpEntry tests and add global alert mock * Fix: Temporary linting suppressions in FindHelpResults; update README with linting strategy note * chore: update package-lock.json to sync with package.json dependencies
1 parent a02a533 commit 65ca9c6

File tree

14 files changed

+274
-119
lines changed

14 files changed

+274
-119
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,12 @@ See the project instructions for the full GitHub workflow. In short:
7171

7272
---
7373

74+
## ⚠️ TypeScript & Linting Workaround
75+
76+
Some temporary TypeScript and ESLint suppressions are in place—particularly in `FindHelpResults.tsx`—to avoid breaking builds while types are refined. This includes type assertions (`as any`, `!`) and relaxed location typing.
77+
78+
These will be addressed post-MVP once shared types (e.g. for services and location context) are finalised. All suppressions are tracked in Trello under `Linting Suppression and Deferred Resolution Strategy` - https://trello.com/c/bISJ2l1L.
79+
80+
---
81+
7482
For full developer instructions and context, see the `rebuild-docs.docx` or ask for clarification before making assumptions.

__tests__/components/FindHelpEntry.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('FindHelpEntry', () => {
4949
});
5050

5151
it('shows alert on invalid postcode response', async () => {
52-
jest.spyOn(window, 'alert').mockImplementation(() => {});
52+
const alertSpy = jest.spyOn(window, 'alert');
5353
global.fetch = jest.fn(() =>
5454
Promise.resolve({
5555
json: () => Promise.resolve({ error: 'Invalid postcode' }),
@@ -61,7 +61,7 @@ describe('FindHelpEntry', () => {
6161
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
6262

6363
await waitFor(() => {
64-
expect(window.alert).toHaveBeenCalledWith(expect.stringMatching(/something went wrong/i));
64+
expect(alertSpy).toHaveBeenCalledWith(expect.stringMatching(/something went wrong/i));
6565
});
6666
});
67-
});
67+
});

jest.setup.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
// jest.setup.ts
21
import '@testing-library/jest-dom';
32

4-
// Mock global fetch
3+
// Provide a global fetch mock for components using fetch
54
global.fetch = jest.fn(() =>
65
Promise.resolve({
76
json: () => Promise.resolve([
@@ -25,13 +24,17 @@ global.fetch = jest.fn(() =>
2524
})
2625
) as jest.Mock;
2726

28-
// Suppress React act(...) warnings globally
27+
// Capture original console.error before overriding it
2928
const originalError = console.error;
3029

30+
// Suppress only specific React test warnings
3131
beforeAll(() => {
32-
jest.spyOn(console, 'error').mockImplementation((...args) => {
33-
const [msg] = args;
34-
if (typeof msg === 'string' && msg.includes('not wrapped in act')) return;
35-
originalError(...args);
32+
jest.spyOn(console, 'error').mockImplementation((msg) => {
33+
const [text] = Array.isArray(msg) ? msg : [msg];
34+
if (typeof text === 'string' && text.includes('not wrapped in act')) return;
35+
originalError(...(Array.isArray(msg) ? msg : [msg]));
3636
});
3737
});
38+
39+
// ✅ Define global alert as jest mock to avoid jsdom crash
40+
global.alert = jest.fn();

next.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { NextConfig } from "next";
22

33
const nextConfig: NextConfig = {
4-
/* config options here */
54
reactStrictMode: true,
5+
env: {
6+
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY,
7+
},
68
};
79

810
export default nextConfig;

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "next dev",
8-
"prebuild": "jest",
9-
"build": "next build",
8+
"build": "cross-env NEXT_DISABLE_ESLINT=true jest && next build",
109
"start": "next start",
1110
"lint": "next lint",
1211
"test": "jest",
@@ -46,6 +45,7 @@
4645
"autoprefixer": "^10.4.21",
4746
"babel-jest": "^30.0.0-beta.3",
4847
"babel-plugin-module-resolver": "^5.0.2",
48+
"cross-env": "^7.0.3",
4949
"dotenv": "^16.5.0",
5050
"eslint": "^9.28.0",
5151
"eslint-config-next": "^15.3.3",

src/app/api/get-service-providers/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export function GET() {
1111
postcode: provider.postcode,
1212
latitude: provider.latitude,
1313
longitude: provider.longitude,
14-
locationId: provider.locationId,
1514
verified: provider.verified
1615
}));
1716
});

src/app/find-help/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { LocationProvider } from '@/contexts/LocationContext';
44
import FindHelpEntry from '@/components/FindHelp/FindHelpEntry';
55
import FindHelpResults from '@/components/FindHelp/FindHelpResults';
66
import rawProviders from '@/data/service-providers.json';
7+
import type { ServiceProvider } from '@/types';
78

89
export default function FindHelpPage() {
9-
const providers = rawProviders as any[];
10+
const providers = rawProviders as ServiceProvider[];
1011

1112
return (
1213
<LocationProvider>

src/components/FindHelp/FindHelpResults.tsx

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// FindHelpResults.tsx
22
'use client';
33

4-
import { useMemo, useState } from 'react';
4+
import { useMemo, useState, useCallback } from 'react';
55
import { useLocation } from '@/contexts/LocationContext';
66
import ServiceCard from './ServiceCard';
77
import FilterPanel from './FilterPanel';
@@ -12,6 +12,26 @@ interface Props {
1212
providers: ServiceProvider[];
1313
}
1414

15+
interface FlattenedServiceWithExtras extends FlattenedService {
16+
organisation: string;
17+
organisationSlug: string;
18+
lat: number;
19+
lng: number;
20+
distance?: number;
21+
}
22+
23+
interface MapMarker {
24+
id: string;
25+
lat: number;
26+
lng: number;
27+
title: string;
28+
organisation?: string;
29+
link?: string;
30+
serviceName?: string;
31+
distanceKm?: number;
32+
icon?: string;
33+
}
34+
1535
export default function FindHelpResults({ providers }: Props) {
1636
const { location } = useLocation();
1737
const [showMap, setShowMap] = useState(false);
@@ -20,46 +40,70 @@ export default function FindHelpResults({ providers }: Props) {
2040
const [selectedCategory, setSelectedCategory] = useState('');
2141
const [selectedSubCategory, setSelectedSubCategory] = useState('');
2242

23-
const flattenedServices: FlattenedService[] = useMemo(() => {
43+
const getDistanceFromLatLonInKm = useCallback((lat1: number, lon1: number, lat2: number, lon2: number) => {
44+
const R = 6371;
45+
const dLat = deg2rad(lat2 - lat1);
46+
const dLon = deg2rad(lon2 - lon1);
47+
const a =
48+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
49+
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
50+
Math.sin(dLon / 2) * Math.sin(dLon / 2);
51+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
52+
return R * c;
53+
}, []);
54+
55+
function deg2rad(deg: number) {
56+
return deg * (Math.PI / 180);
57+
}
58+
59+
const flattenedServices: FlattenedServiceWithExtras[] = useMemo(() => {
2460
if (!providers || providers.length === 0) return [];
2561

2662
return providers.flatMap((org) => {
2763
if (!org.services || !Array.isArray(org.services)) return [];
2864

29-
return org.services.map((service) => {
30-
if (!service.latitude || !service.longitude) return null;
31-
return {
65+
return org.services.flatMap((service) => {
66+
if (
67+
typeof service.latitude !== 'number' ||
68+
typeof service.longitude !== 'number'
69+
) {
70+
return [];
71+
}
72+
73+
return [{
3274
id: service.id,
3375
name: service.name,
3476
description: service.description,
3577
category: service.category,
3678
subCategory: service.subCategory,
37-
organisation: org.name,
38-
organisationSlug: org.slug,
3979
lat: service.latitude,
4080
lng: service.longitude,
81+
latitude: service.latitude,
82+
longitude: service.longitude,
83+
organisation: org.name,
84+
organisationSlug: org.slug,
4185
clientGroups: service.clientGroups || [],
4286
openTimes: service.openTimes || [],
43-
};
44-
}).filter(Boolean) as FlattenedService[];
87+
}];
88+
});
4589
});
4690
}, [providers]);
4791

4892
const filteredServicesWithDistance = useMemo(() => {
49-
if (!location) return [];
93+
if (!location || location.lat == null || location.lng == null) return [];
5094

5195
return flattenedServices
52-
.map((service) => {
53-
const distance = getDistanceFromLatLonInKm(location.lat, location.lng, service.lat, service.lng);
54-
return { ...service, distance };
55-
})
96+
.map((service) => ({
97+
...service,
98+
distance: getDistanceFromLatLonInKm(location.lat!, location.lng!, service.lat, service.lng),
99+
}))
56100
.filter((service) => {
57-
const distanceMatch = service.distance <= radius;
101+
const distanceMatch = service.distance! <= radius;
58102
const categoryMatch = selectedCategory ? service.category === selectedCategory : true;
59103
const subCategoryMatch = selectedSubCategory ? service.subCategory === selectedSubCategory : true;
60104
return distanceMatch && categoryMatch && subCategoryMatch;
61105
});
62-
}, [flattenedServices, location, radius, selectedCategory, selectedSubCategory]);
106+
}, [flattenedServices, location, radius, selectedCategory, selectedSubCategory, getDistanceFromLatLonInKm]);
63107

64108
const sortedServices = useMemo(() => {
65109
if (sortOrder === 'alpha') {
@@ -68,24 +112,8 @@ export default function FindHelpResults({ providers }: Props) {
68112
return [...filteredServicesWithDistance].sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0));
69113
}, [filteredServicesWithDistance, sortOrder]);
70114

71-
function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number) {
72-
const R = 6371;
73-
const dLat = deg2rad(lat2 - lat1);
74-
const dLon = deg2rad(lon2 - lon1);
75-
const a =
76-
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
77-
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
78-
Math.sin(dLon / 2) * Math.sin(dLon / 2);
79-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
80-
return R * c;
81-
}
82-
83-
function deg2rad(deg: number) {
84-
return deg * (Math.PI / 180);
85-
}
86-
87-
const combinedMarkers = useMemo(() => {
88-
const markers = filteredServicesWithDistance.map((s) => ({
115+
const combinedMarkers: MapMarker[] = useMemo(() => {
116+
const markers: MapMarker[] = filteredServicesWithDistance.map((s) => ({
89117
id: s.id,
90118
lat: s.lat,
91119
lng: s.lng,
@@ -95,7 +123,8 @@ export default function FindHelpResults({ providers }: Props) {
95123
serviceName: s.name,
96124
distanceKm: s.distance,
97125
}));
98-
if (location) {
126+
127+
if (location && location.lat != null && location.lng != null) {
99128
markers.unshift({
100129
id: 'user-location',
101130
lat: location.lat,
@@ -104,6 +133,7 @@ export default function FindHelpResults({ providers }: Props) {
104133
icon: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png',
105134
});
106135
}
136+
107137
return markers;
108138
}, [filteredServicesWithDistance, location]);
109139

@@ -155,17 +185,15 @@ export default function FindHelpResults({ providers }: Props) {
155185

156186
{showMap && (
157187
<div className="block lg:hidden w-full mb-4" data-testid="map-container">
158-
<GoogleMap center={location} markers={combinedMarkers} />
188+
<GoogleMap center={(location && location.lat != null && location.lng != null) ? { lat: location.lat, lng: location.lng } : null} markers={combinedMarkers} />
159189
</div>
160190
)}
161191

162192
<div className="flex-1 overflow-y-visible lg:overflow-y-auto pr-2">
163193
{sortedServices.length === 0 ? (
164194
<p>No services found within {radius} km of your location.</p>
165195
) : (
166-
<div
167-
className={`gap-4 ${showMap ? 'flex flex-col' : 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'}`}
168-
>
196+
<div className={`gap-4 ${showMap ? 'flex flex-col' : 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'}`}>
169197
{sortedServices.map((service) => (
170198
<div
171199
key={service.id}
@@ -186,7 +214,14 @@ export default function FindHelpResults({ providers }: Props) {
186214

187215
{showMap && (
188216
<div className="hidden lg:block w-full lg:w-1/2 mt-8 lg:mt-0 lg:sticky lg:top-[6.5rem] min-h-[400px]" data-testid="map-container">
189-
<GoogleMap center={location} markers={combinedMarkers} />
217+
<GoogleMap
218+
center={
219+
location && location.lat != null && location.lng != null
220+
? { lat: location.lat, lng: location.lng }
221+
: null
222+
}
223+
markers={combinedMarkers}
224+
/>
190225
</div>
191226
)}
192227
</section>

src/contexts/LocationContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function LocationProvider({ children }: { children: ReactNode }) {
4949
POSITION_UNAVAILABLE: err?.code === 2,
5050
TIMEOUT: err?.code === 3,
5151
});
52-
} catch (e) {
52+
} catch {
5353
console.error('⛔ Geolocation error: Could not serialize error:', err);
5454
}
5555
},
@@ -76,4 +76,4 @@ export function useLocation() {
7676
return context;
7777
}
7878

79-
export { LocationContext };
79+
export { LocationContext };

0 commit comments

Comments
 (0)