datajunction-ui 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NotificationBell.tsx +223 -0
  3. package/src/app/components/UserMenu.tsx +100 -0
  4. package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
  5. package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
  6. package/src/app/icons/NotificationIcon.jsx +27 -0
  7. package/src/app/icons/SettingsIcon.jsx +28 -0
  8. package/src/app/index.tsx +12 -0
  9. package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
  10. package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
  11. package/src/app/pages/NotificationsPage/index.jsx +136 -0
  12. package/src/app/pages/Root/__tests__/index.test.jsx +18 -53
  13. package/src/app/pages/Root/index.tsx +23 -19
  14. package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
  15. package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
  16. package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
  17. package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
  18. package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
  19. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
  20. package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
  21. package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
  22. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
  23. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
  24. package/src/app/pages/SettingsPage/index.jsx +148 -0
  25. package/src/app/services/DJService.js +81 -0
  26. package/src/app/utils/__tests__/date.test.js +198 -0
  27. package/src/app/utils/date.js +65 -0
  28. package/src/styles/index.css +1 -1
  29. package/src/styles/nav-bar.css +274 -0
  30. package/src/styles/settings.css +787 -0
@@ -0,0 +1,95 @@
1
+ import React, { useState } from 'react';
2
+ import CreateServiceAccountModal from './CreateServiceAccountModal';
3
+
4
+ /**
5
+ * Displays and manages service accounts.
6
+ */
7
+ export function ServiceAccountsSection({ accounts, onCreate, onDelete }) {
8
+ const [showModal, setShowModal] = useState(false);
9
+
10
+ const handleDelete = async account => {
11
+ const confirmed = window.confirm(
12
+ `Delete service account "${account.name}"?\n\nThis will revoke all access for this account and cannot be undone.`,
13
+ );
14
+ if (!confirmed) return;
15
+
16
+ try {
17
+ await onDelete(account.client_id);
18
+ } catch (error) {
19
+ console.error('Error deleting service account:', error);
20
+ alert('Failed to delete service account');
21
+ }
22
+ };
23
+
24
+ const handleCreate = async name => {
25
+ const result = await onCreate(name);
26
+ return result;
27
+ };
28
+
29
+ return (
30
+ <section className="settings-section" id="service-accounts">
31
+ <div className="section-title-row">
32
+ <h2 className="settings-section-title">Service Accounts</h2>
33
+ <button className="btn-create" onClick={() => setShowModal(true)}>
34
+ + Create
35
+ </button>
36
+ </div>
37
+ <div className="settings-card">
38
+ <p className="section-description">
39
+ Service accounts allow programmatic access to the DJ API. Create
40
+ accounts for your applications, scripts, or CI/CD pipelines.
41
+ </p>
42
+
43
+ {accounts.length > 0 ? (
44
+ <div className="service-accounts-list">
45
+ <table className="service-accounts-table">
46
+ <thead>
47
+ <tr>
48
+ <th>Name</th>
49
+ <th>Client ID</th>
50
+ <th>Created</th>
51
+ <th></th>
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ {accounts.map(account => (
56
+ <tr key={account.id}>
57
+ <td>{account.name}</td>
58
+ <td>
59
+ <code className="client-id">{account.client_id}</code>
60
+ </td>
61
+ <td className="created-date">
62
+ {new Date(account.created_at).toLocaleDateString()}
63
+ </td>
64
+ <td className="actions-cell">
65
+ <button
66
+ className="btn-icon btn-delete-account"
67
+ onClick={() => handleDelete(account)}
68
+ title="Delete service account"
69
+ >
70
+ ×
71
+ </button>
72
+ </td>
73
+ </tr>
74
+ ))}
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ ) : (
79
+ <p className="empty-state">
80
+ No service accounts yet. Create one to enable programmatic API
81
+ access.
82
+ </p>
83
+ )}
84
+ </div>
85
+
86
+ <CreateServiceAccountModal
87
+ isOpen={showModal}
88
+ onClose={() => setShowModal(false)}
89
+ onCreate={handleCreate}
90
+ />
91
+ </section>
92
+ );
93
+ }
94
+
95
+ export default ServiceAccountsSection;
@@ -0,0 +1,318 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { CreateServiceAccountModal } from '../CreateServiceAccountModal';
4
+
5
+ describe('CreateServiceAccountModal', () => {
6
+ const mockOnClose = jest.fn();
7
+ const mockOnCreate = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ it('does not render when isOpen is false', () => {
14
+ render(
15
+ <CreateServiceAccountModal
16
+ isOpen={false}
17
+ onClose={mockOnClose}
18
+ onCreate={mockOnCreate}
19
+ />,
20
+ );
21
+
22
+ expect(
23
+ screen.queryByText('Create Service Account'),
24
+ ).not.toBeInTheDocument();
25
+ });
26
+
27
+ it('renders modal when isOpen is true', () => {
28
+ render(
29
+ <CreateServiceAccountModal
30
+ isOpen={true}
31
+ onClose={mockOnClose}
32
+ onCreate={mockOnCreate}
33
+ />,
34
+ );
35
+
36
+ expect(screen.getByText('Create Service Account')).toBeInTheDocument();
37
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
38
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
39
+ expect(screen.getByText('Create')).toBeInTheDocument();
40
+ });
41
+
42
+ it('calls onClose when close button is clicked', () => {
43
+ render(
44
+ <CreateServiceAccountModal
45
+ isOpen={true}
46
+ onClose={mockOnClose}
47
+ onCreate={mockOnCreate}
48
+ />,
49
+ );
50
+
51
+ fireEvent.click(screen.getByTitle('Close'));
52
+
53
+ expect(mockOnClose).toHaveBeenCalled();
54
+ });
55
+
56
+ it('calls onClose when Cancel button is clicked', () => {
57
+ render(
58
+ <CreateServiceAccountModal
59
+ isOpen={true}
60
+ onClose={mockOnClose}
61
+ onCreate={mockOnCreate}
62
+ />,
63
+ );
64
+
65
+ fireEvent.click(screen.getByText('Cancel'));
66
+
67
+ expect(mockOnClose).toHaveBeenCalled();
68
+ });
69
+
70
+ it('calls onClose when overlay is clicked', () => {
71
+ render(
72
+ <CreateServiceAccountModal
73
+ isOpen={true}
74
+ onClose={mockOnClose}
75
+ onCreate={mockOnCreate}
76
+ />,
77
+ );
78
+
79
+ // Click on overlay (modal-overlay class)
80
+ const overlay = document.querySelector('.modal-overlay');
81
+ fireEvent.click(overlay);
82
+
83
+ expect(mockOnClose).toHaveBeenCalled();
84
+ });
85
+
86
+ it('does not close when modal content is clicked', () => {
87
+ render(
88
+ <CreateServiceAccountModal
89
+ isOpen={true}
90
+ onClose={mockOnClose}
91
+ onCreate={mockOnCreate}
92
+ />,
93
+ );
94
+
95
+ const content = document.querySelector('.modal-content');
96
+ fireEvent.click(content);
97
+
98
+ expect(mockOnClose).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('disables Create button when name is empty', () => {
102
+ render(
103
+ <CreateServiceAccountModal
104
+ isOpen={true}
105
+ onClose={mockOnClose}
106
+ onCreate={mockOnCreate}
107
+ />,
108
+ );
109
+
110
+ const createButton = screen.getByText('Create');
111
+ expect(createButton).toBeDisabled();
112
+ });
113
+
114
+ it('enables Create button when name is entered', () => {
115
+ render(
116
+ <CreateServiceAccountModal
117
+ isOpen={true}
118
+ onClose={mockOnClose}
119
+ onCreate={mockOnCreate}
120
+ />,
121
+ );
122
+
123
+ const input = screen.getByLabelText('Name');
124
+ fireEvent.change(input, { target: { value: 'my-new-account' } });
125
+
126
+ const createButton = screen.getByText('Create');
127
+ expect(createButton).not.toBeDisabled();
128
+ });
129
+
130
+ it('calls onCreate with trimmed name on submit', async () => {
131
+ mockOnCreate.mockResolvedValue({ client_id: 'test-id' });
132
+
133
+ render(
134
+ <CreateServiceAccountModal
135
+ isOpen={true}
136
+ onClose={mockOnClose}
137
+ onCreate={mockOnCreate}
138
+ />,
139
+ );
140
+
141
+ const input = screen.getByLabelText('Name');
142
+ fireEvent.change(input, { target: { value: ' my-account ' } });
143
+ fireEvent.click(screen.getByText('Create'));
144
+
145
+ await waitFor(() => {
146
+ expect(mockOnCreate).toHaveBeenCalledWith('my-account');
147
+ });
148
+ });
149
+
150
+ it('shows credentials after successful creation', async () => {
151
+ const credentials = {
152
+ name: 'my-account',
153
+ client_id: 'abc-123',
154
+ client_secret: 'secret-xyz',
155
+ };
156
+ mockOnCreate.mockResolvedValue(credentials);
157
+
158
+ render(
159
+ <CreateServiceAccountModal
160
+ isOpen={true}
161
+ onClose={mockOnClose}
162
+ onCreate={mockOnCreate}
163
+ />,
164
+ );
165
+
166
+ const input = screen.getByLabelText('Name');
167
+ fireEvent.change(input, { target: { value: 'my-account' } });
168
+ fireEvent.click(screen.getByText('Create'));
169
+
170
+ await waitFor(() => {
171
+ expect(screen.getByText('Service Account Created!')).toBeInTheDocument();
172
+ });
173
+
174
+ expect(screen.getByText('abc-123')).toBeInTheDocument();
175
+ expect(screen.getByText('secret-xyz')).toBeInTheDocument();
176
+ expect(
177
+ screen.getByText(/client secret will not be shown again/i),
178
+ ).toBeInTheDocument();
179
+ expect(screen.getByText('Done')).toBeInTheDocument();
180
+ });
181
+
182
+ it('shows alert when creation returns error message', async () => {
183
+ window.alert = jest.fn();
184
+ mockOnCreate.mockResolvedValue({ message: 'Account already exists' });
185
+
186
+ render(
187
+ <CreateServiceAccountModal
188
+ isOpen={true}
189
+ onClose={mockOnClose}
190
+ onCreate={mockOnCreate}
191
+ />,
192
+ );
193
+
194
+ const input = screen.getByLabelText('Name');
195
+ fireEvent.change(input, { target: { value: 'my-account' } });
196
+ fireEvent.click(screen.getByText('Create'));
197
+
198
+ await waitFor(() => {
199
+ expect(window.alert).toHaveBeenCalledWith('Account already exists');
200
+ });
201
+ });
202
+
203
+ it('shows alert when creation throws error', async () => {
204
+ window.alert = jest.fn();
205
+ mockOnCreate.mockRejectedValue(new Error('Network error'));
206
+
207
+ render(
208
+ <CreateServiceAccountModal
209
+ isOpen={true}
210
+ onClose={mockOnClose}
211
+ onCreate={mockOnCreate}
212
+ />,
213
+ );
214
+
215
+ const input = screen.getByLabelText('Name');
216
+ fireEvent.change(input, { target: { value: 'my-account' } });
217
+ fireEvent.click(screen.getByText('Create'));
218
+
219
+ await waitFor(() => {
220
+ expect(window.alert).toHaveBeenCalledWith(
221
+ 'Failed to create service account',
222
+ );
223
+ });
224
+ });
225
+
226
+ it('shows Creating... while request is in progress', async () => {
227
+ let resolveCreate;
228
+ mockOnCreate.mockImplementation(
229
+ () => new Promise(resolve => (resolveCreate = resolve)),
230
+ );
231
+
232
+ render(
233
+ <CreateServiceAccountModal
234
+ isOpen={true}
235
+ onClose={mockOnClose}
236
+ onCreate={mockOnCreate}
237
+ />,
238
+ );
239
+
240
+ const input = screen.getByLabelText('Name');
241
+ fireEvent.change(input, { target: { value: 'my-account' } });
242
+ fireEvent.click(screen.getByText('Create'));
243
+
244
+ expect(screen.getByText('Creating...')).toBeInTheDocument();
245
+
246
+ // Resolve the promise
247
+ resolveCreate({ client_id: 'test' });
248
+
249
+ await waitFor(() => {
250
+ expect(screen.queryByText('Creating...')).not.toBeInTheDocument();
251
+ });
252
+ });
253
+
254
+ it('closes and resets state when Done is clicked after creation', async () => {
255
+ const credentials = {
256
+ name: 'my-account',
257
+ client_id: 'abc-123',
258
+ client_secret: 'secret-xyz',
259
+ };
260
+ mockOnCreate.mockResolvedValue(credentials);
261
+
262
+ render(
263
+ <CreateServiceAccountModal
264
+ isOpen={true}
265
+ onClose={mockOnClose}
266
+ onCreate={mockOnCreate}
267
+ />,
268
+ );
269
+
270
+ const input = screen.getByLabelText('Name');
271
+ fireEvent.change(input, { target: { value: 'my-account' } });
272
+ fireEvent.click(screen.getByText('Create'));
273
+
274
+ await waitFor(() => {
275
+ expect(screen.getByText('Done')).toBeInTheDocument();
276
+ });
277
+
278
+ fireEvent.click(screen.getByText('Done'));
279
+
280
+ expect(mockOnClose).toHaveBeenCalled();
281
+ });
282
+
283
+ it('copies client ID to clipboard when copy button is clicked', async () => {
284
+ const credentials = {
285
+ name: 'my-account',
286
+ client_id: 'abc-123',
287
+ client_secret: 'secret-xyz',
288
+ };
289
+ mockOnCreate.mockResolvedValue(credentials);
290
+
291
+ Object.assign(navigator, {
292
+ clipboard: {
293
+ writeText: jest.fn().mockResolvedValue(),
294
+ },
295
+ });
296
+
297
+ render(
298
+ <CreateServiceAccountModal
299
+ isOpen={true}
300
+ onClose={mockOnClose}
301
+ onCreate={mockOnCreate}
302
+ />,
303
+ );
304
+
305
+ const input = screen.getByLabelText('Name');
306
+ fireEvent.change(input, { target: { value: 'my-account' } });
307
+ fireEvent.click(screen.getByText('Create'));
308
+
309
+ await waitFor(() => {
310
+ expect(screen.getByText('abc-123')).toBeInTheDocument();
311
+ });
312
+
313
+ const copyButtons = screen.getAllByTitle('Copy');
314
+ fireEvent.click(copyButtons[0]);
315
+
316
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('abc-123');
317
+ });
318
+ });
@@ -0,0 +1,233 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { NotificationSubscriptionsSection } from '../NotificationSubscriptionsSection';
4
+
5
+ describe('NotificationSubscriptionsSection', () => {
6
+ const mockOnUpdate = jest.fn();
7
+ const mockOnUnsubscribe = jest.fn();
8
+
9
+ const mockSubscriptions = [
10
+ {
11
+ entity_name: 'default.orders_count',
12
+ entity_type: 'node',
13
+ node_type: 'metric',
14
+ activity_types: ['update', 'delete'],
15
+ status: 'valid',
16
+ },
17
+ {
18
+ entity_name: 'default.dim_customers',
19
+ entity_type: 'node',
20
+ node_type: 'dimension',
21
+ activity_types: ['create'],
22
+ status: 'invalid',
23
+ },
24
+ ];
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ it('renders empty state when no subscriptions', () => {
31
+ render(
32
+ <NotificationSubscriptionsSection
33
+ subscriptions={[]}
34
+ onUpdate={mockOnUpdate}
35
+ onUnsubscribe={mockOnUnsubscribe}
36
+ />,
37
+ );
38
+
39
+ expect(screen.getByText(/not watching any nodes/i)).toBeInTheDocument();
40
+ });
41
+
42
+ it('renders subscriptions list', () => {
43
+ render(
44
+ <NotificationSubscriptionsSection
45
+ subscriptions={mockSubscriptions}
46
+ onUpdate={mockOnUpdate}
47
+ onUnsubscribe={mockOnUnsubscribe}
48
+ />,
49
+ );
50
+
51
+ expect(screen.getByText('default.orders_count')).toBeInTheDocument();
52
+ expect(screen.getByText('default.dim_customers')).toBeInTheDocument();
53
+ expect(screen.getByText('METRIC')).toBeInTheDocument();
54
+ expect(screen.getByText('DIMENSION')).toBeInTheDocument();
55
+ });
56
+
57
+ it('shows invalid badge for invalid status', () => {
58
+ render(
59
+ <NotificationSubscriptionsSection
60
+ subscriptions={mockSubscriptions}
61
+ onUpdate={mockOnUpdate}
62
+ onUnsubscribe={mockOnUnsubscribe}
63
+ />,
64
+ );
65
+
66
+ expect(screen.getByText('INVALID')).toBeInTheDocument();
67
+ });
68
+
69
+ it('displays activity types as badges', () => {
70
+ render(
71
+ <NotificationSubscriptionsSection
72
+ subscriptions={mockSubscriptions}
73
+ onUpdate={mockOnUpdate}
74
+ onUnsubscribe={mockOnUnsubscribe}
75
+ />,
76
+ );
77
+
78
+ expect(screen.getByText('update')).toBeInTheDocument();
79
+ expect(screen.getByText('delete')).toBeInTheDocument();
80
+ expect(screen.getByText('create')).toBeInTheDocument();
81
+ });
82
+
83
+ it('enters edit mode when edit button is clicked', () => {
84
+ render(
85
+ <NotificationSubscriptionsSection
86
+ subscriptions={mockSubscriptions}
87
+ onUpdate={mockOnUpdate}
88
+ onUnsubscribe={mockOnUnsubscribe}
89
+ />,
90
+ );
91
+
92
+ const editButtons = screen.getAllByTitle('Edit subscription');
93
+ fireEvent.click(editButtons[0]);
94
+
95
+ expect(screen.getByText('Save')).toBeInTheDocument();
96
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
97
+ expect(screen.getByLabelText('Update')).toBeInTheDocument();
98
+ });
99
+
100
+ it('cancels editing when cancel button is clicked', () => {
101
+ render(
102
+ <NotificationSubscriptionsSection
103
+ subscriptions={mockSubscriptions}
104
+ onUpdate={mockOnUpdate}
105
+ onUnsubscribe={mockOnUnsubscribe}
106
+ />,
107
+ );
108
+
109
+ const editButtons = screen.getAllByTitle('Edit subscription');
110
+ fireEvent.click(editButtons[0]);
111
+
112
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
113
+
114
+ fireEvent.click(screen.getByText('Cancel'));
115
+
116
+ expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
117
+ });
118
+
119
+ it('calls onUnsubscribe when unsubscribe is confirmed', async () => {
120
+ window.confirm = jest.fn().mockReturnValue(true);
121
+ mockOnUnsubscribe.mockResolvedValue();
122
+
123
+ render(
124
+ <NotificationSubscriptionsSection
125
+ subscriptions={mockSubscriptions}
126
+ onUpdate={mockOnUpdate}
127
+ onUnsubscribe={mockOnUnsubscribe}
128
+ />,
129
+ );
130
+
131
+ const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
132
+ fireEvent.click(unsubscribeButtons[0]);
133
+
134
+ expect(window.confirm).toHaveBeenCalledWith(
135
+ 'Unsubscribe from notifications for "default.orders_count"?',
136
+ );
137
+ expect(mockOnUnsubscribe).toHaveBeenCalledWith(mockSubscriptions[0]);
138
+ });
139
+
140
+ it('does not call onUnsubscribe when unsubscribe is cancelled', () => {
141
+ window.confirm = jest.fn().mockReturnValue(false);
142
+
143
+ render(
144
+ <NotificationSubscriptionsSection
145
+ subscriptions={mockSubscriptions}
146
+ onUpdate={mockOnUpdate}
147
+ onUnsubscribe={mockOnUnsubscribe}
148
+ />,
149
+ );
150
+
151
+ const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
152
+ fireEvent.click(unsubscribeButtons[0]);
153
+
154
+ expect(mockOnUnsubscribe).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('shows alert when trying to save with no activity types', () => {
158
+ window.alert = jest.fn();
159
+
160
+ render(
161
+ <NotificationSubscriptionsSection
162
+ subscriptions={mockSubscriptions}
163
+ onUpdate={mockOnUpdate}
164
+ onUnsubscribe={mockOnUnsubscribe}
165
+ />,
166
+ );
167
+
168
+ const editButtons = screen.getAllByTitle('Edit subscription');
169
+ fireEvent.click(editButtons[0]);
170
+
171
+ // Uncheck all activity types
172
+ const updateCheckbox = screen.getByLabelText('Update');
173
+ const deleteCheckbox = screen.getByLabelText('Delete');
174
+ fireEvent.click(updateCheckbox);
175
+ fireEvent.click(deleteCheckbox);
176
+
177
+ fireEvent.click(screen.getByText('Save'));
178
+
179
+ expect(window.alert).toHaveBeenCalledWith(
180
+ 'Please select at least one activity type',
181
+ );
182
+ expect(mockOnUpdate).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it('calls onUpdate with new activity types when saved', async () => {
186
+ mockOnUpdate.mockResolvedValue();
187
+
188
+ render(
189
+ <NotificationSubscriptionsSection
190
+ subscriptions={mockSubscriptions}
191
+ onUpdate={mockOnUpdate}
192
+ onUnsubscribe={mockOnUnsubscribe}
193
+ />,
194
+ );
195
+
196
+ const editButtons = screen.getAllByTitle('Edit subscription');
197
+ fireEvent.click(editButtons[0]);
198
+
199
+ // Add 'create' activity type
200
+ const createCheckbox = screen.getByLabelText('Create');
201
+ fireEvent.click(createCheckbox);
202
+
203
+ fireEvent.click(screen.getByText('Save'));
204
+
205
+ await waitFor(() => {
206
+ expect(mockOnUpdate).toHaveBeenCalledWith(mockSubscriptions[0], [
207
+ 'update',
208
+ 'delete',
209
+ 'create',
210
+ ]);
211
+ });
212
+ });
213
+
214
+ it('renders entity type badge when node_type is not available', () => {
215
+ const subscriptionsWithoutNodeType = [
216
+ {
217
+ entity_name: 'some_entity',
218
+ entity_type: 'namespace',
219
+ activity_types: ['create'],
220
+ },
221
+ ];
222
+
223
+ render(
224
+ <NotificationSubscriptionsSection
225
+ subscriptions={subscriptionsWithoutNodeType}
226
+ onUpdate={mockOnUpdate}
227
+ onUnsubscribe={mockOnUnsubscribe}
228
+ />,
229
+ );
230
+
231
+ expect(screen.getByText('namespace')).toBeInTheDocument();
232
+ });
233
+ });
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { ProfileSection } from '../ProfileSection';
4
+
5
+ describe('ProfileSection', () => {
6
+ it('renders user initials from name', () => {
7
+ const user = {
8
+ name: 'John Doe',
9
+ username: 'johndoe',
10
+ email: 'john@example.com',
11
+ };
12
+
13
+ render(<ProfileSection user={user} />);
14
+
15
+ expect(screen.getByText('JD')).toBeInTheDocument();
16
+ expect(screen.getByText('johndoe')).toBeInTheDocument();
17
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
18
+ });
19
+
20
+ it('renders initials from username when name is not available', () => {
21
+ const user = {
22
+ username: 'alice',
23
+ email: 'alice@example.com',
24
+ };
25
+
26
+ render(<ProfileSection user={user} />);
27
+
28
+ expect(screen.getByText('AL')).toBeInTheDocument();
29
+ });
30
+
31
+ it('renders fallback when no user info available', () => {
32
+ render(<ProfileSection user={null} />);
33
+
34
+ expect(screen.getByText('?')).toBeInTheDocument();
35
+ expect(screen.getAllByText('-')).toHaveLength(2);
36
+ });
37
+
38
+ it('renders section title', () => {
39
+ render(<ProfileSection user={null} />);
40
+
41
+ expect(screen.getByText('Profile')).toBeInTheDocument();
42
+ });
43
+
44
+ it('handles single name', () => {
45
+ const user = {
46
+ name: 'Alice',
47
+ username: 'alice',
48
+ };
49
+
50
+ render(<ProfileSection user={user} />);
51
+
52
+ expect(screen.getByText('A')).toBeInTheDocument();
53
+ });
54
+
55
+ it('handles multi-word name and takes only first two initials', () => {
56
+ const user = {
57
+ name: 'John Paul Smith',
58
+ username: 'jps',
59
+ };
60
+
61
+ render(<ProfileSection user={user} />);
62
+
63
+ expect(screen.getByText('JP')).toBeInTheDocument();
64
+ });
65
+ });