Compare commits

...

5 Commits

Author SHA1 Message Date
Marcel
ab7fe81b2a ci: track act_runner config with Docker socket mount
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m21s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Failing after 3m14s
Documents the NAS runner configuration needed for Testcontainers.
Must be deployed to the runner host alongside the act_runner binary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:01:24 +02:00
Marcel
1333e690dd fix(ci): expose Docker socket env vars for Testcontainers in backend job
DOCKER_HOST makes the socket explicit rather than relying on runner
config propagation; TESTCONTAINERS_RYUK_DISABLED=true avoids Ryuk
watchdog start failures in nested container environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:00:43 +02:00
Marcel
42fda7675a fix(ci): add TZ=Europe/Berlin to frontend test step
date-buckets.spec.ts midnight tests pass timezone-aware dates (+02:00)
which are 22:00 UTC the prior day; setHours(0,0,0,0) uses local TZ.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:00:03 +02:00
Marcel
fb5f47f593 fix(pdf-controls): add focus-visible ring to all PdfControls buttons (WCAG 2.1 §2.4.7)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m56s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
CI / Unit & Component Tests (push) Failing after 3m57s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 3m15s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 15:54:27 +02:00
Marcel
d4abe994a3 fix(a11y): increase all PdfControls buttons to 44×44px touch targets
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m2s
CI / OCR Service Tests (pull_request) Successful in 27s
CI / Backend Unit Tests (pull_request) Failing after 3m11s
Add min-h-[44px] min-w-[44px] to all five PDF viewer buttons (prev,
next, zoom in, zoom out, annotation toggle) and widen icon-only
padding from p-1 to p-2. Adds aria-pressed to the annotation toggle
for correct toggle semantics (WCAG 2.2 §2.5.8 + ARIA 1.2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 15:49:18 +02:00
4 changed files with 130 additions and 5 deletions

View File

@@ -39,6 +39,8 @@ jobs:
- name: Run unit and component tests
run: npm test
working-directory: frontend
env:
TZ: Europe/Berlin
- name: Build frontend
run: npm run build
@@ -78,6 +80,8 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_API_VERSION: "1.43" # NAS runner runs Docker 24.x (max API 1.43); Testcontainers 2.x defaults to 1.44
DOCKER_HOST: unix:///var/run/docker.sock
TESTCONTAINERS_RYUK_DISABLED: "true"
steps:
- uses: actions/checkout@v4

View File

@@ -35,7 +35,7 @@ let {
onclick={onPrev}
disabled={currentPage <= 1}
aria-label="Zurück"
class="rounded p-1 text-ink-3 transition hover:bg-surface/10 disabled:opacity-40"
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1 disabled:opacity-40"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
@@ -52,7 +52,7 @@ let {
onclick={onNext}
disabled={!isLoaded || currentPage >= totalPages}
aria-label="Weiter"
class="rounded p-1 text-ink-3 transition hover:bg-surface/10 disabled:opacity-40"
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1 disabled:opacity-40"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
@@ -65,7 +65,7 @@ let {
<button
onclick={onZoomOut}
aria-label="Verkleinern"
class="rounded p-1 text-ink-3 transition hover:bg-surface/10"
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" />
@@ -75,7 +75,7 @@ let {
<button
onclick={onZoomIn}
aria-label="Vergrößern"
class="rounded p-1 text-ink-3 transition hover:bg-surface/10"
class="min-h-[44px] min-w-[44px] rounded p-2 text-ink-3 transition hover:bg-surface/10 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" />
@@ -89,7 +89,8 @@ let {
<button
onclick={onToggleAnnotations}
aria-label={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()}
class="flex items-center gap-1.5 rounded px-2 py-1 font-sans text-xs transition {showAnnotations
aria-pressed={showAnnotations}
class="flex min-h-[44px] min-w-[44px] items-center gap-1.5 rounded px-3 py-2 font-sans text-xs transition focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-1 {showAnnotations
? 'text-ink-2 hover:bg-surface/10'
: 'bg-surface/10 text-primary'}"
>

View File

@@ -65,3 +65,111 @@ describe('PdfControls — annotation toggle contrast (WCAG 2.1 AA)', () => {
expect(annotationBtn!.className).not.toContain('text-accent');
});
});
describe('PdfControls — focus rings (WCAG 2.1 §2.4.7)', () => {
it('annotation toggle button has focus-visible:ring-2 focus ring', async () => {
const { container } = render(PdfControls, {
...defaultProps,
annotationCount: 2,
showAnnotations: false
});
const allButtons = container.querySelectorAll('button');
const annotationBtn = Array.from(allButtons).find((b) =>
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
);
expect(annotationBtn).not.toBeNull();
expect(annotationBtn!.className).toContain('focus-visible:ring-2');
});
it('icon-only nav/zoom buttons each have focus-visible:ring-2 focus ring', async () => {
const { container } = render(PdfControls, { ...defaultProps });
const allButtons = container.querySelectorAll('button');
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
const label = b.getAttribute('aria-label') ?? '';
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
});
expect(iconOnlyButtons).toHaveLength(4);
for (const btn of iconOnlyButtons) {
expect(btn.className).toContain('focus-visible:ring-2');
}
});
});
describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => {
it('annotation toggle button has min-h-[44px] touch target', async () => {
const { container } = render(PdfControls, {
...defaultProps,
annotationCount: 2,
showAnnotations: false
});
const allButtons = container.querySelectorAll('button');
const annotationBtn = Array.from(allButtons).find((b) =>
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
);
expect(annotationBtn).not.toBeNull();
expect(annotationBtn!.className).toContain('min-h-[44px]');
});
it('annotation toggle button has min-w-[44px] touch target', async () => {
const { container } = render(PdfControls, {
...defaultProps,
annotationCount: 2,
showAnnotations: false
});
const allButtons = container.querySelectorAll('button');
const annotationBtn = Array.from(allButtons).find((b) =>
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
);
expect(annotationBtn).not.toBeNull();
expect(annotationBtn!.className).toContain('min-w-[44px]');
});
it('annotation toggle reflects pressed state via aria-pressed', async () => {
const { container: c1 } = render(PdfControls, {
...defaultProps,
annotationCount: 2,
showAnnotations: false
});
const btn1 = Array.from(c1.querySelectorAll('button')).find((b) =>
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
);
expect(btn1!.getAttribute('aria-pressed')).toBe('false');
cleanup();
const { container: c2 } = render(PdfControls, {
...defaultProps,
annotationCount: 2,
showAnnotations: true
});
const btn2 = Array.from(c2.querySelectorAll('button')).find((b) =>
b.getAttribute('aria-label')?.toLowerCase().includes('annotierungen')
);
expect(btn2!.getAttribute('aria-pressed')).toBe('true');
});
it('icon-only nav/zoom buttons each have min-h-[44px] touch target', async () => {
const { container } = render(PdfControls, { ...defaultProps });
const allButtons = container.querySelectorAll('button');
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
const label = b.getAttribute('aria-label') ?? '';
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
});
expect(iconOnlyButtons).toHaveLength(4);
for (const btn of iconOnlyButtons) {
expect(btn.className).toContain('min-h-[44px]');
}
});
it('icon-only nav/zoom buttons each have min-w-[44px] touch target', async () => {
const { container } = render(PdfControls, { ...defaultProps });
const allButtons = container.querySelectorAll('button');
const iconOnlyButtons = Array.from(allButtons).filter((b) => {
const label = b.getAttribute('aria-label') ?? '';
return ['zurück', 'weiter', 'verkleinern', 'vergrößern'].includes(label.toLowerCase());
});
expect(iconOnlyButtons).toHaveLength(4);
for (const btn of iconOnlyButtons) {
expect(btn.className).toContain('min-w-[44px]');
}
});
});

12
runner-config.yaml Normal file
View File

@@ -0,0 +1,12 @@
# runner-config.yaml — only the relevant section
container:
# passed as DOCKER_HOST inside the job container
docker_host: "unix:///var/run/docker.sock"
# whitelists the socket path so workflows can mount it
valid_volumes:
- "/var/run/docker.sock"
# appended to `docker run` when the runner spawns a job container
options: "-v /var/run/docker.sock:/var/run/docker.sock"
# keep network mode default (bridge) — Testcontainers handles its own networking
force_pull: false