fix(feed): an optimistic write outlives only the server answers older than it
A reset wrote a position mask over the cached list, so a replay's saved position stayed hidden: a reopen resumed at 0 and its first checkpoint wrote over the progress. The reset now writes 0 into the cache, where the player's closing patch writes the next one. Overrides carry their write order, and a landing refetch drops only those written before it set out. The player session ends after the modal unmounts, so a clip auto-marked watched as it closes still refetches the list.
This commit is contained in:
@@ -39,22 +39,36 @@ vi.mock("./VirtualFeed", () => ({
|
||||
)
|
||||
),
|
||||
}));
|
||||
vi.mock("./PlayerModal", () => ({
|
||||
default: ({
|
||||
video,
|
||||
onState,
|
||||
onClose,
|
||||
}: {
|
||||
video: { id: string };
|
||||
onState: (id: string, status: string) => void;
|
||||
onClose: () => void;
|
||||
}) => (
|
||||
<div>
|
||||
<button onClick={() => onState(video.id, "watched")}>mark</button>
|
||||
<button onClick={onClose}>close</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
// The player's progress hook marks a clip closed near its end watched from its unmount cleanup:
|
||||
// `watchOnUnmount` stands in for that.
|
||||
const player = vi.hoisted(() => ({ watchOnUnmount: false }));
|
||||
vi.mock("./PlayerModal", async () => {
|
||||
const { useEffect } = await import("react");
|
||||
return {
|
||||
default: function Player({
|
||||
video,
|
||||
onState,
|
||||
onClose,
|
||||
}: {
|
||||
video: { id: string };
|
||||
onState: (id: string, status: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (player.watchOnUnmount) onState(video.id, "watched");
|
||||
},
|
||||
[video.id, onState],
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onState(video.id, "watched")}>mark</button>
|
||||
<button onClick={onClose}>close</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import Feed from "./Feed";
|
||||
import { api, DEFAULT_TAG_MODES, type FeedFilters, type Video } from "../lib/api";
|
||||
@@ -76,6 +90,7 @@ beforeAll(() => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
player.watchOnUnmount = false;
|
||||
});
|
||||
|
||||
const FILTERS: FeedFilters = {
|
||||
@@ -164,6 +179,30 @@ describe("Feed — a video marked watched in the player", () => {
|
||||
expect(screen.queryByText("open v1")).toBeNull();
|
||||
});
|
||||
|
||||
it("refetches when the player marks the clip watched as it closes", async () => {
|
||||
const { feed } = mount();
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
player.watchOnUnmount = true;
|
||||
const before = feed.mock.calls.length;
|
||||
fireEvent.click(await screen.findByText("close"));
|
||||
await waitFor(() => expect(feed.mock.calls.length).toBeGreaterThan(before));
|
||||
});
|
||||
|
||||
it("waits for the session's writes to land before it refetches", async () => {
|
||||
const { feed } = mount();
|
||||
let land: (v: unknown) => void = () => {};
|
||||
vi.mocked(api.setState).mockReturnValue(new Promise((r) => (land = r)) as never);
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
fireEvent.click(await screen.findByText("mark"));
|
||||
const before = feed.mock.calls.length;
|
||||
fireEvent.click(screen.getByText("close"));
|
||||
await act(() => new Promise((r) => setTimeout(r, 30)));
|
||||
// A refetch now would ask a server that has not taken the mark yet.
|
||||
expect(feed.mock.calls.length).toBe(before);
|
||||
await act(async () => land({ ok: true }));
|
||||
await waitFor(() => expect(feed.mock.calls.length).toBeGreaterThan(before));
|
||||
});
|
||||
|
||||
it("does not refetch on a close with nothing changed", async () => {
|
||||
const { feed } = mount();
|
||||
fireEvent.click(await screen.findByText("open v1"));
|
||||
|
||||
Reference in New Issue
Block a user