mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
test(queries): pin achievements hook collapse + audit analytics consumers
v1.4.34 IW-F-Perf — three changes. 1) New `use-achievements-query.test.tsx` mounts three consumers under one TanStack QueryClient and asserts the shared queryKey collapses them into a single network call. Pre-fix the notifier issued its own request alongside the card; the regression guard surfaces it immediately if a future drift reintroduces a per-user discriminator. 2) Refresh `recent-achievements-card.test.tsx` and `achievements/__tests__/page.test.tsx` to mock the new shared hook directly instead of the raw `@tanstack/react-query` `useQuery` shim. The two test suites stay scoped to render-shape, not the network plumbing. 3) Capture the `/api/analytics` slim-vs-thick audit in `.planning/round-v1434-iwfperf-analytics-audit.md`. The dashboard root reads `bpInTargetPct*` + `glucoseByContext` so it must stay thick; the checklist is correctly slim by IW2 design but is mounted inside the dashboard render tree so its slim call duplicates the thick one already in flight. Documented two clean migration paths and flagged the work for v1.4.34.x behind IW-B's in-flight slim-slice routing.
This commit is contained in:
1 parent
3b77b6a073
commit
c6345b71fa
4 files changed
+234
-4
No files matched your search
@@ -155,8 +155,11 @@ const mockData = {
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: mockData, isLoading: false }),
|
||||
// v1.4.34 IW-F-Perf — page now reads through the shared
|
||||
// `useAchievementsQuery` hook; mock it directly so the test stays
|
||||
// scoped to the page render and bypasses the TanStack provider.
|
||||
vi.mock("@/lib/queries/use-achievements-query", () => ({
|
||||
useAchievementsQuery: () => ({ data: mockData, isLoading: false }),
|
||||
}));
|
||||
|
||||
import AchievementsPage, { groupByCategory } from "../page";
|
||||
|
||||
@@ -28,8 +28,11 @@ let mockData: {
|
||||
metrics: {},
|
||||
};
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: mockData }),
|
||||
// v1.4.34 IW-F-Perf — the card now reads through the shared
|
||||
// `useAchievementsQuery` hook; the test mocks the hook directly so the
|
||||
// network layer + TanStack provider scaffolding stays out of scope.
|
||||
vi.mock("@/lib/queries/use-achievements-query", () => ({
|
||||
useAchievementsQuery: () => ({ data: mockData }),
|
||||
}));
|
||||
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* v1.4.34 IW-F-Perf — consumer-collapse contract.
|
||||
*
|
||||
* Three independent consumers
|
||||
* (`<RecentAchievementsCard>`, `/achievements` mother page,
|
||||
* `<AchievementUnlockNotifier>`) all read through
|
||||
* `useAchievementsQuery()`. The shared hook is the only seam where the
|
||||
* queryKey is centralised; if a future drift reintroduces a per-user
|
||||
* discriminator or a literal `["gamification", "achievements"]` shadow
|
||||
* key, this test surfaces the regression before HAR proves it again
|
||||
* in production.
|
||||
*
|
||||
* Two contracts pinned:
|
||||
*
|
||||
* 1. Mounting all three consumers under one TanStack QueryClient
|
||||
* produces exactly one network call. The shared cache cell wins;
|
||||
* the divergent v1.4.33 keys are gone.
|
||||
* 2. The unlock notifier's polling interval rides the shared cache,
|
||||
* so the card never re-renders on the 2-minute cadence by itself
|
||||
* — the only refetch fires through the notifier's hook instance
|
||||
* and updates the single cache cell that both consumers read.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { useAchievementsQuery } from "../use-achievements-query";
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ isAuthenticated: true }),
|
||||
}));
|
||||
|
||||
const samplePayload = {
|
||||
summary: { unlockedCount: 0, totalCount: 0 },
|
||||
achievements: [],
|
||||
metrics: {},
|
||||
};
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockReset();
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: samplePayload }),
|
||||
});
|
||||
// `fetch` is part of the Node global in vitest's node environment; the
|
||||
// hook reaches for it via `fetch(...)` directly.
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function ConsumerA() {
|
||||
useAchievementsQuery();
|
||||
return null;
|
||||
}
|
||||
|
||||
function ConsumerB() {
|
||||
useAchievementsQuery();
|
||||
return null;
|
||||
}
|
||||
|
||||
function ConsumerC() {
|
||||
useAchievementsQuery({ refetchInterval: 2 * 60 * 1000 });
|
||||
return null;
|
||||
}
|
||||
|
||||
function withQueryClient(node: ReactNode): string {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Force the query to actually fire on mount so the test can
|
||||
// count the network calls — the hook's `refetchOnMount: false`
|
||||
// governs subsequent mounts within the staleTime window, not
|
||||
// the first mount.
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("useAchievementsQuery — consumer collapse", () => {
|
||||
it("fires a single network call when three consumers mount together", async () => {
|
||||
withQueryClient(
|
||||
<>
|
||||
<ConsumerA />
|
||||
<ConsumerB />
|
||||
<ConsumerC />
|
||||
</>,
|
||||
);
|
||||
|
||||
// SSR renders are synchronous, so the queryFn is enqueued but not
|
||||
// awaited inside renderToStaticMarkup. The microtask flush below
|
||||
// mirrors what react-query does between mount and the network
|
||||
// call — one tick is enough to see the fetch attempt.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
// The three consumers share `queryKeys.gamificationAchievements()`
|
||||
// so TanStack hands every subscriber the same cache slot; one cell
|
||||
// = one in-flight queryFn = one fetch. Pre-fix the notifier carried
|
||||
// a `userId` discriminator and fired its own request.
|
||||
expect(fetchMock.mock.calls.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("uses /api/gamification/achievements without query params", async () => {
|
||||
withQueryClient(<ConsumerA />);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
if (fetchMock.mock.calls.length > 0) {
|
||||
const [url] = fetchMock.mock.calls[0]!;
|
||||
expect(url).toBe("/api/gamification/achievements");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user