fix: ignore stale map load responses
This commit is contained in:
parent
567087ab53
commit
44cc376e1b
@ -76,6 +76,7 @@ export default function LocationMapManager() {
|
|||||||
const [pendingLeave, setPendingLeave] = useState(false);
|
const [pendingLeave, setPendingLeave] = useState(false);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const toastRef = useRef(toast);
|
const toastRef = useRef(toast);
|
||||||
|
const loadRequestRef = useRef(0);
|
||||||
|
|
||||||
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
||||||
const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role));
|
const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role));
|
||||||
@ -158,6 +159,9 @@ export default function LocationMapManager() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMap = useCallback(async () => {
|
const loadMap = useCallback(async () => {
|
||||||
|
const requestId = loadRequestRef.current + 1;
|
||||||
|
loadRequestRef.current = requestId;
|
||||||
|
|
||||||
if (!activeHousehold?.id || !locationId) {
|
if (!activeHousehold?.id || !locationId) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@ -167,6 +171,8 @@ export default function LocationMapManager() {
|
|||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
try {
|
try {
|
||||||
const response = await getLocationMap(activeHousehold.id, locationId);
|
const response = await getLocationMap(activeHousehold.id, locationId);
|
||||||
|
if (loadRequestRef.current !== requestId) return;
|
||||||
|
|
||||||
const nextState = response.data;
|
const nextState = response.data;
|
||||||
const nextCanManage = Boolean(
|
const nextCanManage = Boolean(
|
||||||
nextState.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)
|
nextState.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)
|
||||||
@ -218,6 +224,8 @@ export default function LocationMapManager() {
|
|||||||
!nextState.published_map
|
!nextState.published_map
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (loadRequestRef.current !== requestId) return;
|
||||||
|
|
||||||
const message = getApiErrorMessage(error, "Failed to load map");
|
const message = getApiErrorMessage(error, "Failed to load map");
|
||||||
toastRef.current.error("Load map failed", message);
|
toastRef.current.error("Load map failed", message);
|
||||||
setMapState(null);
|
setMapState(null);
|
||||||
@ -228,7 +236,9 @@ export default function LocationMapManager() {
|
|||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
setLoadError(message);
|
setLoadError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (loadRequestRef.current === requestId) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [activeHousehold?.id, activeHousehold?.role, locationId, syncMapDraftFromState]);
|
}, [activeHousehold?.id, activeHousehold?.role, locationId, syncMapDraftFromState]);
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,12 @@ const storeLocation = {
|
|||||||
display_name: "Costco - Eastvale",
|
display_name: "Costco - Eastvale",
|
||||||
is_default: true,
|
is_default: true,
|
||||||
};
|
};
|
||||||
|
const secondStoreLocation = {
|
||||||
|
...storeLocation,
|
||||||
|
id: 11,
|
||||||
|
location_name: "Ontario",
|
||||||
|
display_name: "Costco - Ontario",
|
||||||
|
};
|
||||||
const zones = [
|
const zones = [
|
||||||
{ id: 501, name: "Bakery", sort_order: 10, item_count: 2 },
|
{ id: 501, name: "Bakery", sort_order: 10, item_count: 2 },
|
||||||
{ id: 502, name: "Frozen Foods", sort_order: 20, item_count: 1 },
|
{ id: 502, name: "Frozen Foods", sort_order: 20, item_count: 1 },
|
||||||
@ -146,7 +152,11 @@ function draftAndPublishedMapState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockMapShell(page: Page, household = adminHousehold) {
|
async function mockMapShell(
|
||||||
|
page: Page,
|
||||||
|
household = adminHousehold,
|
||||||
|
locations: Array<typeof storeLocation> = [storeLocation]
|
||||||
|
) {
|
||||||
await seedAuthStorage(page, { username: "map-user", role: household.role });
|
await seedAuthStorage(page, { username: "map-user", role: household.role });
|
||||||
await mockConfig(page);
|
await mockConfig(page);
|
||||||
|
|
||||||
@ -162,7 +172,7 @@ async function mockMapShell(page: Page, household = adminHousehold) {
|
|||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: JSON.stringify([storeLocation]),
|
body: JSON.stringify(locations),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -470,6 +480,107 @@ test("load failure shows retryable error instead of no-map setup", async ({ page
|
|||||||
expect(attempts).toBe(2);
|
expect(attempts).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ignores stale map responses after switching locations", async ({ page }) => {
|
||||||
|
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
|
||||||
|
|
||||||
|
const oldLocationState = publishedMapState([
|
||||||
|
{
|
||||||
|
id: 471,
|
||||||
|
location_map_id: 901,
|
||||||
|
zone_id: 501,
|
||||||
|
zone_name: "Bakery",
|
||||||
|
type: "zone",
|
||||||
|
label: "Old Bakery",
|
||||||
|
x: 40,
|
||||||
|
y: 40,
|
||||||
|
width: 260,
|
||||||
|
height: 160,
|
||||||
|
rotation: 0,
|
||||||
|
locked: false,
|
||||||
|
visible: true,
|
||||||
|
sort_order: 1,
|
||||||
|
},
|
||||||
|
], true);
|
||||||
|
const nextLocationBaseState = publishedMapState([
|
||||||
|
{
|
||||||
|
id: 472,
|
||||||
|
location_map_id: 902,
|
||||||
|
zone_id: 502,
|
||||||
|
zone_name: "Produce",
|
||||||
|
type: "zone",
|
||||||
|
label: "Ontario Produce",
|
||||||
|
x: 80,
|
||||||
|
y: 80,
|
||||||
|
width: 280,
|
||||||
|
height: 160,
|
||||||
|
rotation: 0,
|
||||||
|
locked: false,
|
||||||
|
visible: true,
|
||||||
|
sort_order: 1,
|
||||||
|
},
|
||||||
|
], true);
|
||||||
|
const nextPublishedMap = {
|
||||||
|
...nextLocationBaseState.published_map,
|
||||||
|
id: 902,
|
||||||
|
store_location_id: secondStoreLocation.id,
|
||||||
|
};
|
||||||
|
const nextLocationState = {
|
||||||
|
...nextLocationBaseState,
|
||||||
|
location: secondStoreLocation,
|
||||||
|
map: nextPublishedMap,
|
||||||
|
published_map: nextPublishedMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolveFirstRequestStarted: () => void = () => {};
|
||||||
|
let releaseFirstResponse: () => void = () => {};
|
||||||
|
let resolveFirstResponseSent: () => void = () => {};
|
||||||
|
const firstRequestStarted = new Promise<void>((resolve) => {
|
||||||
|
resolveFirstRequestStarted = resolve;
|
||||||
|
});
|
||||||
|
const firstResponseGate = new Promise<void>((resolve) => {
|
||||||
|
releaseFirstResponse = resolve;
|
||||||
|
});
|
||||||
|
const firstResponseSent = new Promise<void>((resolve) => {
|
||||||
|
resolveFirstResponseSent = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/locations/10/map", async (route) => {
|
||||||
|
resolveFirstRequestStarted();
|
||||||
|
await firstResponseGate;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(oldLocationState),
|
||||||
|
});
|
||||||
|
resolveFirstResponseSent();
|
||||||
|
});
|
||||||
|
await page.route("**/households/1/locations/11/map", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(nextLocationState),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/stores/100/locations/10/map");
|
||||||
|
await firstRequestStarted;
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.history.pushState({}, "", "/stores/100/locations/11/map");
|
||||||
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
|
||||||
|
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
|
||||||
|
|
||||||
|
releaseFirstResponse();
|
||||||
|
await firstResponseSent;
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "Map area Old Bakery" })).toHaveCount(0);
|
||||||
|
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
|
||||||
|
});
|
||||||
|
|
||||||
test("admin can add the first area from a blank map canvas", async ({ page }) => {
|
test("admin can add the first area from a blank map canvas", async ({ page }) => {
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user