refactoring-backend #3

Merged
lukas.pupkalipinski merged 403 commits from refactoring-backend into main 2026-05-20 20:23:46 +02:00
3 changed files with 84 additions and 4 deletions
Showing only changes of commit 8b4a2f0b71 - Show all commits

View File

@@ -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.
@@ -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.

View 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);
});
});

View File

@@ -57,11 +57,11 @@ export function useMapData(
if (debounceRef.current != null) {
clearTimeout(debounceRef.current);
}
// Show loading immediately so the skeleton / spinner appears.
setLoading(true);
setError(null);
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.
abortRef.current?.abort();
abortRef.current = new AbortController();