File size: 4,797 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { act } from '@testing-library/react';
import { render, screen, fireEvent } from '../../../tests/helpers/render';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { useSystemNoticeStore } from '../../store/systemNoticeStore';
import { BannerRenderer } from './SystemNoticeBanner';
import type { SystemNoticeDTO } from '../../store/systemNoticeStore';

function makeBanner(overrides: Partial<SystemNoticeDTO> = {}): SystemNoticeDTO {
  return {
    id: 'banner-1',
    display: 'banner',
    severity: 'info',
    titleKey: 'Maintenance notice',
    bodyKey: 'System will be down briefly.',
    dismissible: true,
    ...overrides,
  };
}

describe('BannerRenderer', () => {
  beforeEach(() => {
    server.use(
      http.post('/api/system-notices/:id/dismiss', () => {
        return new HttpResponse(null, { status: 204 });
      }),
    );
    useSystemNoticeStore.setState({ notices: [], loaded: true });
  });

  afterEach(() => {
    vi.clearAllMocks();
    document.documentElement.style.removeProperty('--banner-stack-h');
  });

  it('FE-SN-BANNER-001: renders banner with correct title and body', async () => {
    const notice = makeBanner();
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    expect(screen.getByText('Maintenance notice')).toBeTruthy();
    expect(screen.getByText('System will be down briefly.')).toBeTruthy();
  });

  it('FE-SN-BANNER-002: dismiss button calls store.dismiss(id)', async () => {
    const notice = makeBanner();
    useSystemNoticeStore.setState({ notices: [notice], loaded: true });

    const dismissSpy = vi.spyOn(useSystemNoticeStore.getState(), 'dismiss');
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    const dismissBtn = screen.getByLabelText(/Dismiss/);
    await act(async () => {
      fireEvent.click(dismissBtn);
    });

    expect(dismissSpy).toHaveBeenCalledWith('banner-1');
  });

  it('FE-SN-BANNER-003: two banners stack correctly', async () => {
    const n1 = makeBanner({ id: 'banner-1', titleKey: 'First notice' });
    const n2 = makeBanner({ id: 'banner-2', titleKey: 'Second notice' });
    await act(async () => {
      render(<BannerRenderer notices={[n1, n2]} />);
    });

    expect(screen.getByText('First notice')).toBeTruthy();
    expect(screen.getByText('Second notice')).toBeTruthy();
  });

  it('FE-SN-BANNER-004: third banner is not rendered (only top 2 shown)', async () => {
    // Server returns notices highest-priority first; BannerRenderer takes slice(0,2)
    const n1 = makeBanner({ id: 'banner-1', titleKey: 'Highest notice' });
    const n2 = makeBanner({ id: 'banner-2', titleKey: 'Second notice' });
    const n3 = makeBanner({ id: 'banner-3', titleKey: 'Lowest notice' });
    await act(async () => {
      render(<BannerRenderer notices={[n1, n2, n3]} />);
    });

    expect(screen.getByText('Highest notice')).toBeTruthy();
    expect(screen.getByText('Second notice')).toBeTruthy();
    expect(screen.queryByText('Lowest notice')).toBeNull();
  });

  it('FE-SN-BANNER-005: critical banner has aria-live="assertive"', async () => {
    const notice = makeBanner({ severity: 'critical', id: 'crit-1' });
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    const alertEl = screen.getByRole('alert');
    expect(alertEl.getAttribute('aria-live')).toBe('assertive');
  });

  it('FE-SN-BANNER-006: info banner has aria-live="polite"', async () => {
    const notice = makeBanner({ severity: 'info' });
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    const statusEl = screen.getByRole('status');
    expect(statusEl.getAttribute('aria-live')).toBe('polite');
  });

  it('FE-SN-BANNER-007: warn banner has aria-live="polite"', async () => {
    const notice = makeBanner({ severity: 'warn', id: 'warn-1' });
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    const statusEl = screen.getByRole('status');
    expect(statusEl.getAttribute('aria-live')).toBe('polite');
  });

  it('FE-SN-BANNER-008: renders nothing when notices array is empty', () => {
    const { container } = render(<BannerRenderer notices={[]} />);
    expect(container.firstChild).toBeNull();
  });

  it('FE-SN-BANNER-009: non-dismissible banner hides dismiss button', async () => {
    const notice = makeBanner({ dismissible: false });
    await act(async () => {
      render(<BannerRenderer notices={[notice]} />);
    });

    expect(screen.getByText('Maintenance notice')).toBeTruthy();
    expect(screen.queryByLabelText(/Dismiss/)).toBeNull();
  });
});