Compare commits

...

10 Commits

Author SHA1 Message Date
Marcel
431287a785 fix(notification): add role=link and touch target to view-all button
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m50s
CI / OCR Service Tests (push) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m10s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Failing after 10s
CI / Backend Unit Tests (push) Successful in 4m13s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 11s
CI / Unit & Component Tests (pull_request) Failing after 1m51s
CI / OCR Service Tests (pull_request) Successful in 17s
- role="link" restores screen reader link semantics (Leonie blocker)
- min-h-[44px] px-1 meets WCAG 2.2 §2.5.8 and our 44×48px target size
- Comment in handleViewAll explains close-before-navigate ordering
- Tests updated to getByRole('link') + new call-order assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:51:28 +02:00
Marcel
9e8bb0addd docs(adr-012): correct pattern note to document button+goto, not anchor+preventDefault
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:48:15 +02:00
Marcel
f0971a23a0 fix(notification): replace view-all anchor with button to prevent iframe navigation
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m47s
CI / OCR Service Tests (pull_request) Successful in 15s
CI / Unit & Component Tests (push) Failing after 1m51s
CI / OCR Service Tests (push) Successful in 17s
CI / Backend Unit Tests (push) Successful in 4m12s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 11s
CI / Backend Unit Tests (pull_request) Successful in 4m19s
CI / fail2ban Regex (pull_request) Successful in 37s
CI / Compose Bucket Idempotency (pull_request) Failing after 10s
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 <a href> with a
<button type="button"> removes the href entirely — the interceptor never
fires — and the existing goto() mock in tests is sufficient.

Also splits the single view-all test into two focused it() blocks and
clears mocks in afterEach to prevent cross-test mock leakage.

Fixes #551

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:37:54 +02:00
Marcel
c8d052d307 docs(adr-012): add overlay navigation pattern note
Some checks failed
CI / Backend Unit Tests (pull_request) Successful in 4m12s
CI / fail2ban Regex (pull_request) Successful in 37s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
CI / Unit & Component Tests (push) Failing after 1m45s
CI / OCR Service Tests (push) Successful in 16s
CI / Backend Unit Tests (push) Successful in 4m15s
CI / fail2ban Regex (push) Successful in 40s
CI / Compose Bucket Idempotency (push) Failing after 11s
CI / Unit & Component Tests (pull_request) Failing after 1m52s
CI / OCR Service Tests (pull_request) Successful in 16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:25:54 +02:00
Marcel
7aa129977d refactor(notification): extract handleViewAll named function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:24:58 +02:00
Marcel
22704a14d6 test(notification): assert href is preserved on view-all link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:20:46 +02:00
Marcel
0a6a3fd03a fix(style): move transkription print styles to global CSS to suppress Tailwind noise
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m53s
CI / OCR Service Tests (push) Successful in 16s
CI / Backend Unit Tests (push) Successful in 4m8s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 11s
CI / Unit & Component Tests (pull_request) Failing after 1m47s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m10s
CI / fail2ban Regex (pull_request) Successful in 37s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:49:43 +02:00
Marcel
97bb1ee60d fix(style): move ChronikFuerDichBox animation to global CSS to suppress Tailwind noise
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:49:10 +02:00
Marcel
0387d51e15 fix(notification): prevent iframe navigation — use goto instead of href follow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:48:57 +02:00
Marcel
f6a0d7aa3e test(notification): add goto mock and tighten selector in NotificationDropdown spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:48:57 +02:00
6 changed files with 87 additions and 43 deletions

View File

@@ -79,6 +79,8 @@ 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 triggers a navigation action, use `<button type="button">` with an `onclick` handler that calls `goto(path)` — do **not** use `<a href="…">` with `e.preventDefault()`. SvelteKit registers its link interceptor as a capture-phase `document` listener, so it fires before the component's bubble-phase `onclick`. By the time `e.preventDefault()` runs the router has already initiated navigation, which tears down the vitest-browser Playwright orchestrator iframe. A `<button>` carries no `href`, so the capture-phase interceptor never fires. See `NotificationDropdown.svelte` for the canonical example.
---
## Consequences

View File

@@ -79,7 +79,7 @@ function href(n: NotificationItem): string {
<ul role="list" class="flex flex-col gap-2">
{#each unread as n (n.id)}
<li
class="fade-in group flex items-start gap-3 rounded-sm p-2 transition-colors hover:bg-canvas"
class="chronik-fade-in group flex items-start gap-3 rounded-sm p-2 transition-colors hover:bg-canvas"
>
<a
href={href(n)}
@@ -124,26 +124,3 @@ function href(n: NotificationItem): string {
</ul>
{/if}
</section>
<style>
.fade-in {
animation: chronik-fade-in 160ms ease-out;
}
@keyframes chronik-fade-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.fade-in {
animation: none;
}
}
</style>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
import { relativeTime } from '$lib/shared/utils/time';
import type { NotificationItem } from '$lib/notification/notifications.svelte';
@@ -11,6 +12,11 @@ type Props = {
};
let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
function handleViewAll() {
onClose(); // close first — avoids stale dropdown during navigation transition
goto('/aktivitaeten');
}
</script>
<div
@@ -127,12 +133,13 @@ let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
{/if}
<div class="border-t border-line px-4 py-2">
<a
href="/aktivitaeten"
onclick={onClose}
class="text-xs font-medium text-ink-2 transition-colors hover:text-ink"
<button
type="button"
role="link"
onclick={handleViewAll}
class="min-h-[44px] px-1 text-xs font-medium text-ink-2 transition-colors hover:text-ink"
>
{m.chronik_view_all()}
</a>
</button>
</div>
</div>

View File

@@ -1,9 +1,15 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { goto } from '$app/navigation';
import NotificationDropdown from './NotificationDropdown.svelte';
afterEach(cleanup);
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const makeNotification = (overrides: Record<string, unknown> = {}) => ({
id: 'n1',
@@ -164,11 +170,44 @@ describe('NotificationDropdown', () => {
}
});
await page.getByRole('link').click();
await page.getByRole('link', { name: /alle aktivitäten|view all/i }).click();
expect(onClose).toHaveBeenCalledOnce();
});
it('navigates to /aktivitaeten when the view-all link is clicked', async () => {
render(NotificationDropdown, {
props: {
notifications: [],
onMarkRead: () => {},
onMarkAllRead: () => {},
onClose: () => {}
}
});
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<typeof vi.fn>).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: {

View File

@@ -125,15 +125,3 @@ const klaerungChips = [
<p class="font-serif text-sm leading-relaxed text-ink-2">{m.richtlinien_closing_body()}</p>
</div>
</main>
<style>
@media print {
:global(.app-nav) {
display: none;
}
@page {
margin: 1.5cm;
}
}
</style>

View File

@@ -459,3 +459,34 @@
transform: translateX(350%);
}
}
@media print {
:global(.app-nav) {
display: none;
}
@page {
margin: 1.5cm;
}
}
.chronik-fade-in {
animation: chronik-fade-in 160ms ease-out;
}
@keyframes chronik-fade-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.chronik-fade-in {
animation: none;
}
}