Compare commits
15 Commits
8dd9e58fa4
...
d21ba8fed2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d21ba8fed2 | ||
|
|
23cbb6be22 | ||
|
|
9260866f47 | ||
|
|
7c8811e439 | ||
|
|
ef592ddd0c | ||
|
|
6c596babcb | ||
|
|
763e9f5708 | ||
|
|
37026bbbb8 | ||
|
|
53ecfee25e | ||
|
|
fa4f8ed661 | ||
|
|
890b811bc1 | ||
|
|
ed91c9bcf6 | ||
|
|
661e8582a2 | ||
|
|
7ee038faaf | ||
|
|
ae1688319e |
@@ -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.
|
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
|
## Consequences
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function href(n: NotificationItem): string {
|
|||||||
<ul role="list" class="flex flex-col gap-2">
|
<ul role="list" class="flex flex-col gap-2">
|
||||||
{#each unread as n (n.id)}
|
{#each unread as n (n.id)}
|
||||||
<li
|
<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
|
<a
|
||||||
href={href(n)}
|
href={href(n)}
|
||||||
@@ -124,26 +124,3 @@ function href(n: NotificationItem): string {
|
|||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</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>
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ describe('AnnotationLayer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
||||||
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
|
await expect.element(page.getByTestId('annotation-delete-ann-1')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show delete button when canDraw is false even if annotation is active', async () => {
|
it('does not show delete button when canDraw is false even if annotation is active', async () => {
|
||||||
@@ -120,6 +120,6 @@ describe('AnnotationLayer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
||||||
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
|
await expect.element(page.getByTestId('annotation-delete-ann-1')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('AnnotationShape', () => {
|
|||||||
onpointerleave: () => {}
|
onpointerleave: () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
|
await expect.element(page.getByTestId('annotation-delete-ann-1')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show delete button when showDelete is true but neither hovered nor active', async () => {
|
it('does not show delete button when showDelete is true but neither hovered nor active', async () => {
|
||||||
@@ -60,7 +60,7 @@ describe('AnnotationShape', () => {
|
|||||||
onpointerleave: () => {}
|
onpointerleave: () => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page.getByTestId('annotation-delete-ann-1').query()).toBeNull();
|
await expect.element(page.getByTestId('annotation-delete-ann-1')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows delete button when showDelete is true and isHovered is true', async () => {
|
it('shows delete button when showDelete is true and isHovered is true', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { relativeTime } from '$lib/shared/utils/time';
|
import { relativeTime } from '$lib/shared/utils/time';
|
||||||
import type { NotificationItem } from '$lib/notification/notifications.svelte';
|
import type { NotificationItem } from '$lib/notification/notifications.svelte';
|
||||||
@@ -11,6 +12,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
|
let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
|
||||||
|
|
||||||
|
function handleViewAll(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
goto('/aktivitaeten');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -129,7 +136,7 @@ let { notifications, onMarkRead, onMarkAllRead, onClose }: Props = $props();
|
|||||||
<div class="border-t border-line px-4 py-2">
|
<div class="border-t border-line px-4 py-2">
|
||||||
<a
|
<a
|
||||||
href="/aktivitaeten"
|
href="/aktivitaeten"
|
||||||
onclick={onClose}
|
onclick={handleViewAll}
|
||||||
class="text-xs font-medium text-ink-2 transition-colors hover:text-ink"
|
class="text-xs font-medium text-ink-2 transition-colors hover:text-ink"
|
||||||
>
|
>
|
||||||
{m.chronik_view_all()}
|
{m.chronik_view_all()}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page } from 'vitest/browser';
|
import { page } from 'vitest/browser';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import NotificationDropdown from './NotificationDropdown.svelte';
|
import NotificationDropdown from './NotificationDropdown.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
const makeNotification = (overrides: Record<string, unknown> = {}) => ({
|
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(onClose).toHaveBeenCalledOnce();
|
||||||
|
expect(goto).toHaveBeenCalledWith('/aktivitaeten');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders MENTION items with the mention verb text', async () => {
|
it('renders MENTION items with the mention verb text', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page } from 'vitest/browser';
|
import { page } from 'vitest/browser';
|
||||||
import OcrTrainingCard from './OcrTrainingCard.svelte';
|
import OcrTrainingCard from './OcrTrainingCard.svelte';
|
||||||
@@ -74,6 +74,12 @@ describe('OcrTrainingCard — enabled state', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('OcrTrainingCard — success dismiss button', () => {
|
describe('OcrTrainingCard — success dismiss button', () => {
|
||||||
|
beforeEach(() => vi.useFakeTimers());
|
||||||
|
afterEach(() => {
|
||||||
|
vi.runAllTimers();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it('dismiss button has 44×44px touch target (h-11 w-11)', async () => {
|
it('dismiss button has 44×44px touch target (h-11 w-11)', async () => {
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
||||||
|
|
||||||
@@ -108,7 +114,9 @@ describe('OcrTrainingCard — in-flight state', () => {
|
|||||||
// While fetch is still pending the button label becomes "…"
|
// While fetch is still pending the button label becomes "…"
|
||||||
await expect.element(page.getByRole('button', { name: '…' })).toBeInTheDocument();
|
await expect.element(page.getByRole('button', { name: '…' })).toBeInTheDocument();
|
||||||
|
|
||||||
// Cleanup: resolve the pending promise
|
|
||||||
resolveFetch({ ok: false });
|
resolveFetch({ ok: false });
|
||||||
|
await expect
|
||||||
|
.element(page.getByRole('button', { name: /Training starten/i }))
|
||||||
|
.not.toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,15 +125,3 @@ const klaerungChips = [
|
|||||||
<p class="font-serif text-sm leading-relaxed text-ink-2">{m.richtlinien_closing_body()}</p>
|
<p class="font-serif text-sm leading-relaxed text-ink-2">{m.richtlinien_closing_body()}</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
@media print {
|
|
||||||
:global(.app-nav) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@page {
|
|
||||||
margin: 1.5cm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -459,3 +459,34 @@
|
|||||||
transform: translateX(350%);
|
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