From 8f06f3e0723428f10ece2d98f255c06f6e3a8ebc Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 14:42:23 -0700 Subject: [PATCH] fix: ignore stale map buy responses --- frontend/src/pages/LocationMapManager.jsx | 25 +++++++++-- frontend/tests/location-map-manager.spec.ts | 49 ++++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 0b3c6f0..958b476 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -68,6 +68,7 @@ export default function LocationMapManager() { const { activeHousehold, loading: householdLoading, hasLoaded } = useContext(HouseholdContext); const { stores, activeStore, setActiveStore } = useContext(StoreContext); const [buyModalItem, setBuyModalItem] = useState(null); + const mapBuyScopeRef = useRef({ activeHouseholdId: null, locationId: null }); const { beginSaving, @@ -172,6 +173,18 @@ export default function LocationMapManager() { }); }, [setMapState]); + useEffect(() => { + mapBuyScopeRef.current = { + activeHouseholdId: activeHousehold?.id, + locationId, + }; + }, [activeHousehold?.id, locationId]); + + const isCurrentMapBuyScope = useCallback((activeHouseholdId, scopedLocationId) => ( + String(mapBuyScopeRef.current.activeHouseholdId || "") === String(activeHouseholdId || "") && + String(mapBuyScopeRef.current.locationId || "") === String(scopedLocationId || "") + ), []); + const handleMapBuyCancel = useCallback(() => { setBuyModalItem(null); }, []); @@ -186,13 +199,16 @@ export default function LocationMapManager() { return; } + const activeHouseholdId = activeHousehold.id; + const scopedLocationId = locationId; const item = buyModalItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem; try { const currentIndex = buyModalItems.findIndex((candidate) => candidate.id === item.id); const resolvedIndex = currentIndex >= 0 ? currentIndex : 0; - await markBought(activeHousehold.id, locationId, item.item_name, quantity, true); + await markBought(activeHouseholdId, scopedLocationId, item.item_name, quantity, true); + if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return; let nextBuyModalItems = buyModalItems; if (quantity >= item.quantity) { @@ -205,9 +221,11 @@ export default function LocationMapManager() { } else { let updatedItem; try { - const response = await getItemByName(activeHousehold.id, locationId, item.item_name); + const response = await getItemByName(activeHouseholdId, scopedLocationId, item.item_name); + if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return; updatedItem = response.data || getLocallyReducedMapItem(item, quantity); } catch { + if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return; updatedItem = getLocallyReducedMapItem(item, quantity); toast.info("Updated item locally", "Latest quantity will sync when the map reloads."); } @@ -221,10 +239,11 @@ export default function LocationMapManager() { toast.success("Marked item bought", `Marked item ${item.item_name} as bought`); } catch (error) { + if (!isCurrentMapBuyScope(activeHouseholdId, scopedLocationId)) return; const message = getApiErrorMessage(error, "Failed to mark item as bought"); toast.error("Mark item bought failed", `Mark item bought failed: ${message}`); } - }, [activeHousehold?.id, buyModalItem, buyModalItems, locationId, toast, updateMapItems]); + }, [activeHousehold?.id, buyModalItem, buyModalItems, isCurrentMapBuyScope, locationId, toast, updateMapItems]); const mapSize = useMemo( () => ({ diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index d348bb5..40318d0 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -845,8 +845,30 @@ test("closes stale map buy modal after switching locations", async ({ page }) => location: secondStoreLocation, map: nextPublishedMap, published_map: nextPublishedMap, - items: [], + items: [ + { + id: 1, + item_name: "ontario oranges", + quantity: 1, + bought: false, + zone_id: 502, + zone: "Produce", + added_by_users: ["map-user"], + }, + ], }; + let resolveBuyRequestStarted: () => void = () => {}; + let releaseBuyResponse: () => void = () => {}; + let resolveBuyResponseSent: () => void = () => {}; + const buyRequestStarted = new Promise((resolve) => { + resolveBuyRequestStarted = resolve; + }); + const buyResponseGate = new Promise((resolve) => { + releaseBuyResponse = resolve; + }); + const buyResponseSent = new Promise((resolve) => { + resolveBuyResponseSent = resolve; + }); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ @@ -862,11 +884,28 @@ test("closes stale map buy modal after switching locations", async ({ page }) => body: JSON.stringify(nextLocationState), }); }); + await page.route("**/households/1/locations/10/list/item", async (route) => { + if (route.request().method() !== "PATCH") { + await route.fulfill({ status: 404, contentType: "application/json", body: "{}" }); + return; + } + + resolveBuyRequestStarted(); + await buyResponseGate; + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true }), + }); + resolveBuyResponseSent(); + }); await page.goto("/stores/100/locations/10/map"); await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click(); await page.getByRole("button", { name: "Mark french bread bought" }).click(); await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + await buyRequestStarted; await page.evaluate(() => { window.history.pushState({}, "", "/stores/100/locations/11/map"); @@ -877,6 +916,14 @@ test("closes stale map buy modal after switching locations", async ({ page }) => await expect(page.locator(".location-map-title span")).toHaveText("Ontario"); await expect(page.locator(".confirm-buy-modal")).toHaveCount(0); await expect(page.getByText("french bread")).toHaveCount(0); + await page.getByRole("button", { name: "Map area Ontario Produce" }).click(); + await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible(); + + releaseBuyResponse(); + await buyResponseSent; + + await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible(); + await expect(page.getByText("Marked item bought")).toHaveCount(0); }); test("closes stale map delete confirmation after switching locations", async ({ page }) => {