Fix useMapData debounce loading state
This commit is contained in:
@@ -205,7 +205,7 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TASK-011 — No `React.memo` on any heavy component
|
### TASK-011 — No `React.memo` on any heavy component (done)
|
||||||
|
|
||||||
**Where found:** Every component in `frontend/src/components/` — zero uses of `React.memo` exist in the codebase.
|
**Where found:** Every component in `frontend/src/components/` — zero uses of `React.memo` exist in the codebase.
|
||||||
|
|
||||||
@@ -222,7 +222,9 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TASK-012 — `useMapData` sets `loading=true` before the debounce fires
|
### TASK-012 — `useMapData` sets `loading=true` before the debounce fires (done)
|
||||||
|
|
||||||
|
**Where fixed:** `frontend/src/hooks/useMapData.ts`, `frontend/src/hooks/__tests__/useMapData.test.ts`
|
||||||
|
|
||||||
**Where found:** `frontend/src/hooks/useMapData.ts`, `load` callback — `setLoading(true)` is called at the top of `load`, but the actual fetch is deferred inside a `setTimeout` of 300 ms.
|
**Where found:** `frontend/src/hooks/useMapData.ts`, `load` callback — `setLoading(true)` is called at the top of `load`, but the actual fetch is deferred inside a `setTimeout` of 300 ms.
|
||||||
|
|
||||||
|
|||||||
78
frontend/src/hooks/__tests__/useMapData.test.ts
Normal file
78
frontend/src/hooks/__tests__/useMapData.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||||
|
import type { BansByCountryResponse } from "../../types/map";
|
||||||
|
import { useMapData } from "../useMapData";
|
||||||
|
import * as api from "../../api/map";
|
||||||
|
|
||||||
|
vi.mock("../../api/map");
|
||||||
|
|
||||||
|
describe("useMapData", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delays loading until the debounced fetch begins", async () => {
|
||||||
|
const fetchMock = vi.mocked(api.fetchBansByCountry);
|
||||||
|
const firstResponse: BansByCountryResponse = {
|
||||||
|
countries: { US: 1 },
|
||||||
|
country_names: { US: "United States" },
|
||||||
|
bans: [],
|
||||||
|
total: 1,
|
||||||
|
};
|
||||||
|
const secondResponse: BansByCountryResponse = {
|
||||||
|
countries: { US: 2 },
|
||||||
|
country_names: { US: "United States" },
|
||||||
|
bans: [],
|
||||||
|
total: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secondResolve: (value: BansByCountryResponse) => void;
|
||||||
|
fetchMock.mockResolvedValueOnce(firstResponse);
|
||||||
|
fetchMock.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
new Promise<BansByCountryResponse>((resolve) => {
|
||||||
|
secondResolve = resolve;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useMapData("24h", "all", "fail2ban"));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 310));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
secondResolve(secondResponse);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
expect(result.current.total).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -57,11 +57,11 @@ export function useMapData(
|
|||||||
if (debounceRef.current != null) {
|
if (debounceRef.current != null) {
|
||||||
clearTimeout(debounceRef.current);
|
clearTimeout(debounceRef.current);
|
||||||
}
|
}
|
||||||
// Show loading immediately so the skeleton / spinner appears.
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
debounceRef.current = setTimeout((): void => {
|
debounceRef.current = setTimeout((): void => {
|
||||||
|
// Show loading only when the fetch is about to start.
|
||||||
|
setLoading(true);
|
||||||
// Abort any in-flight request from a previous filter selection.
|
// Abort any in-flight request from a previous filter selection.
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
abortRef.current = new AbortController();
|
abortRef.current = new AbortController();
|
||||||
|
|||||||
Reference in New Issue
Block a user