Share canonical user and memo query caches across creator, reaction, comment, mention, and relation lookups. Reuse list data for relation snippets and cover overlapping queries with regression tests.
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
import type { PropsWithChildren } from "react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useResolvedRelationMemos } from "@/components/MemoMetadata/Relation/useResolvedRelationMemos";
|
|
import { memoKeys } from "@/hooks/useMemoQueries";
|
|
import { useUser, userKeys, useUsersByNames, useUsersByUsernames } from "@/hooks/useUserQueries";
|
|
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
|
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
|
|
|
const clients = vi.hoisted(() => ({
|
|
batchGetUsers: vi.fn(),
|
|
getMemo: vi.fn(),
|
|
getUser: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/connect", () => ({
|
|
memoServiceClient: {
|
|
getMemo: clients.getMemo,
|
|
},
|
|
shortcutServiceClient: {},
|
|
userServiceClient: {
|
|
batchGetUsers: clients.batchGetUsers,
|
|
getUser: clients.getUser,
|
|
},
|
|
}));
|
|
|
|
const createQueryClient = () =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
const createWrapper = (queryClient: QueryClient) =>
|
|
function QueryWrapper({ children }: PropsWithChildren) {
|
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
};
|
|
|
|
describe("query deduplication", () => {
|
|
beforeEach(() => {
|
|
clients.batchGetUsers.mockReset();
|
|
clients.getMemo.mockReset();
|
|
clients.getUser.mockReset();
|
|
});
|
|
|
|
it("fetches each user name once across individual and overlapping group queries", async () => {
|
|
clients.getUser.mockImplementation(async ({ name }: { name: string }) => ({ name, username: name }) as User);
|
|
const queryClient = createQueryClient();
|
|
|
|
const { result } = renderHook(
|
|
() => ({
|
|
individual: useUser("users/1"),
|
|
firstGroup: useUsersByNames(["users/1", "users/2"]),
|
|
secondGroup: useUsersByNames(["users/2", "users/3"]),
|
|
}),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.individual.isSuccess).toBe(true);
|
|
expect(result.current.firstGroup.isSuccess).toBe(true);
|
|
expect(result.current.secondGroup.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(clients.getUser.mock.calls.map(([request]) => request.name).sort()).toEqual(["users/1", "users/2", "users/3"]);
|
|
});
|
|
|
|
it("seeds user detail queries from a username batch response", async () => {
|
|
const alice = { name: "users/1", username: "alice" } as User;
|
|
clients.batchGetUsers.mockResolvedValue({ users: [alice] });
|
|
const queryClient = createQueryClient();
|
|
const wrapper = createWrapper(queryClient);
|
|
|
|
const batch = renderHook(() => useUsersByUsernames(["alice"]), { wrapper });
|
|
await waitFor(() => expect(batch.result.current.isSuccess).toBe(true));
|
|
|
|
expect(queryClient.getQueryData(userKeys.detail(alice.name))).toBe(alice);
|
|
|
|
const detail = renderHook(() => useUser(alice.name), { wrapper });
|
|
await waitFor(() => expect(detail.result.current.isSuccess).toBe(true));
|
|
expect(clients.getUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reuses memo list data and canonical detail queries when resolving relations", async () => {
|
|
const cachedMemo = { name: "memos/cached", snippet: "Already in the list" } as Memo;
|
|
const missingMemo = { name: "memos/missing", snippet: "Fetched once" } as Memo;
|
|
clients.getMemo.mockResolvedValue(missingMemo);
|
|
|
|
const queryClient = createQueryClient();
|
|
queryClient.setQueryData(memoKeys.list({}), {
|
|
pages: [{ memos: [cachedMemo], nextPageToken: "" }],
|
|
pageParams: [""],
|
|
});
|
|
|
|
const { result } = renderHook(
|
|
() => ({
|
|
first: useResolvedRelationMemos([cachedMemo.name, missingMemo.name]),
|
|
second: useResolvedRelationMemos([missingMemo.name]),
|
|
}),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.first[cachedMemo.name]?.snippet).toBe(cachedMemo.snippet);
|
|
expect(result.current.first[missingMemo.name]?.snippet).toBe(missingMemo.snippet);
|
|
expect(result.current.second[missingMemo.name]?.snippet).toBe(missingMemo.snippet);
|
|
});
|
|
|
|
expect(clients.getMemo).toHaveBeenCalledTimes(1);
|
|
expect(clients.getMemo).toHaveBeenCalledWith({ name: missingMemo.name });
|
|
});
|
|
});
|