Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 95 additions & 56 deletions __tests__/components/waves/drop/useSingleWaveDropData.test.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,118 @@
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React from "react";
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";

import { useSingleWaveDropData } from "@/components/waves/drop/useSingleWaveDropData";
import { fetchDropV2ById } from "@/services/api/wave-drops-v2-api";
import { DropSize } from "@/helpers/waves/drop.helpers";
import {
fetchDropMetadataByIdV2,
fetchDropV2ById,
} from "@/services/api/wave-drops-v2-api";

jest.mock("@/services/api/wave-drops-v2-api", () => ({
fetchDropMetadataByIdV2: jest.fn(),
fetchDropV2ById: jest.fn(),
}));

const useWaveDataMock = jest.fn(() => ({ data: { id: "wave-1" } }));
jest.mock("@/hooks/useWaveData", () => ({
useWaveData: () => ({ data: { id: "wave-1" } }),
useWaveData: (props: unknown) => useWaveDataMock(props),
}));

const fetchDropV2ByIdMock = fetchDropV2ById as jest.MockedFunction<
typeof fetchDropV2ById
>;
const fetchDropMetadataByIdV2Mock =
fetchDropMetadataByIdV2 as jest.MockedFunction<
typeof fetchDropMetadataByIdV2
>;

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
defaultOptions: {
queries: {
retry: false,
},
},
});

return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return function Wrapper({ children }: { readonly children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
};

const createInitialDrop = (id: string) =>
({
id,
wave: { id: "wave-1" },
metadata: [{ data_key: "priority", data_value: id }],
stableHash: `${id}-hash`,
stableKey: `${id}-key`,
type: DropSize.FULL,
}) as any;

const createDeferred = <T,>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});

return { promise, resolve };
};

