fix(header): address PR review blockers

- AuthHeader: remove duplicated locale logic, use <LanguageSwitcher inverted />
- Fix text-white/55 → text-white/70 in AppNav and LanguageSwitcher (WCAG AA)
- E2E: add axe accessibility checks, replace [data-hydrated] with role selectors,
  add 768px hamburger test and BRAND_NAVY comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-31 09:37:24 +02:00
parent 281934529e
commit 53038dea68
4 changed files with 55 additions and 24 deletions

View File

@@ -1,19 +1,29 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
// #012851 — brand-navy, defined as --c-primary in layout.css
const BRAND_NAVY = 'rgb(1, 40, 81)'; const BRAND_NAVY = 'rgb(1, 40, 81)';
test.describe('Header — brand-navy background', () => { test.describe('Header — brand-navy background', () => {
test('header background is brand-navy in light mode', async ({ page }) => { test('header background is brand-navy in light mode', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.waitForSelector('[data-hydrated]'); await expect(page.getByRole('navigation')).toBeVisible();
const bg = await page.locator('header').evaluate((el) => getComputedStyle(el).backgroundColor); const bg = await page.locator('header').evaluate((el) => getComputedStyle(el).backgroundColor);
expect(bg).toBe(BRAND_NAVY); expect(bg).toBe(BRAND_NAVY);
}); });
test('header passes accessibility audit in light mode', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
const results = await new AxeBuilder({ page }).include('header').analyze();
expect(results.violations).toEqual([]);
});
test('header background stays brand-navy after switching to dark mode', async ({ page }) => { test('header background stays brand-navy after switching to dark mode', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.waitForSelector('[data-hydrated]'); await expect(page.getByRole('navigation')).toBeVisible();
await page await page
.getByRole('banner') .getByRole('banner')
@@ -25,12 +35,40 @@ test.describe('Header — brand-navy background', () => {
expect(bg).toBe(BRAND_NAVY); expect(bg).toBe(BRAND_NAVY);
}); });
test('header passes accessibility audit in dark mode', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
await page
.getByRole('banner')
.getByRole('button', { name: /dark mode/i })
.click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
const results = await new AxeBuilder({ page }).include('header').analyze();
expect(results.violations).toEqual([]);
});
test('logo text is visible at 375px viewport', async ({ page }) => { test('logo text is visible at 375px viewport', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 }); await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/'); await page.goto('/');
await expect(page.getByRole('banner').getByText('Familienarchiv')).toBeVisible(); await expect(page.getByRole('banner').getByText('Familienarchiv')).toBeVisible();
}); });
test('hamburger menu opens on tablet viewport (768px)', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
const hamburger = page.getByRole('button', { name: /menü öffnen/i });
await expect(hamburger).toBeVisible();
await hamburger.click();
await expect(
page.getByRole('navigation', { name: /mobile/i }).or(page.locator('#mobile-nav'))
).toBeVisible();
});
}); });
test.describe('Login page — AuthHeader', () => { test.describe('Login page — AuthHeader', () => {
@@ -47,4 +85,12 @@ test.describe('Login page — AuthHeader', () => {
await expect(header.getByRole('button', { name: 'DE' })).toBeVisible(); await expect(header.getByRole('button', { name: 'DE' })).toBeVisible();
}); });
test('login page header passes accessibility audit', async ({ page }) => {
await page.goto('/login');
await expect(page.locator('header')).toBeVisible();
const results = await new AxeBuilder({ page }).include('header').analyze();
expect(results.violations).toEqual([]);
});
}); });

View File

@@ -18,7 +18,7 @@ const activeLocale = $derived(getLocale().toUpperCase());
? 'font-bold text-white' ? 'font-bold text-white'
: 'font-bold text-ink' : 'font-bold text-ink'
: inverted : inverted
? 'font-normal text-white/55 hover:text-white/85' ? 'font-normal text-white/70 hover:text-white'
: 'font-normal text-ink-3 hover:text-ink'}" : 'font-normal text-ink-3 hover:text-ink'}"
> >
{locale} {locale}

View File

@@ -44,7 +44,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {
class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent
{page.url.pathname === '/' || page.url.pathname.startsWith('/documents') {page.url.pathname === '/' || page.url.pathname.startsWith('/documents')
? 'border-b-2 border-accent text-white' ? 'border-b-2 border-accent text-white'
: 'text-white/55 hover:text-white/85'}" : 'text-white/70 hover:text-white'}"
> >
{m.nav_documents()} {m.nav_documents()}
</a> </a>
@@ -54,7 +54,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {
class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent
{page.url.pathname.startsWith('/persons') {page.url.pathname.startsWith('/persons')
? 'border-b-2 border-accent text-white' ? 'border-b-2 border-accent text-white'
: 'text-white/55 hover:text-white/85'}" : 'text-white/70 hover:text-white'}"
> >
{m.nav_persons()} {m.nav_persons()}
</a> </a>
@@ -64,7 +64,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {
class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent
{page.url.pathname.startsWith('/korrespondenz') {page.url.pathname.startsWith('/korrespondenz')
? 'border-b-2 border-accent text-white' ? 'border-b-2 border-accent text-white'
: 'text-white/55 hover:text-white/85'}" : 'text-white/70 hover:text-white'}"
> >
{m.nav_conversations()} {m.nav_conversations()}
</a> </a>
@@ -74,7 +74,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {
class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent class="my-2 inline-flex items-center px-3 font-sans text-xs font-bold tracking-widest uppercase transition-colors focus:outline-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-accent
{page.url.pathname.startsWith('/admin') {page.url.pathname.startsWith('/admin')
? 'border-b-2 border-accent text-white' ? 'border-b-2 border-accent text-white'
: 'text-white/55 hover:text-white/85'}" : 'text-white/70 hover:text-white'}"
> >
{m.nav_admin()} {m.nav_admin()}
</a> </a>

View File

@@ -1,9 +1,5 @@
<script lang="ts"> <script lang="ts">
import { setLocale, getLocale } from '$lib/paraglide/runtime'; import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
const locales = ['DE', 'EN', 'ES'] as const;
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
const activeLocale = $derived(getLocale().toUpperCase());
</script> </script>
<header class="bg-brand-navy"> <header class="bg-brand-navy">
@@ -16,18 +12,7 @@ const activeLocale = $derived(getLocale().toUpperCase());
> >
</a> </a>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#each locales as locale (locale)} <LanguageSwitcher inverted />
<button
type="button"
onclick={() => setLocale(localeMap[locale])}
class="px-1.5 py-1 font-sans text-xs tracking-widest transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent
{activeLocale === locale
? 'font-bold text-white'
: 'font-normal text-white/55 hover:text-white/85'}"
>
{locale}
</button>
{/each}
</div> </div>
</div> </div>
</div> </div>