datajunction-ui 0.0.26 → 0.0.27-alpha.0
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.
- package/package.json +2 -2
- package/src/app/components/Search.jsx +41 -33
- package/src/app/components/__tests__/Search.test.jsx +46 -11
- package/src/app/index.tsx +3 -3
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
- package/src/app/pages/AddEditNodePage/index.jsx +61 -17
- package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
- package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
- package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
- package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
- package/src/app/pages/Root/index.tsx +1 -6
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
- package/src/app/services/DJService.js +492 -3
- package/src/app/services/__tests__/DJService.test.jsx +582 -0
- package/src/mocks/mockNodes.jsx +36 -0
- package/webpack.config.js +27 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
3
|
import { Root } from '../index';
|
|
4
4
|
import DJClientContext from '../../../providers/djclient';
|
|
5
5
|
import { HelmetProvider } from 'react-helmet-async';
|
|
@@ -20,18 +20,22 @@ describe('<Root />', () => {
|
|
|
20
20
|
getNodesByNames: jest.fn().mockResolvedValue([]),
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('renders with the correct title and navigation', async () => {
|
|
28
|
-
render(
|
|
23
|
+
const renderRoot = () => {
|
|
24
|
+
return render(
|
|
29
25
|
<HelmetProvider>
|
|
30
26
|
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
31
27
|
<Root />
|
|
32
28
|
</DJClientContext.Provider>
|
|
33
29
|
</HelmetProvider>,
|
|
34
30
|
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders with the correct title and navigation', async () => {
|
|
38
|
+
renderRoot();
|
|
35
39
|
|
|
36
40
|
await waitFor(() => {
|
|
37
41
|
expect(document.title).toEqual('DataJunction');
|
|
@@ -39,6 +43,73 @@ describe('<Root />', () => {
|
|
|
39
43
|
|
|
40
44
|
// Check navigation links exist
|
|
41
45
|
expect(screen.getByText('Explore')).toBeInTheDocument();
|
|
42
|
-
expect(screen.getByText('
|
|
46
|
+
expect(screen.getByText('Query Planner')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders Docs dropdown', async () => {
|
|
50
|
+
renderRoot();
|
|
51
|
+
|
|
52
|
+
await waitFor(() => {
|
|
53
|
+
expect(screen.getByText('Docs')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Default docs site should be visible in dropdown
|
|
57
|
+
expect(screen.getByText('Open-Source')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders notification bell and user menu when auth is enabled', async () => {
|
|
61
|
+
// Default REACT_DISABLE_AUTH is not 'true', so auth components should show
|
|
62
|
+
renderRoot();
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(document.title).toEqual('DataJunction');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Look for nav-right container which contains the notification and user menu
|
|
69
|
+
const navRight = document.querySelector('.nav-right');
|
|
70
|
+
expect(navRight).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles notification dropdown toggle', async () => {
|
|
74
|
+
renderRoot();
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(document.title).toEqual('DataJunction');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Find the notification bell button and click it
|
|
81
|
+
const bellButton = document.querySelector('[aria-label="Notifications"]');
|
|
82
|
+
if (bellButton) {
|
|
83
|
+
fireEvent.click(bellButton);
|
|
84
|
+
// The dropdown should open
|
|
85
|
+
await waitFor(() => {
|
|
86
|
+
// After clicking, the dropdown state changes
|
|
87
|
+
expect(bellButton).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles user menu dropdown toggle', async () => {
|
|
93
|
+
renderRoot();
|
|
94
|
+
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(document.title).toEqual('DataJunction');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// The nav-right container should be present for auth-enabled mode
|
|
100
|
+
const navRight = document.querySelector('.nav-right');
|
|
101
|
+
expect(navRight).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders logo link correctly', async () => {
|
|
105
|
+
renderRoot();
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(document.title).toEqual('DataJunction');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Check logo link - name is "Data Junction" with space
|
|
112
|
+
const logoLink = screen.getByRole('link', { name: /data.*junction/i });
|
|
113
|
+
expect(logoLink).toHaveAttribute('href', '/');
|
|
43
114
|
});
|
|
44
115
|
});
|
|
@@ -61,12 +61,7 @@ export function Root() {
|
|
|
61
61
|
</span>
|
|
62
62
|
<span className="menu-link">
|
|
63
63
|
<span className="menu-title">
|
|
64
|
-
<a href="/
|
|
65
|
-
</span>
|
|
66
|
-
</span>
|
|
67
|
-
<span className="menu-link">
|
|
68
|
-
<span className="menu-title">
|
|
69
|
-
<a href="/materialization-planner">Planner</a>
|
|
64
|
+
<a href="/planner">Query Planner</a>
|
|
70
65
|
</span>
|
|
71
66
|
</span>
|
|
72
67
|
<span className="menu-link">
|
|
@@ -14,6 +14,7 @@ const mockDjClient = {
|
|
|
14
14
|
commonDimensions: jest.fn(),
|
|
15
15
|
sqls: jest.fn(),
|
|
16
16
|
data: jest.fn(),
|
|
17
|
+
stream: jest.fn(),
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
const mockMetrics = [
|
|
@@ -97,6 +98,16 @@ const mockCommonDimensions = [
|
|
|
97
98
|
},
|
|
98
99
|
];
|
|
99
100
|
|
|
101
|
+
// Additional dimensions for testing type handling
|
|
102
|
+
const mockDimensionsWithBool = [
|
|
103
|
+
{
|
|
104
|
+
name: 'default.is_active',
|
|
105
|
+
type: 'bool',
|
|
106
|
+
path: ['default.repair_order'],
|
|
107
|
+
},
|
|
108
|
+
...mockCommonDimensions,
|
|
109
|
+
];
|
|
110
|
+
|
|
100
111
|
describe('SQLBuilderPage', () => {
|
|
101
112
|
beforeEach(() => {
|
|
102
113
|
mockDjClient.metrics.mockResolvedValue(mockMetrics);
|
|
@@ -171,3 +182,74 @@ describe('SQLBuilderPage', () => {
|
|
|
171
182
|
expect(mockDjClient.sqls).toHaveBeenCalled();
|
|
172
183
|
});
|
|
173
184
|
});
|
|
185
|
+
|
|
186
|
+
describe('SQLBuilderPage - Data fetching', () => {
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
jest.clearAllMocks();
|
|
189
|
+
mockDjClient.metrics.mockResolvedValue(mockMetrics);
|
|
190
|
+
mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
|
|
191
|
+
mockDjClient.sqls.mockResolvedValue({ sql: 'SELECT * FROM table' });
|
|
192
|
+
mockDjClient.data.mockResolvedValue({});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('fetches metrics on initial render', async () => {
|
|
196
|
+
render(
|
|
197
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
198
|
+
<SQLBuilderPage />
|
|
199
|
+
</DJClientContext.Provider>,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('displays instruction card when no selections', async () => {
|
|
212
|
+
render(
|
|
213
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
214
|
+
<SQLBuilderPage />
|
|
215
|
+
</DJClientContext.Provider>,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
expect(screen.getByText('Using the SQL Builder')).toBeInTheDocument();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(
|
|
223
|
+
screen.getByText(/Start by selecting one or more/),
|
|
224
|
+
).toBeInTheDocument();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('clears dimensions when no common dimensions found', async () => {
|
|
228
|
+
mockDjClient.commonDimensions.mockResolvedValue([]);
|
|
229
|
+
|
|
230
|
+
render(
|
|
231
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
232
|
+
<SQLBuilderPage />
|
|
233
|
+
</DJClientContext.Provider>,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Wait for metrics
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Select metric
|
|
242
|
+
const selectMetrics = screen.getAllByTestId('select-metrics')[0];
|
|
243
|
+
fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' });
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
|
|
246
|
+
});
|
|
247
|
+
fireEvent.click(screen.getByText('default.num_repair_orders'));
|
|
248
|
+
fireEvent.click(screen.getAllByText('Group By')[0]);
|
|
249
|
+
|
|
250
|
+
// When no common dimensions
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(screen.getAllByText('0 Shared Dimensions')[0]).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -315,4 +315,41 @@ describe('CreateServiceAccountModal', () => {
|
|
|
315
315
|
|
|
316
316
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('abc-123');
|
|
317
317
|
});
|
|
318
|
+
|
|
319
|
+
it('copies client secret to clipboard when copy button is clicked', async () => {
|
|
320
|
+
const credentials = {
|
|
321
|
+
name: 'my-account',
|
|
322
|
+
client_id: 'abc-123',
|
|
323
|
+
client_secret: 'secret-xyz',
|
|
324
|
+
};
|
|
325
|
+
mockOnCreate.mockResolvedValue(credentials);
|
|
326
|
+
|
|
327
|
+
Object.assign(navigator, {
|
|
328
|
+
clipboard: {
|
|
329
|
+
writeText: jest.fn().mockResolvedValue(),
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
render(
|
|
334
|
+
<CreateServiceAccountModal
|
|
335
|
+
isOpen={true}
|
|
336
|
+
onClose={mockOnClose}
|
|
337
|
+
onCreate={mockOnCreate}
|
|
338
|
+
/>,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const input = screen.getByLabelText('Name');
|
|
342
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
343
|
+
fireEvent.click(screen.getByText('Create'));
|
|
344
|
+
|
|
345
|
+
await waitFor(() => {
|
|
346
|
+
expect(screen.getByText('secret-xyz')).toBeInTheDocument();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Click the second copy button (client secret)
|
|
350
|
+
const copyButtons = screen.getAllByTitle('Copy');
|
|
351
|
+
fireEvent.click(copyButtons[1]);
|
|
352
|
+
|
|
353
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('secret-xyz');
|
|
354
|
+
});
|
|
318
355
|
});
|
|
@@ -147,4 +147,52 @@ describe('ServiceAccountsSection', () => {
|
|
|
147
147
|
expect(screen.getByText('Client ID')).toBeInTheDocument();
|
|
148
148
|
expect(screen.getByText('Created')).toBeInTheDocument();
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
it('handles delete error gracefully', async () => {
|
|
152
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
153
|
+
const alertSpy = jest.spyOn(window, 'alert').mockImplementation();
|
|
154
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
155
|
+
mockOnDelete.mockRejectedValue(new Error('Delete failed'));
|
|
156
|
+
|
|
157
|
+
render(
|
|
158
|
+
<ServiceAccountsSection
|
|
159
|
+
accounts={mockAccounts}
|
|
160
|
+
onCreate={mockOnCreate}
|
|
161
|
+
onDelete={mockOnDelete}
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const deleteButtons = screen.getAllByTitle('Delete service account');
|
|
166
|
+
fireEvent.click(deleteButtons[0]);
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(alertSpy).toHaveBeenCalledWith('Failed to delete service account');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
consoleSpy.mockRestore();
|
|
173
|
+
alertSpy.mockRestore();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('closes modal when close button is clicked', () => {
|
|
177
|
+
render(
|
|
178
|
+
<ServiceAccountsSection
|
|
179
|
+
accounts={[]}
|
|
180
|
+
onCreate={mockOnCreate}
|
|
181
|
+
onDelete={mockOnDelete}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Open modal
|
|
186
|
+
fireEvent.click(screen.getByText('+ Create'));
|
|
187
|
+
expect(screen.getByText('Create Service Account')).toBeInTheDocument();
|
|
188
|
+
|
|
189
|
+
// Close modal
|
|
190
|
+
fireEvent.click(screen.getByTitle('Close'));
|
|
191
|
+
|
|
192
|
+
// Modal should no longer be visible (it stays in DOM but should be hidden)
|
|
193
|
+
// The modal content should be hidden
|
|
194
|
+
expect(
|
|
195
|
+
screen.queryByText('Create Service Account'),
|
|
196
|
+
).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
150
198
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
3
|
import { SettingsPage } from '../index';
|
|
4
4
|
import DJClientContext from '../../../providers/djclient';
|
|
5
5
|
import { UserProvider } from '../../../providers/UserProvider';
|
|
@@ -184,4 +184,172 @@ describe('SettingsPage', () => {
|
|
|
184
184
|
expect(screen.getByText('SOURCE')).toBeInTheDocument();
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
it('handles subscription update', async () => {
|
|
189
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
190
|
+
{
|
|
191
|
+
entity_name: 'default.my_metric',
|
|
192
|
+
entity_type: 'node',
|
|
193
|
+
activity_types: ['update'],
|
|
194
|
+
alert_types: ['web'],
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
199
|
+
{
|
|
200
|
+
name: 'default.my_metric',
|
|
201
|
+
type: 'METRIC',
|
|
202
|
+
current: {
|
|
203
|
+
displayName: 'My Metric',
|
|
204
|
+
status: 'VALID',
|
|
205
|
+
mode: 'PUBLISHED',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
mockDjClient.subscribeToNotifications.mockResolvedValue({
|
|
211
|
+
status: 200,
|
|
212
|
+
json: { message: 'Subscribed' },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
renderWithContext();
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(screen.getByText('default.my_metric')).toBeInTheDocument();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// The subscription is rendered with checkboxes for activity types
|
|
222
|
+
// Find and interact with the update checkbox (if available in UI)
|
|
223
|
+
const updateCheckbox = screen.queryByLabelText(/update/i);
|
|
224
|
+
if (updateCheckbox) {
|
|
225
|
+
fireEvent.click(updateCheckbox);
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(mockDjClient.subscribeToNotifications).toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('handles subscription unsubscribe', async () => {
|
|
233
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
234
|
+
{
|
|
235
|
+
entity_name: 'default.my_metric',
|
|
236
|
+
entity_type: 'node',
|
|
237
|
+
activity_types: ['update'],
|
|
238
|
+
alert_types: ['web'],
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
243
|
+
{
|
|
244
|
+
name: 'default.my_metric',
|
|
245
|
+
type: 'METRIC',
|
|
246
|
+
current: {
|
|
247
|
+
displayName: 'My Metric',
|
|
248
|
+
status: 'VALID',
|
|
249
|
+
mode: 'PUBLISHED',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
mockDjClient.unsubscribeFromNotifications.mockResolvedValue({
|
|
255
|
+
status: 200,
|
|
256
|
+
json: { message: 'Unsubscribed' },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
renderWithContext();
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(screen.getByText('default.my_metric')).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Find unsubscribe button
|
|
266
|
+
const unsubscribeBtn = screen.queryByRole('button', {
|
|
267
|
+
name: /unsubscribe/i,
|
|
268
|
+
});
|
|
269
|
+
if (unsubscribeBtn) {
|
|
270
|
+
fireEvent.click(unsubscribeBtn);
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
expect(mockDjClient.unsubscribeFromNotifications).toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('opens create service account modal', async () => {
|
|
278
|
+
renderWithContext();
|
|
279
|
+
|
|
280
|
+
await waitFor(() => {
|
|
281
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Find and click create button
|
|
285
|
+
const createBtn = screen.getByRole('button', { name: /create/i });
|
|
286
|
+
fireEvent.click(createBtn);
|
|
287
|
+
|
|
288
|
+
// Modal should be visible
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(screen.getByText('Create Service Account')).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('handles service account deletion', async () => {
|
|
295
|
+
mockDjClient.listServiceAccounts.mockResolvedValue([
|
|
296
|
+
{
|
|
297
|
+
id: 1,
|
|
298
|
+
name: 'my-pipeline',
|
|
299
|
+
client_id: 'abc-123',
|
|
300
|
+
created_at: '2024-12-01T00:00:00Z',
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
mockDjClient.deleteServiceAccount.mockResolvedValue({
|
|
305
|
+
message: 'Deleted',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
renderWithContext();
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(screen.getByText('my-pipeline')).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Find delete button
|
|
315
|
+
const deleteBtn = screen.queryByRole('button', { name: /delete/i });
|
|
316
|
+
if (deleteBtn) {
|
|
317
|
+
fireEvent.click(deleteBtn);
|
|
318
|
+
await waitFor(() => {
|
|
319
|
+
expect(mockDjClient.deleteServiceAccount).toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('handles non-node subscription types gracefully', async () => {
|
|
325
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
326
|
+
{
|
|
327
|
+
entity_name: 'namespace.test',
|
|
328
|
+
entity_type: 'namespace',
|
|
329
|
+
activity_types: ['create'],
|
|
330
|
+
alert_types: ['web'],
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
renderWithContext();
|
|
335
|
+
|
|
336
|
+
await waitFor(() => {
|
|
337
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Namespace subscription should still appear
|
|
341
|
+
await waitFor(() => {
|
|
342
|
+
expect(screen.getByText('namespace.test')).toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('skips fetching if userLoading', async () => {
|
|
347
|
+
// When user is still loading, the component waits
|
|
348
|
+
mockDjClient.whoami.mockImplementation(() => new Promise(() => {}));
|
|
349
|
+
|
|
350
|
+
renderWithContext();
|
|
351
|
+
|
|
352
|
+
// getNotificationPreferences should not be called while user is loading
|
|
353
|
+
expect(mockDjClient.getNotificationPreferences).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
187
355
|
});
|