datajunction-ui 0.0.15 → 0.0.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -0,0 +1,263 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, act } from '@testing-library/react';
3
+ import NodeMaterializationDelete from '../NodeMaterializationDelete';
4
+ import DJClientContext from '../../providers/djclient';
5
+
6
+ // Mock window.location.reload
7
+ delete window.location;
8
+ window.location = { reload: jest.fn() };
9
+
10
+ // Mock window.confirm
11
+ window.confirm = jest.fn();
12
+
13
+ const mockDjClient = {
14
+ DataJunctionAPI: {
15
+ deleteMaterialization: jest.fn(),
16
+ },
17
+ };
18
+
19
+ describe('<NodeMaterializationDelete />', () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ window.confirm.mockReturnValue(true);
23
+ });
24
+
25
+ const defaultProps = {
26
+ nodeName: 'default.test_node',
27
+ materializationName: 'test_materialization',
28
+ nodeVersion: 'v1.0',
29
+ };
30
+
31
+ it('renders delete button', () => {
32
+ const { container } = render(
33
+ <DJClientContext.Provider value={mockDjClient}>
34
+ <NodeMaterializationDelete {...defaultProps} />
35
+ </DJClientContext.Provider>,
36
+ );
37
+
38
+ const deleteButton = container.querySelector('button[type="submit"]');
39
+ expect(deleteButton).toBeInTheDocument();
40
+ });
41
+
42
+ it('renders with null nodeVersion', () => {
43
+ const { container } = render(
44
+ <DJClientContext.Provider value={mockDjClient}>
45
+ <NodeMaterializationDelete
46
+ nodeName="default.test_node"
47
+ materializationName="test_materialization"
48
+ nodeVersion={null}
49
+ />
50
+ </DJClientContext.Provider>,
51
+ );
52
+
53
+ const deleteButton = container.querySelector('button[type="submit"]');
54
+ expect(deleteButton).toBeInTheDocument();
55
+ });
56
+
57
+ it('shows confirm dialog when delete button is clicked', async () => {
58
+ const { container } = render(
59
+ <DJClientContext.Provider value={mockDjClient}>
60
+ <NodeMaterializationDelete {...defaultProps} />
61
+ </DJClientContext.Provider>,
62
+ );
63
+
64
+ const deleteButton = container.querySelector('button[type="submit"]');
65
+
66
+ await act(async () => {
67
+ fireEvent.click(deleteButton);
68
+ });
69
+
70
+ expect(window.confirm).toHaveBeenCalledWith(
71
+ expect.stringContaining(
72
+ 'Deleting materialization job test_materialization',
73
+ ),
74
+ );
75
+ });
76
+
77
+ it('does not call deleteMaterialization when user cancels confirm', async () => {
78
+ window.confirm.mockReturnValueOnce(false);
79
+
80
+ const { container } = render(
81
+ <DJClientContext.Provider value={mockDjClient}>
82
+ <NodeMaterializationDelete {...defaultProps} />
83
+ </DJClientContext.Provider>,
84
+ );
85
+
86
+ const deleteButton = container.querySelector('button[type="submit"]');
87
+
88
+ await act(async () => {
89
+ fireEvent.click(deleteButton);
90
+ });
91
+
92
+ expect(window.confirm).toHaveBeenCalled();
93
+ expect(
94
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
95
+ ).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it('calls deleteMaterialization with correct params on success - status 200', async () => {
99
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
100
+ status: 200,
101
+ json: { message: 'Deleted successfully' },
102
+ });
103
+
104
+ const { container, getByText } = render(
105
+ <DJClientContext.Provider value={mockDjClient}>
106
+ <NodeMaterializationDelete {...defaultProps} />
107
+ </DJClientContext.Provider>,
108
+ );
109
+
110
+ const deleteButton = container.querySelector('button[type="submit"]');
111
+
112
+ await act(async () => {
113
+ fireEvent.click(deleteButton);
114
+ });
115
+
116
+ await waitFor(() => {
117
+ expect(
118
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
119
+ ).toHaveBeenCalledWith(
120
+ 'default.test_node',
121
+ 'test_materialization',
122
+ 'v1.0',
123
+ );
124
+ expect(
125
+ getByText(/Successfully deleted materialization job/),
126
+ ).toBeInTheDocument();
127
+ expect(window.location.reload).toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ it('calls deleteMaterialization with correct params on success - status 201', async () => {
132
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
133
+ status: 201,
134
+ json: { message: 'Deleted successfully' },
135
+ });
136
+
137
+ const { container } = render(
138
+ <DJClientContext.Provider value={mockDjClient}>
139
+ <NodeMaterializationDelete {...defaultProps} />
140
+ </DJClientContext.Provider>,
141
+ );
142
+
143
+ const deleteButton = container.querySelector('button[type="submit"]');
144
+
145
+ await act(async () => {
146
+ fireEvent.click(deleteButton);
147
+ });
148
+
149
+ await waitFor(() => {
150
+ expect(
151
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
152
+ ).toHaveBeenCalled();
153
+ expect(window.location.reload).toHaveBeenCalled();
154
+ });
155
+ });
156
+
157
+ it('calls deleteMaterialization with correct params on success - status 204', async () => {
158
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
159
+ status: 204,
160
+ json: {},
161
+ });
162
+
163
+ const { container } = render(
164
+ <DJClientContext.Provider value={mockDjClient}>
165
+ <NodeMaterializationDelete {...defaultProps} />
166
+ </DJClientContext.Provider>,
167
+ );
168
+
169
+ const deleteButton = container.querySelector('button[type="submit"]');
170
+
171
+ await act(async () => {
172
+ fireEvent.click(deleteButton);
173
+ });
174
+
175
+ await waitFor(() => {
176
+ expect(
177
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
178
+ ).toHaveBeenCalled();
179
+ expect(window.location.reload).toHaveBeenCalled();
180
+ });
181
+ });
182
+
183
+ it('displays error message when deletion fails', async () => {
184
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
185
+ status: 500,
186
+ json: { message: 'Internal server error' },
187
+ });
188
+
189
+ const { container, getByText } = render(
190
+ <DJClientContext.Provider value={mockDjClient}>
191
+ <NodeMaterializationDelete {...defaultProps} />
192
+ </DJClientContext.Provider>,
193
+ );
194
+
195
+ const deleteButton = container.querySelector('button[type="submit"]');
196
+
197
+ await act(async () => {
198
+ fireEvent.click(deleteButton);
199
+ });
200
+
201
+ await waitFor(() => {
202
+ expect(
203
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
204
+ ).toHaveBeenCalled();
205
+ expect(getByText('Internal server error')).toBeInTheDocument();
206
+ expect(window.location.reload).not.toHaveBeenCalled();
207
+ });
208
+ });
209
+
210
+ it('hides delete button after successful deletion', async () => {
211
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
212
+ status: 200,
213
+ json: { message: 'Deleted successfully' },
214
+ });
215
+
216
+ const { container } = render(
217
+ <DJClientContext.Provider value={mockDjClient}>
218
+ <NodeMaterializationDelete {...defaultProps} />
219
+ </DJClientContext.Provider>,
220
+ );
221
+
222
+ const deleteButton = container.querySelector('button[type="submit"]');
223
+ expect(deleteButton).toBeInTheDocument();
224
+
225
+ await act(async () => {
226
+ fireEvent.click(deleteButton);
227
+ });
228
+
229
+ await waitFor(() => {
230
+ expect(
231
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
232
+ ).toHaveBeenCalled();
233
+ });
234
+ });
235
+
236
+ it('passes null nodeVersion to deleteMaterialization when not provided', async () => {
237
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
238
+ status: 200,
239
+ json: { message: 'Deleted successfully' },
240
+ });
241
+
242
+ const { container } = render(
243
+ <DJClientContext.Provider value={mockDjClient}>
244
+ <NodeMaterializationDelete
245
+ nodeName="default.test_node"
246
+ materializationName="test_mat"
247
+ />
248
+ </DJClientContext.Provider>,
249
+ );
250
+
251
+ const deleteButton = container.querySelector('button[type="submit"]');
252
+
253
+ await act(async () => {
254
+ fireEvent.click(deleteButton);
255
+ });
256
+
257
+ await waitFor(() => {
258
+ expect(
259
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
260
+ ).toHaveBeenCalledWith('default.test_node', 'test_mat', null);
261
+ });
262
+ });
263
+ });
@@ -1,55 +1,183 @@
1
- import * as React from 'react';
2
- import { render, screen } from '@testing-library/react';
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
3
  import QueryInfo from '../QueryInfo';
