datajunction-ui 0.0.15 → 0.0.17

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.
@@ -0,0 +1,283 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, act } from '@testing-library/react';
3
+ import AddNamespacePopover from '../AddNamespacePopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ // Mock window.location.reload
7
+ delete window.location;
8
+ window.location = { reload: jest.fn() };
9
+
10
+ const mockDjClient = {
11
+ DataJunctionAPI: {
12
+ addNamespace: jest.fn(),
13
+ },
14
+ };
15
+
16
+ describe('<AddNamespacePopover />', () => {
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ const defaultProps = {
22
+ namespace: 'default',
23
+ };
24
+
25
+ it('renders the toggle button', () => {
26
+ const { getByLabelText } = render(
27
+ <DJClientContext.Provider value={mockDjClient}>
28
+ <AddNamespacePopover {...defaultProps} />
29
+ </DJClientContext.Provider>,
30
+ );
31
+
32
+ expect(getByLabelText('AddNamespaceTogglePopover')).toBeInTheDocument();
33
+ });
34
+
35
+ it('opens popover when toggle button is clicked', async () => {
36
+ const { getByLabelText, getByRole } = render(
37
+ <DJClientContext.Provider value={mockDjClient}>
38
+ <AddNamespacePopover {...defaultProps} />
39
+ </DJClientContext.Provider>,
40
+ );
41
+
42
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
43
+
44
+ await waitFor(() => {
45
+ expect(
46
+ getByRole('dialog', { name: 'AddNamespacePopover' }),
47
+ ).toBeVisible();
48
+ });
49
+ });
50
+
51
+ it('pre-fills namespace field with parent namespace', async () => {
52
+ const { getByLabelText, getByDisplayValue } = render(
53
+ <DJClientContext.Provider value={mockDjClient}>
54
+ <AddNamespacePopover namespace="parent" />
55
+ </DJClientContext.Provider>,
56
+ );
57
+
58
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
59
+
60
+ await waitFor(() => {
61
+ expect(getByDisplayValue('parent.')).toBeInTheDocument();
62
+ });
63
+ });
64
+
65
+ it('displays namespace input field and save button', async () => {
66
+ const { getByLabelText, getByText } = render(
67
+ <DJClientContext.Provider value={mockDjClient}>
68
+ <AddNamespacePopover {...defaultProps} />
69
+ </DJClientContext.Provider>,
70
+ );
71
+
72
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
73
+
74
+ await waitFor(() => {
75
+ expect(getByLabelText('Namespace')).toBeInTheDocument();
76
+ expect(getByLabelText('SaveNamespace')).toBeInTheDocument();
77
+ expect(getByText('Save')).toBeInTheDocument();
78
+ });
79
+ });
80
+
81
+ it('allows typing in namespace field', async () => {
82
+ const { getByLabelText, getByPlaceholderText } = render(
83
+ <DJClientContext.Provider value={mockDjClient}>
84
+ <AddNamespacePopover {...defaultProps} />
85
+ </DJClientContext.Provider>,
86
+ );
87
+
88
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
89
+
90
+ const namespaceInput = getByPlaceholderText('New namespace');
91
+
92
+ await act(async () => {
93
+ fireEvent.change(namespaceInput, {
94
+ target: { value: 'default.new_namespace' },
95
+ });
96
+ });
97
+
98
+ expect(namespaceInput.value).toBe('default.new_namespace');
99
+ });
100
+
101
+ it('calls addNamespace with correct value on form submission - success', async () => {
102
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
103
+ status: 200,
104
+ json: { message: 'Namespace created' },
105
+ });
106
+
107
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
108
+ <DJClientContext.Provider value={mockDjClient}>
109
+ <AddNamespacePopover {...defaultProps} />
110
+ </DJClientContext.Provider>,
111
+ );
112
+
113
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
114
+
115
+ const namespaceInput = getByPlaceholderText('New namespace');
116
+
117
+ await act(async () => {
118
+ fireEvent.change(namespaceInput, { target: { value: 'default.child' } });
119
+ });
120
+
121
+ const saveButton = getByLabelText('SaveNamespace');
122
+
123
+ await act(async () => {
124
+ fireEvent.click(saveButton);
125
+ });
126
+
127
+ await waitFor(() => {
128
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
129
+ 'default.child',
130
+ );
131
+ expect(getByText('Saved')).toBeInTheDocument();
132
+ });
133
+
134
+ // Should reload page after success
135
+ expect(window.location.reload).toHaveBeenCalled();
136
+ });
137
+
138
+ it('calls addNamespace with correct value on form submission - status 201', async () => {
139
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
140
+ status: 201,
141
+ json: { message: 'Namespace created' },
142
+ });
143
+
144
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
145
+ <DJClientContext.Provider value={mockDjClient}>
146
+ <AddNamespacePopover {...defaultProps} />
147
+ </DJClientContext.Provider>,
148
+ );
149
+
150
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
151
+
152
+ const namespaceInput = getByPlaceholderText('New namespace');
153
+
154
+ await act(async () => {
155
+ fireEvent.change(namespaceInput, {
156
+ target: { value: 'default.another' },
157
+ });
158
+ });
159
+
160
+ const saveButton = getByLabelText('SaveNamespace');
161
+
162
+ await act(async () => {
163
+ fireEvent.click(saveButton);
164
+ });
165
+
166
+ await waitFor(() => {
167
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
168
+ 'default.another',
169
+ );
170
+ expect(getByText('Saved')).toBeInTheDocument();
171
+ });
172
+
173
+ expect(window.location.reload).toHaveBeenCalled();
174
+ });
175
+
176
+ it('displays error message when addNamespace fails', async () => {
177
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
178
+ status: 400,
179
+ json: { message: 'Namespace already exists' },
180
+ });
181
+
182
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
183
+ <DJClientContext.Provider value={mockDjClient}>
184
+ <AddNamespacePopover {...defaultProps} />
185
+ </DJClientContext.Provider>,
186
+ );
187
+
188
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
189
+
190
+ const namespaceInput = getByPlaceholderText('New namespace');
191
+
192
+ await act(async () => {
193
+ fireEvent.change(namespaceInput, {
194
+ target: { value: 'default.duplicate' },
195
+ });
196
+ });
197
+
198
+ const saveButton = getByLabelText('SaveNamespace');
199
+
200
+ await act(async () => {
201
+ fireEvent.click(saveButton);
202
+ });
203
+
204
+ await waitFor(() => {
205
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
206
+ 'default.duplicate',
207
+ );
208
+ expect(getByText('Namespace already exists')).toBeInTheDocument();
209
+ });
210
+
211
+ // Should still reload page even on failure
212
+ expect(window.location.reload).toHaveBeenCalled();
213
+ });
214
+
215
+ it('closes popover when toggle button is clicked again', async () => {
216
+ const { getByLabelText, getByRole, container } = render(
217
+ <DJClientContext.Provider value={mockDjClient}>
218
+ <AddNamespacePopover {...defaultProps} />
219
+ </DJClientContext.Provider>,
220
+ );
221
+
222
+ // Open popover
223
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
224
+
225
+ await waitFor(() => {
226
+ expect(getByRole('dialog')).toBeVisible();
227
+ });
228
+
229
+ // Close popover
230
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
231
+
232
+ // Popover should still exist but be hidden
233
+ const popover = container.querySelector('[role="dialog"]');
234
+ expect(popover).toHaveStyle({ display: 'none' });
235
+ });
236
+
237
+ it('handles nested namespace creation', async () => {
238
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
239
+ status: 200,
240
+ json: { message: 'Namespace created' },
241
+ });
242
+
243
+ const { getByLabelText, getByDisplayValue } = render(
244
+ <DJClientContext.Provider value={mockDjClient}>
245
+ <AddNamespacePopover namespace="parent.child" />
246
+ </DJClientContext.Provider>,
247
+ );
248
+
249
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
250
+
251
+ await waitFor(() => {
252
+ expect(getByDisplayValue('parent.child.')).toBeInTheDocument();
253
+ });
254
+ });
255
+
256
+ it('submits with initial value if not changed', async () => {
257
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
258
+ status: 200,
259
+ json: { message: 'Namespace created' },
260
+ });
261
+
262
+ const { getByLabelText, getByText } = render(
263
+ <DJClientContext.Provider value={mockDjClient}>
264
+ <AddNamespacePopover namespace="test" />
265
+ </DJClientContext.Provider>,
266
+ );
267
+
268
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
269
+
270
+ const saveButton = getByLabelText('SaveNamespace');
271
+
272
+ await act(async () => {
273
+ fireEvent.click(saveButton);
274
+ });
275
+
276
+ await waitFor(() => {
277
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
278
+ 'test.',
279
+ );
280
+ expect(getByText('Saved')).toBeInTheDocument();
281
+ });
282
+ });
283
+ });
@@ -201,7 +201,15 @@ describe('NamespacePage', () => {
201
201
  fireEvent.click(screen.getByText('common'));
202
202
  });
