fix(a11y): bump progress bar text to text-xs minimum, add motion-safe to upload animation
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m39s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 2m46s
CI / Unit & Component Tests (pull_request) Failing after 2m37s
CI / OCR Service Tests (pull_request) Successful in 36s
CI / Backend Unit Tests (pull_request) Failing after 2m50s

- text-[9px]/text-[10px] in required-fields bar raised to text-xs (12px),
  meeting the project minimum for the 60+ audience (WCAG 1.4.4)
- Upload animation now uses motion-safe: prefix so it stops for users
  with prefers-reduced-motion set (WCAG 2.1 SC 2.3.3)
- Strengthened UploadZone tests: onCancel uses [role=status] button
  selector instead of first-button heuristic; added positive file
  selection test (valid PDF calls onFile), file-too-large test, and
  MIME rejection now also asserts the error message is visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 14:31:05 +02:00
parent 270005e0da
commit 0e1f076727
3 changed files with 36 additions and 4 deletions

View File

@@ -55,7 +55,9 @@ function handleDrop(e: DragEvent) {
{#if isUploading}
<div role="status" class="flex flex-col items-center gap-3 text-center">
<div class="h-0.5 w-48 overflow-hidden rounded-full bg-white/10">
<div class="h-full animate-[slide_1.4s_ease-in-out_infinite] bg-brand-mint/70"></div>
<div
class="h-full bg-brand-mint/70 motion-safe:animate-[slide_1.4s_ease-in-out_infinite]"
></div>
</div>
<p class="max-w-[200px] truncate text-xs font-medium text-brand-mint/70">{filename}</p>
<p class="text-xs text-white/40">Wird hochgeladen …</p>

View File

@@ -47,7 +47,8 @@ describe('UploadZone', () => {
render(UploadZone, {
props: { filename: 'scan.pdf', isUploading: true, isDragging: false, error: null, onCancel }
});
const btn = document.querySelector('button')!;
// Click the button inside [role="status"] — more specific than querySelector('button')
const btn = document.querySelector('[role="status"] button') as HTMLButtonElement;
btn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(onCancel).toHaveBeenCalledOnce();
});
@@ -68,6 +69,18 @@ describe('UploadZone', () => {
});
describe('file selection', () => {
it('calls onFile for a valid PDF', () => {
const onFile = vi.fn();
render(UploadZone, {
props: { filename: 'scan.pdf', isUploading: false, isDragging: false, error: null, onFile }
});
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
const pdf = new File(['%PDF-1.4'], 'brief.pdf', { type: 'application/pdf' });
Object.defineProperty(input, 'files', { value: [pdf], writable: false });
input.dispatchEvent(new Event('change', { bubbles: true }));
expect(onFile).toHaveBeenCalledWith(pdf);
});
it('does not call onFile for an unsupported MIME type', async () => {
const onFile = vi.fn();
render(UploadZone, {
@@ -80,6 +93,23 @@ describe('UploadZone', () => {
Object.defineProperty(input, 'files', { value: [docxFile], writable: false });
input.dispatchEvent(new Event('change', { bubbles: true }));
expect(onFile).not.toHaveBeenCalled();
await expect
.element(page.getByText('Dieser Dateityp wird nicht unterstützt (PDF, JPG, PNG, TIFF).'))
.toBeVisible();
});
it('does not call onFile when file exceeds 50 MB', async () => {
const onFile = vi.fn();
render(UploadZone, {
props: { filename: 'scan.pdf', isUploading: false, isDragging: false, error: null, onFile }
});
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
const bigFile = new File(['x'.repeat(1)], 'huge.pdf', { type: 'application/pdf' });
Object.defineProperty(bigFile, 'size', { value: 51 * 1024 * 1024 });
Object.defineProperty(input, 'files', { value: [bigFile], writable: false });
input.dispatchEvent(new Event('change', { bubbles: true }));
expect(onFile).not.toHaveBeenCalled();
await expect.element(page.getByText('Die Datei ist zu groß (max. 50 MB).')).toBeVisible();
});
});
});

View File

@@ -120,7 +120,7 @@ async function handleReplaceFile(e: Event) {
<!-- Required-fields progress bar -->
<div class="flex items-center gap-3 border-b border-line bg-surface px-6 py-1.5">
<span class="text-[9px] font-bold tracking-widest text-ink-3 uppercase">Pflichtfelder</span>
<span class="text-xs font-bold tracking-widest text-ink-3 uppercase">Pflichtfelder</span>
<div
class="h-0.5 flex-1 rounded-full bg-line"
role="progressbar"
@@ -134,7 +134,7 @@ async function handleReplaceFile(e: Event) {
style="width:{requiredPct}%"
></div>
</div>
<span class="text-[10px] font-bold text-brand-navy">{requiredFilled} / 3</span>
<span class="text-xs font-bold text-brand-navy">{requiredFilled} / 3</span>
</div>
<!-- Main content -->