4
4
 
5
5
  describe('<QueryInfo />', () => {
6
6
  const defaultProps = {
7
- id: '123',
8
- state: 'Running',
9
- engine_name: 'Spark SQL',
10
- engine_version: '1.0',
11
- errors: ['Error 1', 'Error 2'],
12
- links: ['http://example.com', 'http://example2.com'],
13
- output_table: 'table1',
14
- scheduled: '2023-09-06',
15
- started: '2023-09-07',
7
+ id: 'query-123',
8
+ state: 'completed',
9
+ engine_name: 'spark',
10
+ engine_version: '3.2.0',
11
+ errors: [],
12
+ links: [],
13
+ output_table: 'output.table',
14
+ scheduled: '2024-01-01 10:00:00',
15
+ started: '2024-01-01 10:05:00',
16
+ finished: '2024-01-01 10:15:00',
16
17
  numRows: 1000,
18
+ isList: false,
17
19
  };
18
20
 
19
- it('renders without crashing', () => {
20
- render(<QueryInfo {...defaultProps} />);
21
- });
22
-
23
- it('displays correct query information', () => {
24
- render(<QueryInfo {...defaultProps} />);
25
-
26
- expect(screen.getByText(defaultProps.id)).toBeInTheDocument();
27
- expect(
28
- screen.getByText(
29
- `${defaultProps.engine_name} - ${defaultProps.engine_version}`,
30
- ),
31
- ).toBeInTheDocument();
32
- expect(screen.getByText(defaultProps.state)).toBeInTheDocument();
33
- expect(screen.getByText(defaultProps.scheduled)).toBeInTheDocument();
34
- expect(screen.getByText(defaultProps.started)).toBeInTheDocument();
35
- expect(screen.getByText(defaultProps.output_table)).toBeInTheDocument();
36
- expect(screen.getByText(String(defaultProps.numRows))).toBeInTheDocument();
37
- defaultProps.errors.forEach(error => {
38
- expect(screen.getByText(error)).toBeInTheDocument();
39
- });
40
- defaultProps.links.forEach(link => {
41
- expect(screen.getByText(link)).toHaveAttribute('href', link);
42
- });
43
- });
44
-
45
- it('does not render errors and links when they are not provided', () => {
46
- render(<QueryInfo {...defaultProps} errors={[]} links={[]} />);
47
-
48
- defaultProps.errors.forEach(error => {
49
- expect(screen.queryByText(error)).not.toBeInTheDocument();
50
- });
51
- defaultProps.links.forEach(link => {
52
- expect(screen.queryByText(link)).not.toBeInTheDocument();
53
- });
21
+ it('renders table view when isList is false', () => {
22
+ const { getByText, container } = render(<QueryInfo {...defaultProps} />);
23
+
24
+ expect(getByText('Query ID')).toBeInTheDocument();
25
+ expect(getByText('query-123')).toBeInTheDocument();
26
+ expect(container.textContent).toContain('spark');
27
+ expect(getByText('completed')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders list view when isList is true', () => {
31
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
32
+
33
+ expect(getByText('Query ID')).toBeInTheDocument();
34
+ expect(getByText('State')).toBeInTheDocument();
35
+ expect(getByText('Engine')).toBeInTheDocument();
36
+ });
37
+
38
+ it('displays errors in table view', () => {
39
+ const propsWithErrors = {
40
+ ...defaultProps,
41
+ errors: ['Error 1', 'Error 2'],
42
+ };
43
+
44
+ const { getByText } = render(<QueryInfo {...propsWithErrors} />);
45
+
46
+ expect(getByText('Error 1')).toBeInTheDocument();
47
+ expect(getByText('Error 2')).toBeInTheDocument();
48
+ });
49
+
50
+ it('displays links in table view', () => {
51
+ const propsWithLinks = {
52
+ ...defaultProps,
53
+ links: ['https://example.com/query1', 'https://example.com/query2'],
54
+ };
55
+
56
+ const { getByText } = render(<QueryInfo {...propsWithLinks} />);
57
+
58
+ expect(getByText('https://example.com/query1')).toBeInTheDocument();
59
+ expect(getByText('https://example.com/query2')).toBeInTheDocument();
60
+ });
61
+
62
+ it('renders empty state when no errors', () => {
63
+ const { container } = render(<QueryInfo {...defaultProps} />);
64
+
65
+ const errorCell = container.querySelector('td:nth-child(6)');
66
+ expect(errorCell).toBeInTheDocument();
67
+ });
68
+
69
+ it('renders empty state when no links', () => {
70
+ const { container } = render(<QueryInfo {...defaultProps} />);
71
+
72
+ const linksCell = container.querySelector('td:nth-child(7)');
73
+ expect(linksCell).toBeInTheDocument();
74
+ });
75
+
76
+ it('displays all query information in table view', () => {
77
+ const { getByText } = render(<QueryInfo {...defaultProps} />);
78
+
79
+ expect(getByText('output.table')).toBeInTheDocument();
80
+ expect(getByText('1000')).toBeInTheDocument();
81
+ expect(getByText('2024-01-01 10:00:00')).toBeInTheDocument();
82
+ expect(getByText('2024-01-01 10:05:00')).toBeInTheDocument();
83
+ });
84
+
85
+ it('renders list view with query ID link when links present', () => {
86
+ const propsWithLinks = {
87
+ ...defaultProps,
88
+ links: ['https://example.com/query'],
89
+ isList: true,
90
+ };
91
+
92
+ const { container } = render(<QueryInfo {...propsWithLinks} />);
93
+
94
+ const link = container.querySelector('a[href="https://example.com/query"]');
95
+ expect(link).toBeInTheDocument();
96
+ expect(link).toHaveTextContent('query-123');
97
+ });
98
+
99
+ it('renders list view with query ID as text when no links', () => {
100
+ const propsNoLinks = {
101
+ ...defaultProps,
102
+ links: [],
103
+ isList: true,
104
+ };
105
+
106
+ const { getByText } = render(<QueryInfo {...propsNoLinks} />);
107
+
108
+ expect(getByText('query-123')).toBeInTheDocument();
109
+ });
110
+
111
+ it('displays errors with syntax highlighter in list view', () => {
112
+ const propsWithErrors = {
113
+ ...defaultProps,
114
+ errors: ['Syntax error on line 5', 'Connection timeout'],
115
+ isList: true,
116
+ };
117
+
118
+ const { getByText } = render(<QueryInfo {...propsWithErrors} />);
119
+
120
+ expect(getByText('Logs')).toBeInTheDocument();
121
+ });
122
+
123
+ it('displays finished timestamp in list view', () => {
124
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
125
+
126
+ expect(getByText('Finished')).toBeInTheDocument();
127
+ expect(getByText('2024-01-01 10:15:00')).toBeInTheDocument();
128
+ });
129
+
130
+ it('displays output table and row count in list view', () => {
131
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
132
+
133
+ expect(getByText('Output Table:')).toBeInTheDocument();
134
+ expect(getByText('output.table')).toBeInTheDocument();
135
+ expect(getByText('Rows:')).toBeInTheDocument();
136
+ expect(getByText('1000')).toBeInTheDocument();
137
+ });
138
+
139
+ it('displays multiple links in list view', () => {
140
+ const propsWithLinks = {
141
+ ...defaultProps,
142
+ links: ['https://link1.com', 'https://link2.com', 'https://link3.com'],
143
+ isList: true,
144
+ };
145
+
146
+ const { getByText } = render(<QueryInfo {...propsWithLinks} />);
147
+
148
+ expect(getByText('https://link1.com')).toBeInTheDocument();
149
+ expect(getByText('https://link2.com')).toBeInTheDocument();
150
+ expect(getByText('https://link3.com')).toBeInTheDocument();
151
+ });
152
+
153
+ it('renders empty logs section when no errors in list view', () => {
154
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
155
+
156
+ expect(getByText('Logs')).toBeInTheDocument();
157
+ });
158
+
159
+ it('displays engine name and version correctly', () => {
160
+ const { container } = render(<QueryInfo {...defaultProps} />);
161
+
162
+ const badges = container.querySelectorAll('.badge');
163
+ const engineBadge = Array.from(badges).find(b =>
164
+ b.textContent.includes('spark'),
165
+ );
166
+ expect(engineBadge).toBeTruthy();
167
+ expect(engineBadge.textContent).toContain('3.2.0');
168
+ });
169
+
170
+ it('handles undefined optional props', () => {
171
+ const minimalProps = {
172
+ id: 'query-456',
173
+ state: 'running',
174
+ engine_name: 'trino',
175
+ engine_version: '1.0',
176
+ };
177
+
178
+ const { getByText } = render(<QueryInfo {...minimalProps} />);
179
+
180
+ expect(getByText('query-456')).toBeInTheDocument();
181
+ expect(getByText('running')).toBeInTheDocument();
54
182
  });
55
183
  });