Compare commits
10 Commits
bc2dd3a98a
...
431287a785
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
431287a785 | ||
|
|
9e8bb0addd | ||
|
|
f0971a23a0 | ||
|
|
c8d052d307 | ||
|
|
7aa129977d | ||
|
|
22704a14d6 | ||
|
|
0a6a3fd03a | ||
|
|
97bb1ee60d | ||
|
|
0387d51e15 | ||
|
|
f6a0d7aa3e |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user