fix(test): NotificationDropdown iframe navigation crash + Tailwind CI noise #548

Merged
marcel merged 7 commits from feat/issue-545-notification-dropdown-iframe-fix into main 2026-05-12 11:35:41 +02:00
6 changed files with 49 additions and 38 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 contains a navigation link (`<a href="…">`), 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.
---
## 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,12 @@ type Props = {
};
let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
function handleViewAll(e: MouseEvent) {
e.preventDefault();
onClose();
goto('/aktivitaeten');
}
</script>
<div
@@ -129,7 +136,7 @@ let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
<div class="border-t border-line px-4 py-2">
<a
href="/aktivitaeten"
onclick={onClose}
onclick={handleViewAll}
class="text-xs font-medium text-ink-2 transition-colors hover:text-ink"
>
{m.chronik_view_all()}

View File

@@ -1,8 +1,11 @@
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';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
afterEach(cleanup);
const makeNotification = (overrides: Record<string, unknown> = {}) => ({
@@ -164,9 +167,12 @@ describe('NotificationDropdown', () => {
}
});
await page.getByRole('link').click();
const viewAllLink = page.getByRole('link', { name: /alle aktivitäten|view all/i });
await expect.element(viewAllLink).toHaveAttribute('href', '/aktivitaeten');
await viewAllLink.click();
expect(onClose).toHaveBeenCalledOnce();
expect(goto).toHaveBeenCalledWith('/aktivitaeten');
});
it('renders MENTION items with the mention verb text', async () => {

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;
}
}