fix(#118): resolve wcag2a/wcag2aa violations found by axe-core suite
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
- Add <svelte:head><title> to home, persons, admin, login, and error pages - Add aria-label to hidden file input in DropZone (sr-only but must be labelled) - Add aria-label to search input in SearchFilterBar - Create +error.svelte so error pages always have a document title - axe-core spec: add buildAxe() helper, disable color-contrast (brand palette, tracked separately) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@ import { test, expect } from '@playwright/test';
|
|||||||
* Authenticated pages use the stored admin session from playwright.config.ts.
|
* Authenticated pages use the stored admin session from playwright.config.ts.
|
||||||
* The login page test overrides to an unauthenticated context.
|
* The login page test overrides to an unauthenticated context.
|
||||||
*
|
*
|
||||||
* On first run: if violations are found they are logged with full details so
|
* Known exclusion:
|
||||||
* that they can be either fixed or explicitly excluded here with a comment
|
* color-contrast — brand palette (ink-3, text-ink/60) does not meet AA contrast
|
||||||
* explaining the reason.
|
* ratios. Requires a design review with Leonie before fixing. Tracked separately.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AUTHENTICATED_PAGES = [
|
const AUTHENTICATED_PAGES = [
|
||||||
@@ -17,13 +17,17 @@ const AUTHENTICATED_PAGES = [
|
|||||||
{ name: 'admin', path: '/admin' }
|
{ name: 'admin', path: '/admin' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function buildAxe(page: Parameters<typeof AxeBuilder>[0]['page']) {
|
||||||
|
return new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).disableRules(['color-contrast']);
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Accessibility — authenticated pages', () => {
|
test.describe('Accessibility — authenticated pages', () => {
|
||||||
for (const { name, path } of AUTHENTICATED_PAGES) {
|
for (const { name, path } of AUTHENTICATED_PAGES) {
|
||||||
test(`${name} page has no critical wcag2a/wcag2aa violations`, async ({ page }) => {
|
test(`${name} page has no critical wcag2a/wcag2aa violations`, async ({ page }) => {
|
||||||
await page.goto(path);
|
await page.goto(path);
|
||||||
await page.waitForSelector('[data-hydrated]');
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
|
||||||
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze();
|
const results = await buildAxe(page).analyze();
|
||||||
|
|
||||||
if (results.violations.length > 0) {
|
if (results.violations.length > 0) {
|
||||||
const summary = results.violations
|
const summary = results.violations
|
||||||
@@ -44,7 +48,7 @@ test.describe('Accessibility — login page', () => {
|
|||||||
await page.goto('/login');
|
await page.goto('/login');
|
||||||
await expect(page.getByLabel('Benutzername')).toBeVisible();
|
await expect(page.getByLabel('Benutzername')).toBeVisible();
|
||||||
|
|
||||||
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze();
|
const results = await buildAxe(page).analyze();
|
||||||
|
|
||||||
if (results.violations.length > 0) {
|
if (results.violations.length > 0) {
|
||||||
const summary = results.violations
|
const summary = results.violations
|
||||||
|
|||||||
12
frontend/src/routes/+error.svelte
Normal file
12
frontend/src/routes/+error.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Fehler – Familienarchiv</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="px-4 py-12 text-center font-sans">
|
||||||
|
<p class="font-sans text-6xl font-bold text-ink">{page.status}</p>
|
||||||
|
<p class="mt-2 font-sans text-sm text-ink-2">{page.error?.message ?? 'Internal Error'}</p>
|
||||||
|
</div>
|
||||||
@@ -67,6 +67,10 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Archiv</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<main class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
<main class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
||||||
<SearchFilterBar
|
<SearchFilterBar
|
||||||
bind:q={q}
|
bind:q={q}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ $effect(() => {
|
|||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
accept=".pdf,.jpg,.jpeg,.png,.tif,.tiff"
|
accept=".pdf,.jpg,.jpeg,.png,.tif,.tiff"
|
||||||
|
aria-label={m.upload_label()}
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
onchange={handleFileSelect}
|
onchange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ let {
|
|||||||
oninput={onSearch}
|
oninput={onSearch}
|
||||||
onfocus={onfocus}
|
onfocus={onfocus}
|
||||||
onblur={onblur}
|
onblur={onblur}
|
||||||
|
aria-label={m.docs_search_placeholder()}
|
||||||
placeholder={m.docs_search_placeholder()}
|
placeholder={m.docs_search_placeholder()}
|
||||||
class="block w-full border-line py-2.5 pr-10 pl-3 placeholder-ink-3 shadow-sm focus:border-ink focus:ring-ink"
|
class="block w-full border-line py-2.5 pr-10 pl-3 placeholder-ink-3 shadow-sm focus:border-ink focus:ring-ink"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ let { data, form } = $props();
|
|||||||
let activeTab = $state('users');
|
let activeTab = $state('users');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Administration</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
||||||
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h1 class="font-serif text-3xl text-ink">{m.admin_heading()}</h1>
|
<h1 class="font-serif text-3xl text-ink">{m.admin_heading()}</h1>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
|
|||||||
const activeLocale = $derived(getLocale().toUpperCase());
|
const activeLocale = $derived(getLocale().toUpperCase());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Anmelden</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="relative flex min-h-screen flex-col bg-canvas">
|
<div class="relative flex min-h-screen flex-col bg-canvas">
|
||||||
<!-- Language switcher -->
|
<!-- Language switcher -->
|
||||||
<div class="absolute top-4 right-4 flex items-center gap-1">
|
<div class="absolute top-4 right-4 flex items-center gap-1">
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ function handleSearch() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Personen</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||||
<!-- Header Area -->
|
<!-- Header Area -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user