describe("useSingleWaveDropData", () => {
beforeEach(() => {
jest.resetAllMocks();
useWaveDataMock.mockReturnValue({ data: { id: "wave-1" } });
fetchDropMetadataByIdV2Mock.mockResolvedValue([
{ data_key: "priority", data_value: "drop-1" },
{ data_key: "title", data_value: "Full Title" },
]);
});

it("fetches single-drop detail without eager top raters", async () => {
fetchDropV2ByIdMock.mockResolvedValue({
id: "drop-1",
wave: { id: "wave-1" },
} as any);

renderHook(
() =>
useSingleWaveDropData(
{
id: "drop-1",
wave: { id: "wave-1" },
stableHash: "hash",
stableKey: "key",
} as any,
jest.fn()
),
it("fetches detail metadata without fetching single-drop detail", async () => {
const initialDrop = createInitialDrop("drop-1");

const { result } = renderHook(
() => useSingleWaveDropData(initialDrop, jest.fn()),
{ wrapper: createWrapper() }
);

expect(fetchDropV2ByIdMock).not.toHaveBeenCalled();
expect(fetchDropMetadataByIdV2Mock).toHaveBeenCalledWith(
expect.objectContaining({
dropId: "drop-1",
priorityMetadata: initialDrop.metadata,
})
);
expect(useWaveDataMock).toHaveBeenCalledWith(
expect.objectContaining({ waveId: "wave-1" })
);
expect(result.current.drop).toEqual(
expect.objectContaining({
id: "drop-1",
metadata: initialDrop.metadata,
})
);

await waitFor(() => {
expect(fetchDropV2ByIdMock).toHaveBeenCalledWith(
"drop-1",
expect.objectContaining({ aborted: false }),
{ includeTopRaters: false }
);
expect(result.current.drop.metadata).toEqual([
{ data_key: "priority", data_value: "drop-1" },
{ data_key: "title", data_value: "Full Title" },
]);
});
});

it("does not expose the previous drop while a new drop id is loading", async () => {
const secondDrop = createDeferred<any>();
fetchDropV2ByIdMock
.mockResolvedValueOnce({
expect(result.current.extendedDrop).toEqual(
expect.objectContaining({
id: "drop-1",
wave: { id: "wave-1" },
} as any)
.mockReturnValueOnce(secondDrop.promise);
type: DropSize.FULL,
stableHash: "drop-1-hash",
stableKey: "drop-1-key",
metadata: [
{ data_key: "priority", data_value: "drop-1" },
{ data_key: "title", data_value: "Full Title" },
],
})
);
});

it("switches directly to a new initial drop without exposing stale detail data", async () => {
fetchDropMetadataByIdV2Mock.mockImplementation(async ({ dropId }) => [
{ data_key: "full", data_value: dropId },
]);

const { result, rerender } = renderHook(
({ initialDrop }) => useSingleWaveDropData(initialDrop, jest.fn()),
Expand All @@ -95,22 +122,34 @@ describe("useSingleWaveDropData", () => {
}
);

expect(result.current.drop.id).toBe("drop-1");

await waitFor(() => {
expect(result.current.drop?.id).toBe("drop-1");
expect(result.current.drop.metadata).toEqual([
{ data_key: "full", data_value: "drop-1" },
]);
});

rerender({ initialDrop: createInitialDrop("drop-2") });

expect(result.current.drop).toBeUndefined();
expect(result.current.extendedDrop).toBeNull();

secondDrop.resolve({
id: "drop-2",
wave: { id: "wave-1" },
});
expect(result.current.drop.id).toBe("drop-2");
expect(result.current.extendedDrop.id).toBe("drop-2");
expect(result.current.drop.metadata).toEqual([
{ data_key: "priority", data_value: "drop-2" },
]);

await waitFor(() => {
expect(result.current.drop?.id).toBe("drop-2");
expect(result.current.drop.metadata).toEqual([
{ data_key: "full", data_value: "drop-2" },
]);
});

expect(fetchDropMetadataByIdV2Mock).toHaveBeenCalledWith(
expect.objectContaining({
dropId: "drop-2",
priorityMetadata: [{ data_key: "priority", data_value: "drop-2" }],
})
);
expect(fetchDropV2ByIdMock).not.toHaveBeenCalled();
});
});
76 changes: 70 additions & 6 deletions __tests__/components/waves/drops/WaveDropQuoteWithDropId.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ jest.mock("@/components/waves/drops/WaveDropQuote", () => (props: any) => {
});

const useQuery = jest.fn();
const getQueryData = jest.fn();
jest.mock("@tanstack/react-query", () => ({
useQuery: (opts: any) => useQuery(opts),
useQueryClient: () => ({ getQueryData }),
keepPreviousData: "keep",
}));

const useMyStreamOptional = jest.fn();
jest.mock("@/contexts/wave/MyStreamContext", () => ({
useMyStreamOptional: () => useMyStreamOptional(),
}));

jest.mock("@/services/api/drop-api", () => {
const { QueryKey: ActualQueryKey } = jest.requireActual(
"@/components/react-query-wrapper/ReactQueryWrapper"
Expand All @@ -36,6 +43,8 @@ describe("WaveDropQuoteWithDropId", () => {
beforeEach(() => {
capturedProps = undefined;
jest.clearAllMocks();
getQueryData.mockReturnValue(undefined);
useMyStreamOptional.mockReturnValue(null);
});

it("fetches drop by drop ID and renders quote when no maybeDrop exists", async () => {
Expand All @@ -62,7 +71,7 @@ describe("WaveDropQuoteWithDropId", () => {
expect(fetchDropByIdBatchedMock).toHaveBeenCalledWith("d1");
});

it("treats maybeDrop as stale initial data and fetches fresh data", async () => {
it("uses maybeDrop without fetching fresh data", () => {
const maybeDrop = { id: "d1", wave: { id: "old-wave" } };
useQuery.mockImplementation((opts: any) => {
return { data: opts.initialData };
Expand All @@ -81,11 +90,66 @@ describe("WaveDropQuoteWithDropId", () => {
expect(capturedProps.isNotFound).toBe(false);
const call = useQuery.mock.calls[0][0];
expect(call.queryKey).toEqual([QueryKey.DROP, { drop_id: "d1" }]);
expect(call.enabled).toBe(true);
expect(call.enabled).toBe(false);
expect(call.initialData).toBe(maybeDrop);
expect(call.initialDataUpdatedAt).toBe(0);
await call.queryFn();
expect(fetchDropByIdBatchedMock).toHaveBeenCalledWith("d1");
expect(call).not.toHaveProperty("initialDataUpdatedAt");
expect(fetchDropByIdBatchedMock).not.toHaveBeenCalled();
});

it("uses an already cached drop without fetching fresh data", () => {
const cachedDrop = { id: "d1", wave: { id: "cached-wave" } };
getQueryData.mockReturnValue(cachedDrop);
useQuery.mockImplementation((opts: any) => {
return { data: opts.initialData };
});

render(
<WaveDropQuoteWithDropId
dropId="d1"
partId={2}
maybeDrop={null}
onQuoteClick={jest.fn()}
/>
);

expect(capturedProps.drop).toBe(cachedDrop);
const call = useQuery.mock.calls[0][0];
expect(call.enabled).toBe(false);
expect(call.initialData).toBe(cachedDrop);
});

it("uses a full drop from wave messages without fetching by id", () => {
const waveDrop = {
id: "d1",
wave: { id: "w1" },
type: "FULL",
stableKey: "d1",
stableHash: "d1",
};
useMyStreamOptional.mockReturnValue({
activeWave: { id: "w1" },
waveMessagesStore: {
getData: jest.fn(() => ({ drops: [waveDrop] })),
},
});
useQuery.mockImplementation((opts: any) => {
return { data: opts.initialData };
});

render(
<WaveDropQuoteWithDropId
dropId="d1"
partId={2}
maybeDrop={null}
waveId="w1"
onQuoteClick={jest.fn()}
/>
);

expect(capturedProps.drop).toBe(waveDrop);
const call = useQuery.mock.calls[0][0];
expect(call.enabled).toBe(false);
expect(call.initialData).toBe(waveDrop);
});

it("passes not-found state when the refresh returns the not-found message", () => {
Expand Down Expand Up @@ -130,7 +194,7 @@ describe("WaveDropQuoteWithDropId", () => {
expect(capturedProps.drop).toBeNull();
expect(capturedProps.isNotFound).toBe(true);
const call = useQuery.mock.calls[0][0];
expect(call.enabled).toBe(true);
expect(call.enabled).toBe(false);
});

it("passes not-found state when the refresh returns a 404", () => {
Expand Down
7 changes: 5 additions & 2 deletions __tests__/services/api/drop-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ afterEach(() => {
});

describe("fetchDropsByIds", () => {
it("fetches drops with the v2 drop detail endpoint", async () => {
it("fetches drops with lean v2 drop detail hydration", async () => {
const replyDrop = { id: "reply-1" } as ApiDrop;
fetchDropV2ByIdMock.mockResolvedValue(replyDrop);

const result = await fetchDropsByIds(["reply-1"]);

expect(fetchDropV2ByIdMock).toHaveBeenCalledTimes(1);
expect(fetchDropV2ByIdMock).toHaveBeenCalledWith("reply-1");
expect(fetchDropV2ByIdMock).toHaveBeenCalledWith("reply-1", undefined, {
includeFullMetadata: false,
includeTopRaters: false,
});
expect(result).toEqual([replyDrop]);
});

Expand Down
Loading