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
108 changes: 108 additions & 0 deletions __tests__/components/waves/drops/WaveDropsAll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,114 @@ describe("WaveDropsAll", () => {
});
});

describe("Serial Target Hydration Suspension", () => {
it("releases light drop hydration when target fetching rejects", async () => {
const consoleWarn = jest
.spyOn(console, "warn")
.mockImplementation(() => {});

setupMocks({
waveMessages: {
drops: [createMockDrop({ id: "drop-50", serial_no: 50 })],
hasNextPage: true,
isLoading: false,
isLoadingNextPage: false,
},
});

mockFetchNextPage.mockRejectedValueOnce(new Error("Network error"));

renderComponent({ initialDrop: 10 });

expect(dropsProps.suspendLightDropHydration).toBe(true);

await waitFor(() => {
expect(mockFetchNextPage).toHaveBeenCalledWith(
{
waveId: "test-wave-1",
type: "LIGHT",
targetSerialNo: 10,
},
null
);
});

await waitFor(() => {
expect(dropsProps.suspendLightDropHydration).toBe(false);
});
expect(mockWaitAndRevealDrop).not.toHaveBeenCalled();
consoleWarn.mockRestore();
});

it("releases light drop hydration when target reveal fails", async () => {
setupMocks({
waveMessages: {
drops: [createMockDrop({ id: "drop-50", serial_no: 50 })],
hasNextPage: true,
isLoading: false,
isLoadingNextPage: false,
},
});

mockFetchNextPage.mockResolvedValueOnce([]);
mockWaitAndRevealDrop.mockResolvedValueOnce(false);

renderComponent({ initialDrop: 10 });

expect(dropsProps.suspendLightDropHydration).toBe(true);

await waitFor(() => {
expect(mockWaitAndRevealDrop).toHaveBeenCalledWith(10);
});

await waitFor(() => {
expect(dropsProps.suspendLightDropHydration).toBe(false);
});
});

it("keeps light drop hydration suspended until successful target scroll settles", async () => {
setupMocks({
waveMessages: {
drops: [createMockDrop({ id: "drop-50", serial_no: 50 })],
hasNextPage: true,
isLoading: false,
isLoadingNextPage: false,
},
});

mockFetchNextPage.mockResolvedValueOnce([]);
mockWaitAndRevealDrop.mockResolvedValueOnce(true);

renderComponent({ initialDrop: 10 });

const targetElement = document.createElement("div");
targetElement.scrollIntoView = jest.fn();
Object.defineProperty(targetElement, "getBoundingClientRect", {
value: () => ({ top: 0, bottom: 1 }),
});
dropsProps.targetDropRef.current = targetElement;

expect(dropsProps.suspendLightDropHydration).toBe(true);

await waitFor(() => {
expect(mockWaitAndRevealDrop).toHaveBeenCalledWith(10);
});

await waitFor(() => {
expect(targetElement.scrollIntoView).toHaveBeenCalled();
});
expect(dropsProps.suspendLightDropHydration).toBe(true);

act(() => {
jest.advanceTimersByTime(600);
});

await waitFor(() => {
expect(dropsProps.suspendLightDropHydration).toBe(false);
});
});
});

describe("Error Handling and Edge Cases", () => {
it("handles fetchNextPage failures gracefully", async () => {
const consoleError = jest
Expand Down
4 changes: 4 additions & 0 deletions components/drops/view/DropsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface DropsListProps {
readonly boostedDrops?: ApiDrop[] | undefined;
readonly onBoostedDropClick?: ((serialNo: number) => void) | undefined;
readonly autoCollapseSerials?: ReadonlySet<number> | undefined;
readonly suspendLightDropHydration?: boolean | undefined;
}

const MemoizedDrop = memo(Drop);
Expand All @@ -71,6 +72,7 @@ const DropsList = memo(
boostedDrops,
onBoostedDropClick,
autoCollapseSerials,
suspendLightDropHydration = false,
}: DropsListProps) => {
const handleReply = useCallback<DropActionHandler>(
({ drop, partId }) => onReply({ drop, partId }),
Expand Down Expand Up @@ -274,6 +276,7 @@ const DropsList = memo(
dropSerialNo={drop.serial_no}
waveId={drop.type === DropSize.FULL ? drop.wave.id : drop.waveId}
type={drop.type}
suspendLightDropHydration={suspendLightDropHydration}
>
{dropContentWithPreviewMode}
</VirtualScrollWrapper>
Expand All @@ -289,6 +292,7 @@ const DropsList = memo(
renderBoostCard,
renderUnreadDivider,
autoCollapseSerials,
suspendLightDropHydration,
]);
}
);
Expand Down
25 changes: 15 additions & 10 deletions components/waves/drops/VirtualScrollWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
"use client";

import type {
ReactNode} from "react";
import React, {
useState,
useRef,
useEffect,
useCallback,
} from "react";
import type { ReactNode } from "react";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { DropSize } from "@/helpers/waves/drop.helpers";
import { useMyStream } from "@/contexts/wave/MyStreamContext";

Expand All @@ -26,6 +20,7 @@ interface VirtualScrollWrapperProps {
readonly dropSerialNo: number;
readonly waveId: string;
readonly type: DropSize;
readonly suspendLightDropHydration?: boolean | undefined;

/**
* The child components to be rendered or virtualized.
Expand Down Expand Up @@ -61,6 +56,7 @@ export default function VirtualScrollWrapper({
dropSerialNo,
waveId,
type,
suspendLightDropHydration = false,
}: VirtualScrollWrapperProps) {
const { fetchAroundSerialNo } = useMyStream();

Expand Down Expand Up @@ -126,7 +122,7 @@ export default function VirtualScrollWrapper({
if (inView !== isInView) {
setIsInView(inView);
}
if (inView && type === DropSize.LIGHT) {
if (inView && type === DropSize.LIGHT && !suspendLightDropHydration) {
fetchAroundSerialNo(waveId, dropSerialNo);
}
},
Expand All @@ -150,7 +146,16 @@ export default function VirtualScrollWrapper({
observer.unobserve(containerRef.current);
}
};
}, [isInView, measureHeight]);
}, [
dropSerialNo,
fetchAroundSerialNo,
isInView,
measureHeight,
scrollContainerRef,
suspendLightDropHydration,
type,
waveId,
]);

/**
* Determine if we should render the actual children
Expand Down
Loading
Loading