203
203
 
204
- it('can add new namespace via add namespace popover', async () => {
204
+ it('can add new namespace via inline creation', async () => {
205
+ // Mock window.location to track navigation
206
+ delete window.location;
207
+ window.location = { href: jest.fn() };
208
+ Object.defineProperty(window.location, 'href', {
209
+ set: jest.fn(),
210
+ get: jest.fn(),
211
+ });
212
+
205
213
  mockDjClient.addNamespace.mockReturnValue({
206
214
  status: 201,
207
215
  json: {},
@@ -212,45 +220,53 @@ describe('NamespacePage', () => {
212
220
  </DJClientContext.Provider>
213
221
  );
214
222
  render(
215
- <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
223
+ <MemoryRouter initialEntries={['/namespaces/default']}>
216
224
  <Routes>
217
225
  <Route path="namespaces/:namespace" element={element} />
218
226
  </Routes>
219
227
  </MemoryRouter>,
220
228
  );
221
229
 
222
- // Find the button to toggle the add namespace popover
223
- const addNamespaceToggle = screen.getByRole('button', {
224
- name: 'AddNamespaceTogglePopover',
230
+ // Wait for namespaces to load
231
+ await waitFor(() => {
232
+ expect(screen.getByText('default')).toBeInTheDocument();
225
233
  });
226
- expect(addNamespaceToggle).toBeInTheDocument();
227
234
 
228
- // Click the toggle and verify that the popover displays
229
- fireEvent.click(addNamespaceToggle);
230
- const addNamespacePopover = screen.getByRole('dialog', {
231
- name: 'AddNamespacePopover',
232
- });
233
- expect(addNamespacePopover).toBeInTheDocument();
235
+ // Find the namespace and hover to reveal add button
236
+ const defaultNamespace = screen
237
+ .getByText('default')
238
+ .closest('.select-name');
239
+ fireEvent.mouseEnter(defaultNamespace);
234
240
 
235
- // Type in the new namespace
236
- await userEvent.type(
237
- screen.getByLabelText('Namespace'),
238
- 'some.random.namespace',
241
+ // Find the add namespace button (it exists but is hidden, so use getAllByTitle)
242
+ const addButtons = screen.getAllByTitle('Add child namespace');
243
+ const defaultAddButton = addButtons.find(btn =>
244
+ btn
245
+ .closest('.namespace-item')
246
+ ?.querySelector('a[href="/namespaces/default"]'),
239
247
  );
240
248
 
241
- // Save
242
- const saveNamespace = screen.getByRole('button', {
243
- name: 'SaveNamespace',
249
+ expect(defaultAddButton).toBeInTheDocument();
250
+ fireEvent.click(defaultAddButton);
251
+
252
+ // Type in the new namespace name
253
+ await waitFor(() => {
254
+ const input = screen.getByPlaceholderText('New namespace name');
255
+ expect(input).toBeInTheDocument();
244
256
  });
257
+
258
+ const input = screen.getByPlaceholderText('New namespace name');
259
+ await userEvent.type(input, 'new_child');
260
+
261
+ // Submit the form
262
+ const submitButton = screen.getByRole('button', { name: '✓' });
263
+ fireEvent.click(submitButton);
264
+
245
265
  await waitFor(() => {
246
- fireEvent.click(saveNamespace);
266
+ expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
267
+ 'default.new_child',
268
+ );
247
269
  });
248
- expect(mockDjClient.addNamespace).toHaveBeenCalled();
249
- expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
250
- 'test.namespace.some.random.namespace',
251
- );
252
- expect(screen.getByText('Saved')).toBeInTheDocument();
253
- expect(window.location.reload).toHaveBeenCalled();
254
270
  });
255
271
 
256
272
  it('can fail to add namespace', async () => {
@@ -264,34 +280,51 @@ describe('NamespacePage', () => {
264
280
  </DJClientContext.Provider>
265
281
  );
266
282
  render(
267
- <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
283
+ <MemoryRouter initialEntries={['/namespaces/default']}>
268
284
  <Routes>
269
285
  <Route path="namespaces/:namespace" element={element} />
270
286
  </Routes>
271
287
  </MemoryRouter>,
272
288
  );
273
289
 
274
- // Open the add namespace popover
275
- const addNamespaceToggle = screen.getByRole('button', {
276
- name: 'AddNamespaceTogglePopover',
290
+ // Wait for namespaces to load
291
+ await waitFor(() => {
292
+ expect(screen.getByText('default')).toBeInTheDocument();
277
293
  });
278
- fireEvent.click(addNamespaceToggle);
279
294
 
280
- // Type in the new namespace
281
- await userEvent.type(
282
- screen.getByLabelText('Namespace'),
283
- 'some.random.namespace',
295
+ // Find the namespace and hover to reveal add button
296
+ const defaultNamespace = screen
297
+ .getByText('default')
298
+ .closest('.select-name');
299
+ fireEvent.mouseEnter(defaultNamespace);
300
+
301
+ // Find the add namespace button (it exists but is hidden, so use getAllByTitle)
302
+ const addButtons = screen.getAllByTitle('Add child namespace');
303
+ const defaultAddButton = addButtons.find(btn =>
304
+ btn
305
+ .closest('.namespace-item')
306
+ ?.querySelector('a[href="/namespaces/default"]'),
284
307
  );
285
308
 
286
- // Save
287
- const saveNamespace = screen.getByRole('button', {
288
- name: 'SaveNamespace',
289
- });
309
+ expect(defaultAddButton).toBeInTheDocument();
310
+ fireEvent.click(defaultAddButton);
311
+
312
+ // Type in the new namespace name
290
313
  await waitFor(() => {
291
- fireEvent.click(saveNamespace);
314
+ const input = screen.getByPlaceholderText('New namespace name');
315
+ expect(input).toBeInTheDocument();
292
316
  });
293
317
 
318
+ const input = screen.getByPlaceholderText('New namespace name');
319
+ await userEvent.type(input, 'bad_namespace');
320
+
321
+ // Submit the form
322
+ const submitButton = screen.getByRole('button', { name: '✓' });
323
+ fireEvent.click(submitButton);
324
+
294
325
  // Should display failure alert
295
- expect(screen.getByText('you failed')).toBeInTheDocument();
326
+ await waitFor(() => {
327
+ expect(screen.getByText('you failed')).toBeInTheDocument();
328
+ });
296
329
  });
297
330
  });
@@ -290,23 +290,29 @@ export function NamespacePage() {
290
290
  </div>
291
291
  <div className="table-responsive">
292
292
  <div className={`sidebar`}>
293
- <span
293
+ <div
294
294
  style={{
295
- textTransform: 'uppercase',
296
- fontSize: '0.8125rem',
297
- fontWeight: '600',
298
- color: '#95aac9',
299
295
  padding: '1rem 1rem 1rem 0',
300
296
  }}
301
297
  >
302
- Namespaces <AddNamespacePopover namespace={namespace} />
303
- </span>
298
+ <span
299
+ style={{
300
+ textTransform: 'uppercase',
301
+ fontSize: '0.8125rem',
302
+ fontWeight: '600',
303
+ color: '#95aac9',
304
+ }}
305
+ >
306
+ Namespaces
307
+ </span>
308
+ </div>
304
309
  {namespaceHierarchy
305
310
  ? namespaceHierarchy.map(child => (
306
311
  <Explorer
307
312
  item={child}
308
313
  current={state.namespace}
309
314
  defaultExpand={true}
315
+ isTopLevel={true}
310
316
  key={child.namespace}
311
317
  />
312
318
  ))