From 2ebcbfac3c82217a77ea16c9ed98d1c71128a806 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:48:27 +0200 Subject: [PATCH] fix(notifications): type pages and guard missing rollback state Specify the infinite query response and page parameter types. Preserve the original mutation error when optimistic preparation fails before rollback state is available. Add a regression for failed optimistic preparation. All 19 API tests, 42 component tests, formatting and source review pass. Full types, lint, coverage and remaining WIP gates still need validation. --- .../api/useNotificationAPI.nuxt.test.ts | 44 ++++++++++++++++++- web/app/composables/api/useNotificationAPI.ts | 6 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/web/app/composables/api/useNotificationAPI.nuxt.test.ts b/web/app/composables/api/useNotificationAPI.nuxt.test.ts index ef7eec22..18cfbdd5 100644 --- a/web/app/composables/api/useNotificationAPI.nuxt.test.ts +++ b/web/app/composables/api/useNotificationAPI.nuxt.test.ts @@ -1,5 +1,5 @@ import type { EntryKey, UseInfiniteQueryData } from "@pinia/colada" -import { afterEach, beforeEach, describe, it } from "vitest" +import { afterEach, beforeEach, describe, it, vi } from "vitest" import { clearQueryCache, disposeMockEndpoints, @@ -305,6 +305,48 @@ describe("useNotificationAPI", { concurrent: false }, () => { }) describe("markNotificationsRead", () => { + it("preserves the error when preparing the optimistic update fails", async ({ + expect, + }) => { + const listCalls = mockEndpoint("GET", "/api/notifications", () => + makePage([]), + ) + const countCalls = mockEndpoint( + "GET", + "/api/notifications/count", + () => ({ count: 0 }), + ) + const putCalls = mockEndpoint( + "PUT", + "/api/notifications/read-status", + () => ({}), + ) + const page = makePage([makeNotification("n1", false)]) + seedPages(LIST_KEY_A, page) + const api = makeNotificationAPI() + const queryCache = runInApp(() => useQueryCache()) + const error = new Error("Cache unavailable") + const getQueryData = vi + .spyOn(queryCache, "getQueryData") + .mockImplementationOnce(() => { + throw error + }) + + try { + await expect( + api.markNotificationsRead.mutateAsync({ ids: ["n1"] }), + ).rejects.toBe(error) + expect(getQueryData).toHaveBeenCalledExactlyOnceWith(LIST_KEY_A) + expect(putCalls).toHaveLength(0) + expect(listCalls).toHaveLength(0) + expect(countCalls).toHaveLength(0) + } finally { + getQueryData.mockRestore() + } + + expect(getPages(LIST_KEY_A)).toEqual(makePages(page)) + }) + it.for([ { name: "one notification", ids: ["n1"], remaining: ["n2"] }, { name: "all notifications", ids: [], remaining: [] }, diff --git a/web/app/composables/api/useNotificationAPI.ts b/web/app/composables/api/useNotificationAPI.ts index fcc02869..73fd8d5a 100644 --- a/web/app/composables/api/useNotificationAPI.ts +++ b/web/app/composables/api/useNotificationAPI.ts @@ -18,7 +18,7 @@ export default function () { function useFetchManyNotifications( paramsRef: MaybeRefOrGetter, ) { - return useInfiniteQuery(() => { + return useInfiniteQuery(() => { const { limit, read } = toValue(paramsRef) return { @@ -136,6 +136,10 @@ export default function () { }, onSuccess: invalidateNotifications, onError(_err, _data, { oldNotifs, newNotifs }) { + if (!newNotifs) { + return + } + const entries = queryCache.getEntries({ key: NOTIFICATION_QUERY_KEYS.listRoot, // switching filters can populate another cache while the