test(notification): make setNotifications authoritative in bell a11y tests
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m13s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m37s
CI / fail2ban Regex (push) Successful in 45s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m7s
nightly / deploy-staging (push) Successful in 2m13s
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m13s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m37s
CI / fail2ban Regex (push) Successful in 45s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m7s
nightly / deploy-staging (push) Successful in 2m13s
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>
This commit was merged in pull request #719.
This commit is contained in:
@@ -30,11 +30,12 @@ class NoopEventSource {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('EventSource', NoopEventSource);
|
||||
// init()'s fetchUnreadCount() never settles, so the bell's announced count is
|
||||
// driven solely by setNotifications() — removes the init-fetch-vs-setNotifications
|
||||
// race (the real fetch would resolve to {count:0} and clobber the seeded count).
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Response(JSON.stringify({ count: 0, content: [] }), { status: 200 }))
|
||||
vi.fn(() => new Promise(() => {}))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -89,7 +90,6 @@ describe('NotificationBell — rendering', () => {
|
||||
describe('NotificationBell — announced unread count across a11y states', () => {
|
||||
it('empty: announces no unread count and hides the live badge', async () => {
|
||||
const { setNotifications } = renderBell();
|
||||
await tick();
|
||||
setNotifications([]);
|
||||
await tick();
|
||||
|
||||
@@ -101,7 +101,6 @@ describe('NotificationBell — announced unread count across a11y states', () =>
|
||||
|
||||
it('single: announces one unread notification', async () => {
|
||||
const { setNotifications } = renderBell();
|
||||
await tick();
|
||||
setNotifications([makeNotification({ id: 'n1', read: false })]);
|
||||
await tick();
|
||||
|
||||
@@ -114,7 +113,6 @@ describe('NotificationBell — announced unread count across a11y states', () =>
|
||||
|
||||
it('many: announces the exact unread count', async () => {
|
||||
const { setNotifications } = renderBell();
|
||||
await tick();
|
||||
setNotifications([
|
||||
makeNotification({ id: 'n1', read: false }),
|
||||
makeNotification({ id: 'n2', read: false }),
|
||||
@@ -128,16 +126,21 @@ describe('NotificationBell — announced unread count across a11y states', () =>
|
||||
expect(unreadBadge().textContent?.trim()).toBe('3');
|
||||
});
|
||||
|
||||
it('error: degrades to a zero announced count when the unread-count load fails', async () => {
|
||||
it('error: a failed unread-count load does not wipe the announced count', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')));
|
||||
renderBell();
|
||||
const { setNotifications } = renderBell();
|
||||
setNotifications([
|
||||
makeNotification({ id: 'n1', read: false }),
|
||||
makeNotification({ id: 'n2', read: false })
|
||||
]);
|
||||
await tick();
|
||||
|
||||
// A failed load leaves the count at 0; the bell still announces a valid,
|
||||
// non-urgent state rather than a broken count.
|
||||
// init()'s fetchUnreadCount rejects; its catch must leave the already-set
|
||||
// count intact rather than silently zeroing the live region. This is a
|
||||
// distinct state from "empty" — the count survived a transient load failure.
|
||||
const btn = bellButton();
|
||||
expect(btn.getAttribute('title')).toBe(m.notification_bell_label());
|
||||
expect(btn.getAttribute('title')).toBe(m.notification_bell_unread_label({ count: 2 }));
|
||||
expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title'));
|
||||
expect(unreadBadge().classList.contains('hidden')).toBe(true);
|
||||
expect(unreadBadge().textContent?.trim()).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user