From 8ccc9aba1a99c5481aedc3bab8d51ca41f5ae443 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 16:37:54 +0200 Subject: [PATCH 1/5] fix(notification): replace view-all anchor with button to prevent iframe navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SvelteKit's capture-phase link interceptor fires before the component's onclick handler, so e.preventDefault() was structurally too late to stop iframe navigation in vitest-browser. Replacing the with a diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts index e72573be..3c410403 100644 --- a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts +++ b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts @@ -6,7 +6,10 @@ import NotificationDropdown from './NotificationDropdown.svelte'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); -afterEach(cleanup); +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); const makeNotification = (overrides: Record = {}) => ({ id: 'n1', @@ -156,7 +159,7 @@ describe('NotificationDropdown', () => { expect(onMarkAllRead).toHaveBeenCalledOnce(); }); - it('calls onClose when the view-all link is clicked', async () => { + it('calls onClose when the view-all button is clicked', async () => { const onClose = vi.fn(); render(NotificationDropdown, { props: { @@ -167,11 +170,23 @@ describe('NotificationDropdown', () => { } }); - const viewAllLink = page.getByRole('link', { name: /alle aktivitäten|view all/i }); - await expect.element(viewAllLink).toHaveAttribute('href', '/aktivitaeten'); - await viewAllLink.click(); + await page.getByRole('button', { name: /alle aktivitäten|view all/i }).click(); expect(onClose).toHaveBeenCalledOnce(); + }); + + it('navigates to /aktivitaeten when the view-all button is clicked', async () => { + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await page.getByRole('button', { name: /alle aktivitäten|view all/i }).click(); + expect(goto).toHaveBeenCalledWith('/aktivitaeten'); }); -- 2.49.1 From 3005782a75f54576f90585bd745cdf59f3e75e4b Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 16:48:15 +0200 Subject: [PATCH 2/5] docs(adr-012): correct pattern note to document button+goto, not anchor+preventDefault Co-Authored-By: Claude Sonnet 4.6 --- docs/adr/012-browser-test-mocking-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/012-browser-test-mocking-strategy.md b/docs/adr/012-browser-test-mocking-strategy.md index 0e33b309..d916fc10 100644 --- a/docs/adr/012-browser-test-mocking-strategy.md +++ b/docs/adr/012-browser-test-mocking-strategy.md @@ -79,7 +79,7 @@ The following `vi.mock(module, factory)` calls in browser specs are **acceptable These modules are resolved at static import time (before any test runs). Their `vi.mock` factories are served by birpc synchronously during module graph resolution, not after worker teardown. -**Pattern note:** When an overlay or dropdown contains a navigation link (``), use `e.preventDefault()` + `goto(path)` in the click handler instead of letting the browser follow the `href`. In a vitest-browser Playwright iframe there is no SvelteKit router, so a real navigation tears down the orchestrator iframe and crashes the test run. The `href` attribute should still be present for right-click / open-in-new-tab semantics. +**Pattern note:** When an overlay or dropdown triggers a navigation action, use ` diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts index 3c410403..5337b752 100644 --- a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts +++ b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts @@ -159,7 +159,7 @@ describe('NotificationDropdown', () => { expect(onMarkAllRead).toHaveBeenCalledOnce(); }); - it('calls onClose when the view-all button is clicked', async () => { + it('calls onClose when the view-all link is clicked', async () => { const onClose = vi.fn(); render(NotificationDropdown, { props: { @@ -170,12 +170,12 @@ describe('NotificationDropdown', () => { } }); - await page.getByRole('button', { name: /alle aktivitäten|view all/i }).click(); + await page.getByRole('link', { name: /alle aktivitäten|view all/i }).click(); expect(onClose).toHaveBeenCalledOnce(); }); - it('navigates to /aktivitaeten when the view-all button is clicked', async () => { + it('navigates to /aktivitaeten when the view-all link is clicked', async () => { render(NotificationDropdown, { props: { notifications: [], @@ -185,11 +185,29 @@ describe('NotificationDropdown', () => { } }); - await page.getByRole('button', { name: /alle aktivitäten|view all/i }).click(); + await page.getByRole('link', { name: /alle aktivitäten|view all/i }).click(); expect(goto).toHaveBeenCalledWith('/aktivitaeten'); }); + it('calls onClose before navigating to /aktivitaeten', async () => { + const callOrder: string[] = []; + const onClose = vi.fn(() => callOrder.push('close')); + (goto as ReturnType).mockImplementation(() => callOrder.push('goto')); + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose + } + }); + + await page.getByRole('link', { name: /alle aktivitäten|view all/i }).click(); + + expect(callOrder).toEqual(['close', 'goto']); + }); + it('renders MENTION items with the mention verb text', async () => { render(NotificationDropdown, { props: { -- 2.49.1 From 6b785579544e5f52c5fd8cfc28791e23e280822d Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 17:50:55 +0200 Subject: [PATCH 4/5] refactor(notification-tests): use vi.mocked instead of type cast in call-order test Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/notification/NotificationDropdown.svelte.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts index 5337b752..561ec96d 100644 --- a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts +++ b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts @@ -193,7 +193,7 @@ describe('NotificationDropdown', () => { it('calls onClose before navigating to /aktivitaeten', async () => { const callOrder: string[] = []; const onClose = vi.fn(() => callOrder.push('close')); - (goto as ReturnType).mockImplementation(() => callOrder.push('goto')); + vi.mocked(goto).mockImplementation(() => callOrder.push('goto')); render(NotificationDropdown, { props: { notifications: [], -- 2.49.1 From 89860403f6ae42bf9e24d806d7db957755027581 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 18:01:38 +0200 Subject: [PATCH 5/5] =?UTF-8?q?fix(notification):=20remove=20role=3Dlink?= =?UTF-8?q?=20from=20view-all=20button=20=E2=80=94=20restores=20semantical?= =?UTF-8?q?ly=20honest=20button=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The role=link override on a