fix(notifications): Mark all read empties a band of read rows, and a read landing mid-write cannot refill it

This commit is contained in:
2026-10-28 21:54:00 +01:00
parent ee658b8bfc
commit 0469812499
2 changed files with 74 additions and 9 deletions
@@ -97,7 +97,7 @@ function mount() {
</QueryClientProvider>
);
const { rerender } = render(tree());
return { rerender: () => rerender(tree()) };
return { qc, rerender: () => rerender(tree()) };
}
// Every test has a "Ready 1" on screen once the server list has landed.
const loaded = () => screen.findByText("Ready 1");
@@ -211,4 +211,59 @@ describe("NotificationsPanel · order", () => {
expect(row.isConnected).toBe(true);
expect(document.activeElement).toBe(row);
});
it("a band of rows already read still offers Mark all read, which empties it", async () => {
// Opening the center marked this browser's notices read: the band holds them, and nothing on
// the page is unread any more.
h.server = [server(1, 1 * H, true)];
h.client = [client(2, 2 * H, { read: false })];
const { rerender } = mount();
await loaded();
h.client = [client(2, 2 * H, { read: true })]; // what the mount's markAllRead leaves
await act(async () => rerender());
expect(listUnder("inbox.sectionNeedsYou:1")).toEqual(["Activity 2"]);
const button = screen.getByRole("button", { name: "inbox.markAllRead" });
expect(button.hasAttribute("disabled")).toBe(false);
fireEvent.click(button);
await act(async () => rerender());
expect(screen.queryByText(/inbox\.sectionNeedsYou/)).toBeNull();
});
it("a read that lands before Mark all read commits does not put the rows back", async () => {
let commit!: () => void;
vi.spyOn(api, "markAllNotificationsRead").mockImplementation(
() => new Promise((resolve) => (commit = () => resolve({ ok: true } as never))),
);
h.server = [server(1, 1 * H, false)];
h.client = [];
const { qc } = mount();
await loaded();
fireEvent.click(screen.getByRole("button", { name: "inbox.markAllRead" }));
await waitFor(() => expect(api.markAllNotificationsRead).toHaveBeenCalled());
// The poll (or another row's write) reads the list before the server has committed the write.
void qc.invalidateQueries({ queryKey: ["notifications"] });
await waitFor(() =>
expect(qc.getQueryData<{ items: AppNotification[] }>(["notifications"])?.items[0]?.read).toBe(
false,
),
);
h.server = [server(1, 1 * H, true)];
await act(async () => commit());
await waitFor(() => expect(screen.queryByText(/inbox\.sectionNeedsYou/)).toBeNull());
expect(listUnder("inbox.sectionRecent")).toEqual(["Ready 1"]);
});
it("a refused Mark all read brings the rows back at once, not at the next poll", async () => {
vi.spyOn(api, "markAllNotificationsRead").mockRejectedValue(new Error("offline"));
h.server = [server(1, 1 * H, false)];
h.client = [];
mount();
await loaded();
const reads = vi.mocked(api.notifications).mock.calls.length;
fireEvent.click(screen.getByRole("button", { name: "inbox.markAllRead" }));
await waitFor(() =>
expect(vi.mocked(api.notifications).mock.calls.length).toBeGreaterThan(reads),
);
await waitFor(() => expect(listUnder("inbox.sectionNeedsYou:1")).toEqual(["Ready 1"]));
});
});