fix(mobile-inbox): ignore stale fetches after tab switch (core#2766 / CR2 #11478) #2828
@@ -45,13 +45,31 @@ export function MobileInbox({ dark }: { dark: boolean }) {
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// Guard against stale fetches: when the user switches tabs, a previous
|
||||
// in-flight fetch for the old kind must not overwrite the list after the
|
||||
// new tab's load clears it (CR2 #11478).
|
||||
const loadSeqRef = useRef(0);
|
||||
const load = useCallback(() => {
|
||||
const seq = ++loadSeqRef.current;
|
||||
// core#2766 / CR2 #11478: clear stale rows the moment the kind changes
|
||||
// so the user never sees approval cards under the Tasks tab (or
|
||||
// vice-versa) while the new list is still fetching.
|
||||
setItems([]);
|
||||
setLoading(true);
|
||||
api
|
||||
.get<RequestRow[]>(`/requests/pending?kind=${kind}`)
|
||||
.then((rows) => setItems(Array.isArray(rows) ? rows : []))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setLoading(false));
|
||||
.then((rows) => {
|
||||
if (seq !== loadSeqRef.current) return; // stale response
|
||||
setItems(Array.isArray(rows) ? rows : []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq !== loadSeqRef.current) return; // stale response
|
||||
setItems([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (seq !== loadSeqRef.current) return; // stale response
|
||||
setLoading(false);
|
||||
});
|
||||
}, [kind]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
@@ -84,28 +84,25 @@ describe("MobileInbox", () => {
|
||||
// Switch to Tasks before the task fetch resolves.
|
||||
fireEvent.click(getByRole("tab", { name: "Tasks" }));
|
||||
|
||||
// Stale approval row is still visible.
|
||||
expect(getByText("Delete prod secret?")).toBeTruthy();
|
||||
// Primary action is still "Approve", not "Done".
|
||||
expect(getByRole("button", { name: "Approve" })).toBeTruthy();
|
||||
// Switch to Tasks. The new load clears stale rows immediately.
|
||||
fireEvent.click(getByRole("tab", { name: "Tasks" }));
|
||||
expect(queryByText("Delete prod secret?")).toBeNull();
|
||||
|
||||
// The old approval fetch now resolves. With the sequence guard it
|
||||
// must be ignored: the Task tab should NOT show approval rows.
|
||||
await act(async () => {
|
||||
fireEvent.click(getByRole("button", { name: "Approve" }));
|
||||
approvalFetch.resolve([approval]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(queryByText("Delete prod secret?")).toBeNull();
|
||||
});
|
||||
|
||||
// Must post the approval action, never the task action.
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
"/requests/req-1/respond",
|
||||
expect.objectContaining({ action: "approved" }),
|
||||
);
|
||||
expect(api.post).not.toHaveBeenCalledWith(
|
||||
"/requests/req-1/respond",
|
||||
expect.objectContaining({ action: "done" }),
|
||||
);
|
||||
|
||||
// Once the task fetch resolves, the Tasks tab shows its own content.
|
||||
await act(async () => {
|
||||
taskFetch.resolve([]);
|
||||
});
|
||||
await waitFor(() => expect(queryByText("Delete prod secret?")).toBeNull());
|
||||
await waitFor(() => {
|
||||
expect(getByText(/No pending tasks/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user