diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte b/frontend/src/lib/notification/NotificationDropdown.svelte index 115fe4f7..903ed5ce 100644 --- a/frontend/src/lib/notification/NotificationDropdown.svelte +++ b/frontend/src/lib/notification/NotificationDropdown.svelte @@ -84,15 +84,17 @@ function handleViewAll() { class="contents" use:enhance={() => { optimisticMarkRead(notification.id); - onClose(); - goto( - buildCommentHref( - notification.documentId, - notification.referenceId, - notification.annotationId - ) - ); - return async ({ update }) => { + return async ({ result, update }) => { + if (result.type !== 'failure') { + onClose(); + goto( + buildCommentHref( + notification.documentId, + notification.referenceId, + notification.annotationId + ) + ); + } await update({ reset: false, invalidateAll: false }); }; }} diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts index 91568d78..459bd72b 100644 --- a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts +++ b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts @@ -6,13 +6,28 @@ import NotificationDropdown from './NotificationDropdown.svelte'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); -// Invoke the SubmitFunction synchronously on form submit so tests can assert -// that optimisticMarkRead / optimisticMarkAllRead are called. +// Configurable result for the enhance mock — tests that need failure set +// mockFormResult.type = 'failure' before clicking. +const mockFormResult = vi.hoisted(() => ({ type: 'success' as string })); + +// Invoke the SubmitFunction and always call the returned result callback with +// mockFormResult so tests can exercise both success and failure branches. vi.mock('$app/forms', () => ({ - enhance(node: HTMLFormElement, submit?: (opts: { formData: FormData }) => unknown) { - const handler = (e: Event) => { + enhance( + node: HTMLFormElement, + submit?: (opts: { + formData: FormData; + }) => (opts: { + result: { type: string; data?: Record }; + update: () => Promise; + }) => Promise + ) { + const handler = async (e: Event) => { e.preventDefault(); - submit?.({ formData: new FormData(node) } as never); + const cb = submit?.({ formData: new FormData(node) } as never); + if (typeof cb === 'function') { + await cb({ result: mockFormResult, update: async () => {} } as never); + } }; node.addEventListener('submit', handler); return { destroy: () => node.removeEventListener('submit', handler) }; @@ -22,6 +37,7 @@ vi.mock('$app/forms', () => ({ afterEach(() => { cleanup(); vi.clearAllMocks(); + mockFormResult.type = 'success'; // reset to default after each test }); const makeNotification = (overrides: Record = {}) => ({ @@ -313,4 +329,69 @@ describe('NotificationDropdown', () => { const forms = document.querySelectorAll('form[action="/aktivitaeten?/dismiss-notification"]'); expect(forms.length).toBe(2); }); + + it('calls onClose and goto with the deep-link URL after a successful dismiss', async () => { + const onClose = vi.fn(); + const n = makeNotification({ + id: 'n42', + documentId: 'd1', + referenceId: 'c1', + annotationId: null, + actorName: 'Anna' + }); + render(NotificationDropdown, { + props: { + notifications: [n], + optimisticMarkRead: () => {}, + optimisticMarkAllRead: () => {}, + onClose + } + }); + + await page.getByRole('button', { name: /Anna hat auf deinen/i }).click(); + + expect(onClose).toHaveBeenCalledOnce(); + expect(goto).toHaveBeenCalledWith('/documents/d1?commentId=c1'); + }); + + it('does NOT call onClose or goto when the dismiss action returns a failure', async () => { + mockFormResult.type = 'failure'; + const onClose = vi.fn(); + const n = makeNotification({ id: 'n99', actorName: 'Bob' }); + render(NotificationDropdown, { + props: { + notifications: [n], + optimisticMarkRead: () => {}, + optimisticMarkAllRead: () => {}, + onClose + } + }); + + await page.getByRole('button', { name: /Bob hat auf deinen/i }).click(); + + expect(onClose).not.toHaveBeenCalled(); + expect(goto).not.toHaveBeenCalled(); + }); + + it('calls goto with annotationId appended when the notification has an annotationId', async () => { + const n = makeNotification({ + id: 'n55', + documentId: 'd1', + referenceId: 'c1', + annotationId: 'a1', + actorName: 'Eva' + }); + render(NotificationDropdown, { + props: { + notifications: [n], + optimisticMarkRead: () => {}, + optimisticMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await page.getByRole('button', { name: /Eva hat auf deinen/i }).click(); + + expect(goto).toHaveBeenCalledWith('/documents/d1?commentId=c1&annotationId=a1'); + }); });