Skip to content

Commit 8f8f41d

Browse files
authored
fix map prod e2e search bounds flake (#103)
* fix map prod e2e search bounds flake and local media seed noise Seed map search context from saved coordinates before MapLibre idle so production Playwright runs do not open search without proximity/bbox. Also seed limit/*.jpg fixtures for the max_photos media test and migrate Supabase local email config from deprecated inbucket to local_smtp. * revert supabase config to inbucket for branch compatibility Supabase branch parsing does not yet accept the local_smtp key; keep inbucket until hosted Supabase catches up with the newer CLI schema.
1 parent 0c41916 commit 8f8f41d

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

e2e/map.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ test("map search is bounded by the current map instead of IP country", async ({
376376
).toBeVisible({
377377
timeout: 10_000,
378378
});
379+
await expect(page.getByTestId("map-view")).toHaveAttribute(
380+
"data-search-context-ready",
381+
"true"
382+
);
379383

380384
await page.getByTestId("map-control-search").click();
381385
await page.getByTestId("geocoding-search-input").fill("Newtown");

scripts/seed-local-media.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,34 @@ async function main() {
188188
await uploadBucketObjects(supabase, bucketName, bucketConfig);
189189
}
190190

191+
const listingPhotoFixture = path.join(
192+
repoRoot,
193+
"supabase",
194+
"storage",
195+
"listing_photos",
196+
"demo",
197+
"garden.jpg"
198+
);
199+
const listingPhotoBody = readFileSync(listingPhotoFixture);
200+
201+
for (const photoName of ["one", "two", "three", "four", "five"]) {
202+
const objectPath = `limit/${photoName}.jpg`;
203+
const { error } = await supabase.storage
204+
.from("listing_photos")
205+
.upload(objectPath, listingPhotoBody, {
206+
contentType: "image/jpeg",
207+
upsert: true,
208+
});
209+
210+
if (error) {
211+
throw new Error(
212+
`Failed to upload listing_photos/${objectPath}: ${error.message}`
213+
);
214+
}
215+
216+
console.log(`Uploaded listing_photos/${objectPath}`);
217+
}
218+
191219
console.log("Local demo media seeding complete.");
192220
}
193221

src/features/map/components/MapView.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
MAP_MAX_ZOOM,
2828
ZOOM_LEVEL_DEFAULT,
2929
ZOOM_LEVEL_SELECTED,
30+
approximateBoundsFromViewState,
3031
getListingCoordinates,
3132
hasValidCoordinates,
3233
padBounds,
@@ -212,6 +213,20 @@ function resolveMapSearchContext(bounds: LngLatBounds): MapSearchContext {
212213
return { bbox, proximity };
213214
}
214215

216+
function resolveInitialSearchContext(
217+
initialCoordinates: InitialMapCoordinates | null
218+
): MapSearchContext | null {
219+
if (!initialCoordinates) return null;
220+
221+
return resolveMapSearchContext(
222+
approximateBoundsFromViewState(
223+
initialCoordinates.longitude,
224+
initialCoordinates.latitude,
225+
initialCoordinates.zoom
226+
)
227+
);
228+
}
229+
215230
function resolveInitialViewState(
216231
selectedListing: SelectedListing | null,
217232
initialCoordinates: InitialMapCoordinates | null
@@ -252,7 +267,7 @@ export default function MapView({
252267
const mapContainerRef = useRef<HTMLDivElement | null>(null);
253268
const [isSearchOpen, setIsSearchOpen] = useState(false);
254269
const [searchContext, setSearchContext] = useState<MapSearchContext | null>(
255-
null
270+
() => resolveInitialSearchContext(initialCoordinates)
256271
);
257272
const [userCoordinates, setUserCoordinates] = useState<{
258273
latitude: number;
@@ -427,6 +442,19 @@ export default function MapView({
427442
}
428443
}, [initialCoordinates]);
429444

445+
useEffect(() => {
446+
if (!initialCoordinates) return;
447+
448+
setSearchContext((currentSearchContext) => {
449+
if (currentSearchContext?.bbox) return currentSearchContext;
450+
451+
const nextSearchContext = resolveInitialSearchContext(initialCoordinates);
452+
return areSearchContextsEqual(currentSearchContext, nextSearchContext)
453+
? currentSearchContext
454+
: nextSearchContext;
455+
});
456+
}, [initialCoordinates]);
457+
430458
const scheduleStoredMapViewSave = useCallback(() => {
431459
if (saveMapViewTimeoutRef.current !== null) {
432460
clearTimeout(saveMapViewTimeoutRef.current);
@@ -539,6 +567,11 @@ export default function MapView({
539567
);
540568
}, [flyToCoordinate]);
541569

570+
const handleOpenSearch = useCallback(() => {
571+
syncCurrentMapState();
572+
setIsSearchOpen(true);
573+
}, [syncCurrentMapState]);
574+
542575
const handleSearchPick = useCallback(
543576
(feature: GeocodingFeature) => {
544577
const center = feature.center;
@@ -561,6 +594,7 @@ export default function MapView({
561594
ref={mapContainerRef}
562595
role="region"
563596
aria-label={t("mapRegionLabel")}
597+
data-search-context-ready={searchContext?.proximity ? "true" : "false"}
564598
data-testid="map-view"
565599
style={initialMapPinZoomStyleRef.current ?? undefined}
566600
>
@@ -612,7 +646,7 @@ export default function MapView({
612646
locateActive={Boolean(userCoordinates)}
613647
locateLabel={t("locateControl")}
614648
onLocate={handleLocate}
615-
onSearch={() => setIsSearchOpen(true)}
649+
onSearch={handleOpenSearch}
616650
onZoomIn={zoomIn}
617651
onZoomOut={zoomOut}
618652
searchLabel={t("searchLabel")}

src/features/map/lib/mapUtils.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import test from "node:test";
22
import assert from "node:assert/strict";
33
import type { LngLatBounds } from "maplibre-gl";
44

5-
import { padBounds } from "./mapUtils.ts";
5+
import { approximateBoundsFromViewState, padBounds } from "./mapUtils.ts";
66

77
function bounds(
88
south: number,
@@ -34,3 +34,13 @@ test("padBounds still splits antimeridian crossings", () => {
3434
{ south: -16, north: 16, west: -180, east: -164 },
3535
]);
3636
});
37+
38+
test("approximateBoundsFromViewState centres on the saved map view", () => {
39+
const approximateBounds = approximateBoundsFromViewState(151.16, -33.91, 7);
40+
41+
assert.ok(approximateBounds.contains([151.16, -33.91]));
42+
assert.ok(approximateBounds.getWest() < 151.16);
43+
assert.ok(approximateBounds.getEast() > 151.16);
44+
assert.ok(approximateBounds.getSouth() < -33.91);
45+
assert.ok(approximateBounds.getNorth() > -33.91);
46+
});

src/features/map/lib/mapUtils.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,62 @@ export function wrapLongitude(lng: number): number {
8585
return ((((lng + 180) % 360) + 360) % 360) - 180;
8686
}
8787

88+
const DEFAULT_MAP_VIEWPORT_WIDTH = 1280;
89+
const DEFAULT_MAP_VIEWPORT_HEIGHT = 900;
90+
91+
function createBounds(
92+
west: number,
93+
south: number,
94+
east: number,
95+
north: number
96+
): LngLatBounds {
97+
return {
98+
getSouthWest: () => ({ lat: south, lng: west }),
99+
getNorthEast: () => ({ lat: north, lng: east }),
100+
getCenter: () => ({
101+
lng: wrapLongitude((west + east) / 2),
102+
lat: (south + north) / 2,
103+
}),
104+
contains: (coordinate) => {
105+
const lng = Array.isArray(coordinate)
106+
? coordinate[0]
107+
: "lng" in coordinate
108+
? coordinate.lng
109+
: coordinate.lon;
110+
const lat = Array.isArray(coordinate) ? coordinate[1] : coordinate.lat;
111+
112+
return lat >= south && lat <= north && lng >= west && lng <= east;
113+
},
114+
getWest: () => west,
115+
getEast: () => east,
116+
getSouth: () => south,
117+
getNorth: () => north,
118+
} as LngLatBounds;
119+
}
120+
121+
// Approximate the current viewport bounds from a saved map view before MapLibre
122+
// has finished loading. Search can use these immediately and refine them on idle.
123+
export function approximateBoundsFromViewState(
124+
longitude: number,
125+
latitude: number,
126+
zoom: number,
127+
width = DEFAULT_MAP_VIEWPORT_WIDTH,
128+
height = DEFAULT_MAP_VIEWPORT_HEIGHT
129+
): LngLatBounds {
130+
const scale = 512 * 2 ** zoom;
131+
const lngDelta = (360 / scale) * (width / 2);
132+
const latRad = (latitude * Math.PI) / 180;
133+
const latDelta =
134+
((360 / scale) * (height / 2)) / Math.max(Math.cos(latRad), 0.01);
135+
136+
const south = Math.max(-90, latitude - latDelta);
137+
const north = Math.min(90, latitude + latDelta);
138+
const west = wrapLongitude(longitude - lngDelta);
139+
const east = wrapLongitude(longitude + lngDelta);
140+
141+
return createBounds(west, south, east, north);
142+
}
143+
88144
// Expand a viewport bbox by a fraction (e.g. 0.3 => 30% larger in each
89145
// direction). This lets us fetch a slightly padded area so that small pans
90146
// reuse already-loaded pins without hitting the network again.

0 commit comments

Comments
 (0)