File size: 6,671 Bytes
766d85d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// FE-ADMIN-DEVNOTIF-001 to FE-ADMIN-DEVNOTIF-010
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { buildUser } from '../../../tests/helpers/factories';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { useAuthStore } from '../../store/authStore';
import { ToastContainer } from '../shared/Toast';
import DevNotificationsPanel from './DevNotificationsPanel';

const ADMIN_USER = buildUser({ id: 1, username: 'testadmin', role: 'admin' });

beforeEach(() => {
  resetAllStores();
  seedStore(useAuthStore, { user: ADMIN_USER, isAuthenticated: true });
});

afterEach(() => {
  server.resetHandlers();
});

describe('DevNotificationsPanel', () => {
  it('FE-ADMIN-DEVNOTIF-001: "DEV ONLY" badge is always visible', () => {
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    expect(screen.getByText('DEV ONLY')).toBeInTheDocument();
  });

  it('FE-ADMIN-DEVNOTIF-002: four section titles render after data loads', async () => {
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    // Wait for async data to populate conditional sections
    await screen.findByText('Trip-Scoped Events');
    await screen.findByText('User-Scoped Events');
    expect(screen.getByText('Type Testing')).toBeInTheDocument();
    expect(screen.getByText('Admin-Scoped Events')).toBeInTheDocument();
  });

  it('FE-ADMIN-DEVNOTIF-003: trip selector populated from API', async () => {
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Trip-Scoped Events');
    const [tripSelect] = screen.getAllByRole('combobox');
    const options = Array.from(tripSelect.querySelectorAll('option'));
    const labels = options.map(o => o.textContent);
    expect(labels).toContain('Paris Adventure');
    expect(labels).toContain('Tokyo Trip');
  });

  it('FE-ADMIN-DEVNOTIF-004: user selector populated from API', async () => {
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('User-Scoped Events');
    const selects = screen.getAllByRole('combobox');
    // Second combobox is the user selector (first is trip selector)
    const userSelect = selects[1];
    const options = Array.from(userSelect.querySelectorAll('option'));
    const labels = options.map(o => o.textContent ?? '');
    expect(labels.some(l => l.includes('admin'))).toBe(true);
    expect(labels.some(l => l.includes('alice'))).toBe(true);
  });

  it('FE-ADMIN-DEVNOTIF-005: clicking "Simple β†’ Me" fires sendTestNotification with correct payload', async () => {
    let capturedBody: Record<string, unknown> | undefined;
    server.use(
      http.post('/api/admin/dev/test-notification', async ({ request }) => {
        capturedBody = await request.json() as Record<string, unknown>;
        return HttpResponse.json({ ok: true });
      }),
    );
    const user = userEvent.setup();
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Type Testing');
    await user.click(screen.getByText('Simple β†’ Me').closest('button')!);
    await waitFor(() => expect(capturedBody).toBeDefined());
    expect(capturedBody).toMatchObject({
      event: 'test_simple',
      scope: 'user',
      targetId: ADMIN_USER.id,
    });
  });

  it('FE-ADMIN-DEVNOTIF-006: success toast shown after fire', async () => {
    server.use(
      http.post('/api/admin/dev/test-notification', () =>
        HttpResponse.json({ ok: true }),
      ),
    );
    const user = userEvent.setup();
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Type Testing');
    await user.click(screen.getByText('Simple β†’ Me').closest('button')!);
    await screen.findByText('Sent: simple-me');
  });

  it('FE-ADMIN-DEVNOTIF-007: all buttons disabled while a send is in-flight', async () => {
    server.use(
      http.post('/api/admin/dev/test-notification', async () => {
        await new Promise(() => {}); // never resolves β€” simulates in-flight
        return HttpResponse.json({ ok: true });
      }),
    );
    const user = userEvent.setup();
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Type Testing');

    // Fire the click but do not await β€” handler never resolves so sending stays true
    void user.click(screen.getByText('Simple β†’ Me').closest('button')!);

    await waitFor(() => {
      const buttons = screen.getAllByRole('button');
      buttons.forEach(btn => expect(btn).toBeDisabled());
    });
  });

  it('FE-ADMIN-DEVNOTIF-008: error toast shown on API failure', async () => {
    server.use(
      http.post('/api/admin/dev/test-notification', () =>
        HttpResponse.json({ message: 'Server error' }, { status: 500 }),
      ),
    );
    const user = userEvent.setup();
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Type Testing');
    await user.click(screen.getByText('Simple β†’ Me').closest('button')!);
    await screen.findByText(/failed|error/i);
  });

  it('FE-ADMIN-DEVNOTIF-009: changing trip selector updates payload targetId', async () => {
    let capturedBody: Record<string, unknown> | undefined;
    server.use(
      http.post('/api/admin/dev/test-notification', async ({ request }) => {
        capturedBody = await request.json() as Record<string, unknown>;
        return HttpResponse.json({ ok: true });
      }),
    );
    const user = userEvent.setup();
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    await screen.findByText('Trip-Scoped Events');

    const [tripSelect] = screen.getAllByRole('combobox');
    const tokyoOption = Array.from(tripSelect.querySelectorAll('option')).find(
      o => o.textContent === 'Tokyo Trip',
    )!;
    const tokyoId = Number(tokyoOption.value);

    await user.selectOptions(tripSelect, 'Tokyo Trip');
    await user.click(screen.getByText('booking_change').closest('button')!);

    await waitFor(() => expect(capturedBody).toBeDefined());
    expect(capturedBody!.targetId).toBe(tokyoId);
  });

  it('FE-ADMIN-DEVNOTIF-010: Trip-Scoped section absent when no trips', async () => {
    server.use(
      http.get('/api/trips', () => HttpResponse.json({ trips: [] })),
    );
    render(<><ToastContainer /><DevNotificationsPanel /></>);
    // Wait for user data to confirm async effects have settled
    await screen.findByText('User-Scoped Events');
    expect(screen.queryByText('Trip-Scoped Events')).not.toBeInTheDocument();
  });
});