Commit Graph

24 Commits

Author SHA1 Message Date
Marcel
30a8e36563 test(notification): make setNotifications authoritative in bell a11y tests
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m40s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m36s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
CI showed the single/many a11y tests failing with count 0: init()'s async
fetchUnreadCount resolved to {count:0} AFTER setNotifications() ran,
clobbering the seeded count (the flake Sara predicted in review). Stub
fetch to never settle so the announced count is driven solely by
setNotifications — deterministic, no race. Also rewrites the 'error' test
to seed a count then fail the load and assert the count SURVIVES, so it is
a meaningful state distinct from 'empty' (was byte-identical, flagged by
Felix/Sara/Leonie). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:32:00 +02:00
Marcel
cdfcb02903 revert(test): abandon shared-mock dedup — infeasible in vitest browser mode
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m42s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m31s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
CI proved cross-file sharing of a virtual-module mock body cannot work in
@vitest/browser-playwright 4.1.6: the static-import spread fails the hoist
("no top level variables"), and the await-vi.hoisted-import form fails to
parse ("Unexpected identifier 'vi'"). vi.hoisted has the same hoist
constraint as vi.mock, so there is no way to thread an external module's
body into the factory here.

Reverts Phase 1: restores the 4 $app/forms/$app/navigation specs to their
inline factories, inlines NotificationBell.spec's forms stub, deletes the
src/__mocks__/$app/* modules and the $mocks alias (vite, vitest-coverage,
kit). The no-factory-ban meta-test stays (no-factory vi.mock is still
banned). ADR-012 amended to record the infeasibility. Everything else
($app/state migration, confirm context-inject, notification refactor, the
pin, the meta-test) is unaffected. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:21:02 +02:00
Marcel
f13999d006 fix(test): load shared mocks via vi.hoisted, not a static import
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m39s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m37s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
CI caught that vi.mock('$app/forms', () => ({ ...formsMock })) with a
static `import * as formsMock` fails: vitest hoists vi.mock above the
import, so the factory references an uninitialised binding
("no top level variables inside"). Load the shared mock module via
`const formsMock = await vi.hoisted(() => import('$mocks/...'))` instead —
the factory may reference a vi.hoisted binding, and the dynamic import runs
at collection time (not in the lazily-invoked factory), so it stays clear
of ADR-012's birpc race and the no-async-mock-factories guard. Applies to
all 5 shared-mock consumers ($app/forms x4, $app/navigation x1). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:52:37 +02:00
Marcel
140b321868 refactor(notification): provide notification store via context + fixture
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m50s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
Converts the module-singleton notificationStore into a context-provided
store so its specs can drive it without mocking the module. notifications.svelte
now exports createNotificationStore() (the former singleton body), plus
provideNotificationStore()/getNotificationStore()/NOTIFICATION_KEY mirroring
the confirm service. Root +layout provides it; NotificationBell and the
Chronik page read it via getNotificationStore().

Tests:
- notifications.svelte.spec drives a fresh createNotificationStore() per test
  (replacing __resetForTest/__setNavigateForTest with setNavigate()).
- notification.test-fixture.svelte wraps the bell, provides the store, and
  exposes setNotifications(items) via onReady (option b).
- NotificationBell.svelte.spec asserts the announced unread count across the
  empty / single / many / error a11y states (AC#5), stubbing EventSource+fetch.
- aktivitaeten page spec injects a real store via render context.

Per the recorded Phase-2b decision (full context refactor). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:27:19 +02:00
Marcel
8b1eeba91c test: migrate remaining 3 $app/forms consumers to shared mock
Completes Phase 1a after the load-bearing ChronikFuerDichBox spec proved
the pattern. ChronikFuerDichBox.test and NotificationDropdown.test (rich
result-firing interceptors) keep their submit-fired assertions
(optimisticMarkRead/MarkAllRead) and use formsMock.setFormResult for the
failure branch. NotificationBell.spec used the simpler intercept-only
factory and renders no form of its own, so it adopts the shared superset
purely as a render-time stub. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:27:19 +02:00
Marcel
392097287c fix(notification): address review suggestions
- ChronikFuerDichBox: move update() inside the failure branch so success
  path skips it, matching NotificationDropdown's pattern
- NotificationDropdown test: add role=alert assertion for mark-all-read
  failure to match existing dismiss-failure coverage in ChronikFuerDichBox
- +page.server.ts: use getErrorMessage(undefined) instead of null so the
  missing-notificationId 400 goes through the same i18n pipeline as other errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
978a2b3cdb fix(notification-dropdown): handle error result type, add role=alert, fix update ordering
- Add role="alert" to error banner so screen-reader users hear failures
- Handle result.type === 'error' (network failure) alongside 'failure' in both enhance callbacks
- Clear errorMessage at the start of each submit so stale errors don't persist on retry
- On dismiss success: skip update() entirely since goto() navigates away from the page
- On dismiss failure: await update() then set error message
- On mark-all success: skip update() (optimistic state already applied)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
30efb54aac fix(notifications): surface action failures as an error banner
When dismiss-notification or mark-all-read returns a failure the dropdown
now shows a localised error message above the list. Added
notification_error_generic key (de/en/es) as the fallback when the
action response carries no explicit error string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
dbf74cb91a fix(notifications): move onClose/goto into enhance result callback
onClose() and goto() were firing before the server responded, making it
impossible for a fail() response to cancel navigation. Moved them inside
the result callback behind a result.type !== 'failure' guard.

Updated the $app/forms enhance mock to always invoke the returned async
callback with a configurable mockFormResult, and added three tests:
- success path calls onClose + goto with the correct deep-link URL
- failure path skips onClose and goto
- annotationId is appended to the URL when present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
3d3c111c2b refactor(notification): replace callback props with form actions in Dropdown and Bell
NotificationDropdown now wraps each row in a <form action="/aktivitaeten?/dismiss-notification">
and the mark-all control in <form action="/aktivitaeten?/mark-all-read">, wired via use:enhance
for optimistic UI. Props renamed onMarkRead/onMarkAllRead → optimisticMarkRead/optimisticMarkAllRead
to match the simplified store API. NotificationBell passes the store helpers directly; handleMarkRead
is removed.

Test mocks updated: $app/forms enhance mock fires SubmitFunction synchronously on form submit so
callback assertions work without a real HTTP round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
cdd5bfa318 refactor(notification): rename markRead/markAllRead to optimistic helpers without fetch
Removes raw fetch() calls from the store. optimisticMarkRead(id) and
optimisticMarkAllRead() now only mutate local $state — the actual API
calls move to SvelteKit form actions on /aktivitaeten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:35:51 +02:00
Marcel
89860403f6 fix(notification): remove role=link from view-all button — restores semantically honest button role
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m50s
CI / OCR Service Tests (pull_request) Successful in 18s
CI / Backend Unit Tests (pull_request) Successful in 4m12s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Failing after 10s
CI / Unit & Component Tests (push) Failing after 2m5s
CI / OCR Service Tests (push) Successful in 17s
CI / Backend Unit Tests (push) Successful in 4m14s
CI / fail2ban Regex (push) Successful in 39s
CI / Compose Bucket Idempotency (push) Failing after 12s
nightly / deploy-staging (push) Failing after 2m36s
The role=link override on a <button> creates a WCAG 4.1.2 keyboard-contract
mismatch: ARIA role=link tells AT users "press Enter to activate (Space does
nothing)", but the native <button> responds to both Enter and Space. Removes
the override so the element is announced as "button" (accurate).

Test selectors updated from getByRole('link') to getByRole('button')
accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:01:38 +02:00
Marcel
6b78557954 refactor(notification-tests): use vi.mocked instead of type cast in call-order test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:50:55 +02:00
Marcel
bc2dd3a98a fix(notification): add role=link and touch target to view-all button
Some checks failed
CI / Backend Unit Tests (push) Successful in 4m15s
CI / fail2ban Regex (push) Successful in 39s
CI / Compose Bucket Idempotency (push) Failing after 11s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m17s
CI / Unit & Component Tests (push) Failing after 1m48s
CI / OCR Service Tests (push) Successful in 17s
CI / Unit & Component Tests (pull_request) Failing after 2m3s
CI / fail2ban Regex (pull_request) Successful in 40s
CI / Compose Bucket Idempotency (pull_request) Failing after 11s
- role="link" restores screen reader link semantics (Leonie blocker)
- min-h-[44px] px-1 meets WCAG 2.2 §2.5.8 and our 44×48px target size
- Comment in handleViewAll explains close-before-navigate ordering
- Tests updated to getByRole('link') + new call-order assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:43:11 +02:00
Marcel
8ccc9aba1a fix(notification): replace view-all anchor with button to prevent iframe navigation
SvelteKit's capture-phase link interceptor fires before the component's
onclick handler, so e.preventDefault() was structurally too late to stop
iframe navigation in vitest-browser. Replacing the <a href> with a
<button type="button"> removes the href entirely — the interceptor never
fires — and the existing goto() mock in tests is sufficient.

Also splits the single view-all test into two focused it() blocks and
clears mocks in afterEach to prevent cross-test mock leakage.

Fixes #551

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:43:11 +02:00
Marcel
37026bbbb8 refactor(notification): extract handleViewAll named function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:35:40 +02:00
Marcel
53ecfee25e test(notification): assert href is preserved on view-all link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:35:40 +02:00
Marcel
ed91c9bcf6 fix(notification): prevent iframe navigation — use goto instead of href follow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:35:40 +02:00
Marcel
661e8582a2 test(notification): add goto mock and tighten selector in NotificationDropdown spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:35:40 +02:00
Marcel
57f4d12808 test(notification): expand NotificationDropdown coverage
Adds MENTION verb text, REPLY verb/glyph, multiple notifications
rendering.

3 new tests covering ~5 branches.

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
2bce127065 test(notification): cover NotificationDropdown branches
Dialog with bell label, empty state vs populated list, mark-all-read
visibility branch, REPLY vs MENTION text, unread-dot rendering, all
three callback wirings (onMarkRead, onMarkAllRead, onClose).

10 tests covering the notification dropdown surface.

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:50:28 +02:00
Marcel
a1b89670c0 docs(legibility): add 18 per-domain README.md files (DOC-6)
Backend (9): document, person, tag, user, geschichte, notification,
ocr, audit, dashboard.
Frontend (8): document, person, tag, user, geschichte, notification,
ocr, shared.
OCR service (1): ocr-service/README.md.

Each README covers: what the domain owns, explicit non-ownership,
public surface (verified by grep against the codebase), internal
layout, and cross-domain dependencies.

Closes #400
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 07:36:38 +02:00
Marcel
d6db7a07bd refactor: move shared utilities to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:35:15 +02:00
Marcel
051d2f246e refactor: move notification domain to lib/notification/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:22:02 +02:00