datajunction-ui 0.0.92 → 0.0.94
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 +1 -1
- package/src/app/components/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/services/DJService.js +73 -6
- package/src/app/services/__tests__/DJService.test.jsx +591 -0
- package/src/styles/index.css +32 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
import NodeDataFlowTab from '../NodeDataFlowTab';
|
|
6
|
+
|
|
7
|
+
// Sankey mock renders the node/link elements with fake props so SankeyNode/SankeyLink are covered
|
|
8
|
+
jest.mock('recharts', () => {
|
|
9
|
+
const React = require('react');
|
|
10
|
+
return {
|
|
11
|
+
Sankey: ({ node: nodeEl, link: linkEl, data, children }) => {
|
|
12
|
+
const nodes = data?.nodes || [];
|
|
13
|
+
const makeNode = (payload, extra = {}) =>
|
|
14
|
+
React.isValidElement(nodeEl)
|
|
15
|
+
? React.cloneElement(nodeEl, {
|
|
16
|
+
x: 10,
|
|
17
|
+
y: 10,
|
|
18
|
+
width: 20,
|
|
19
|
+
height: 30,
|
|
20
|
+
payload,
|
|
21
|
+
hoveredNodeName: null,
|
|
22
|
+
onNodeHover: () => {},
|
|
23
|
+
onNavigate: () => {},
|
|
24
|
+
...extra,
|
|
25
|
+
})
|
|
26
|
+
: null;
|
|
27
|
+
const makeLink = (payload, extra = {}) =>
|
|
28
|
+
React.isValidElement(linkEl)
|
|
29
|
+
? React.cloneElement(linkEl, {
|
|
30
|
+
sourceX: 0,
|
|
31
|
+
targetX: 100,
|
|
32
|
+
sourceY: 50,
|
|
33
|
+
targetY: 50,
|
|
34
|
+
sourceControlX: 30,
|
|
35
|
+
targetControlX: 70,
|
|
36
|
+
linkWidth: 5,
|
|
37
|
+
index: 0,
|
|
38
|
+
payload,
|
|
39
|
+
...extra,
|
|
40
|
+
})
|
|
41
|
+
: null;
|
|
42
|
+
const firstNode = nodes[0];
|
|
43
|
+
const secondNode = nodes[1] || nodes[0];
|
|
44
|
+
return (
|
|
45
|
+
<div data-testid="sankey">
|
|
46
|
+
{/* SankeyNode: null payload → returns null */}
|
|
47
|
+
{makeNode(null)}
|
|
48
|
+
{/* SankeyNode: phantom type → returns <g/> */}
|
|
49
|
+
{firstNode && makeNode({ ...firstNode, type: 'phantom' })}
|
|
50
|
+
{/* SankeyNode: normal */}
|
|
51
|
+
{firstNode && makeNode(firstNode)}
|
|
52
|
+
{/* SankeyNode: hovered (isHovered=true) */}
|
|
53
|
+
{firstNode &&
|
|
54
|
+
makeNode(firstNode, { hoveredNodeName: firstNode.name })}
|
|
55
|
+
{/* SankeyNode: dimmed (hoveredNodeName !== this node) */}
|
|
56
|
+
{firstNode && makeNode(firstNode, { hoveredNodeName: 'other.node' })}
|
|
57
|
+
{/* SankeyNode: no name → cursor:default */}
|
|
58
|
+
{makeNode({ type: 'metric', display_name: 'No Name Node' })}
|
|
59
|
+
{/* SankeyLink: phantom target → returns <g/> */}
|
|
60
|
+
{firstNode &&
|
|
61
|
+
secondNode &&
|
|
62
|
+
makeLink({
|
|
63
|
+
source: firstNode,
|
|
64
|
+
target: { ...secondNode, type: 'phantom' },
|
|
65
|
+
})}
|
|
66
|
+
{/* SankeyLink: no hover */}
|
|
67
|
+
{firstNode &&
|
|
68
|
+
secondNode &&
|
|
69
|
+
makeLink({ source: firstNode, target: secondNode })}
|
|
70
|
+
{/* SankeyLink: hovered matching source → isConnected=true, opacity=0.8 */}
|
|
71
|
+
{firstNode &&
|
|
72
|
+
secondNode &&
|
|
73
|
+
makeLink(
|
|
74
|
+
{ source: firstNode, target: secondNode },
|
|
75
|
+
{ hoveredNodeName: firstNode.name },
|
|
76
|
+
)}
|
|
77
|
+
{/* SankeyLink: hovered not matching → isConnected=false, opacity=0.15 */}
|
|
78
|
+
{firstNode &&
|
|
79
|
+
secondNode &&
|
|
80
|
+
makeLink(
|
|
81
|
+
{ source: firstNode, target: secondNode },
|
|
82
|
+
{ hoveredNodeName: 'something.else' },
|
|
83
|
+
)}
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
// Tooltip mock invokes the content function with various args to cover lines 413-421
|
|
89
|
+
Tooltip: ({ content }) => {
|
|
90
|
+
if (typeof content !== 'function') return null;
|
|
91
|
+
return (
|
|
92
|
+
<div data-testid="tooltip">
|
|
93
|
+
{/* active=false → line 413 returns null */}
|
|
94
|
+
{content({
|
|
95
|
+
active: false,
|
|
96
|
+
payload: [{ payload: { name: 'test', type: 'metric' } }],
|
|
97
|
+
})}
|
|
98
|
+
{/* payload empty → line 413 returns null */}
|
|
99
|
+
{content({ active: true, payload: [] })}
|
|
100
|
+
{/* item=null → line 415 returns null */}
|
|
101
|
+
{content({ active: true, payload: [{}] })}
|
|
102
|
+
{/* item.name exists + item.type → lines 416-421 */}
|
|
103
|
+
{content({
|
|
104
|
+
active: true,
|
|
105
|
+
payload: [{ payload: { name: 'a.b.metric', type: 'metric' } }],
|
|
106
|
+
})}
|
|
107
|
+
{/* item.name falsy → uses source→target fallback (line 418) */}
|
|
108
|
+
{content({
|
|
109
|
+
active: true,
|
|
110
|
+
payload: [
|
|
111
|
+
{
|
|
112
|
+
payload: {
|
|
113
|
+
source: { display_name: 'Src Node', name: 'src' },
|
|
114
|
+
target: { name: 'tgt' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
})}
|
|
119
|
+
{/* item.name falsy, source has no display_name → uses source.name */}
|
|
120
|
+
{content({
|
|
121
|
+
active: true,
|
|
122
|
+
payload: [
|
|
123
|
+
{
|
|
124
|
+
payload: {
|
|
125
|
+
source: { name: 'src.node' },
|
|
126
|
+
target: { display_name: 'Target Display' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ResizeObserver fires callback immediately so containerWidth gets set to 800
|
|
138
|
+
beforeAll(() => {
|
|
139
|
+
global.ResizeObserver = function (callback) {
|
|
140
|
+
return {
|
|
141
|
+
observe: function (el) {
|
|
142
|
+
callback([{ contentRect: { width: 800 } }]);
|
|
143
|
+
},
|
|
144
|
+
disconnect: function () {},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
HTMLCanvasElement.prototype.getContext = () => ({
|
|
148
|
+
font: '',
|
|
149
|
+
measureText: () => ({ width: 50 }),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('<NodeDataFlowTab />', () => {
|
|
154
|
+
const mockDjClient = {
|
|
155
|
+
upstreamsGQL: jest.fn(),
|
|
156
|
+
downstreamsGQL: jest.fn(),
|
|
157
|
+
findCubesWithMetrics: jest.fn(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const renderWithContext = djNode =>
|
|
161
|
+
render(
|
|
162
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
163
|
+
<MemoryRouter>
|
|
164
|
+
<NodeDataFlowTab djNode={djNode} />
|
|
165
|
+
</MemoryRouter>
|
|
166
|
+
</DJClientContext.Provider>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
jest.clearAllMocks();
|
|
171
|
+
mockDjClient.findCubesWithMetrics.mockResolvedValue([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('stays in loading state when djNode has no name (line 186 early return)', async () => {
|
|
175
|
+
renderWithContext({});
|
|
176
|
+
// Effect returns early, loading stays true — no API calls
|
|
177
|
+
expect(mockDjClient.upstreamsGQL).not.toHaveBeenCalled();
|
|
178
|
+
expect(
|
|
179
|
+
screen.queryByText('No data flow relationships found for this node.'),
|
|
180
|
+
).not.toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('shows "No data flow relationships" when dag and downstreams return empty arrays', async () => {
|
|
184
|
+
// Use a source node — metrics get phantom links which make links.length > 0
|
|
185
|
+
const djNode = { name: 'default.source1', type: 'source', parents: [] };
|
|
186
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([]);
|
|
187
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
188
|
+
|
|
189
|
+
renderWithContext(djNode);
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(
|
|
193
|
+
screen.getByText('No data flow relationships found for this node.'),
|
|
194
|
+
).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('renders Sankey when links exist between nodes', async () => {
|
|
199
|
+
const source = {
|
|
200
|
+
name: 'default.source1',
|
|
201
|
+
type: 'source',
|
|
202
|
+
current: { parents: [] },
|
|
203
|
+
};
|
|
204
|
+
const djNode = {
|
|
205
|
+
name: 'default.metric1',
|
|
206
|
+
type: 'metric',
|
|
207
|
+
parents: [{ name: 'default.source1' }],
|
|
208
|
+
};
|
|
209
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source]);
|
|
210
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
211
|
+
|
|
212
|
+
renderWithContext(djNode);
|
|
213
|
+
|
|
214
|
+
await waitFor(() => {
|
|
215
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('handles error from Promise.all gracefully (lines 268-269)', async () => {
|
|
220
|
+
const consoleSpy = jest
|
|
221
|
+
.spyOn(console, 'error')
|
|
222
|
+
.mockImplementation(() => {});
|
|
223
|
+
const djNode = { name: 'default.metric1', type: 'metric', parents: [] };
|
|
224
|
+
mockDjClient.upstreamsGQL.mockRejectedValue(new Error('Network error'));
|
|
225
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
226
|
+
|
|
227
|
+
renderWithContext(djNode);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error));
|
|
231
|
+
});
|
|
232
|
+
expect(
|
|
233
|
+
screen.getByText('No data flow relationships found for this node.'),
|
|
234
|
+
).toBeInTheDocument();
|
|
235
|
+
consoleSpy.mockRestore();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('filters CUBE type from downstream nodes and calls findCubesWithMetrics (lines 195-196)', async () => {
|
|
239
|
+
const djNode = {
|
|
240
|
+
name: 'default.metric1',
|
|
241
|
+
type: 'metric',
|
|
242
|
+
parents: [],
|
|
243
|
+
};
|
|
244
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([]);
|
|
245
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([
|
|
246
|
+
{
|
|
247
|
+
name: 'default.cube1',
|
|
248
|
+
type: 'CUBE',
|
|
249
|
+
current: { parents: [{ name: 'default.metric1' }] },
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'default.transform1',
|
|
253
|
+
type: 'TRANSFORM',
|
|
254
|
+
current: { parents: [{ name: 'default.metric1' }] },
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
// cube has metric as parent → creates a link
|
|
258
|
+
mockDjClient.findCubesWithMetrics.mockResolvedValue([
|
|
259
|
+
{
|
|
260
|
+
name: 'default.cube1',
|
|
261
|
+
type: 'cube',
|
|
262
|
+
parents: [{ name: 'default.metric1' }],
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
renderWithContext(djNode);
|
|
267
|
+
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(mockDjClient.findCubesWithMetrics).toHaveBeenCalledWith([
|
|
270
|
+
'default.cube1',
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('covers sort return 0 branch for same-type non-seed nodes (line 220)', async () => {
|
|
280
|
+
// Two source nodes, neither is the seed (metric1) — sort returns 0
|
|
281
|
+
const source1 = {
|
|
282
|
+
name: 'default.source1',
|
|
283
|
+
type: 'source',
|
|
284
|
+
current: { parents: [] },
|
|
285
|
+
};
|
|
286
|
+
const source2 = {
|
|
287
|
+
name: 'default.source2',
|
|
288
|
+
type: 'source',
|
|
289
|
+
current: { parents: [] },
|
|
290
|
+
};
|
|
291
|
+
const djNode = {
|
|
292
|
+
name: 'default.metric1',
|
|
293
|
+
type: 'metric',
|
|
294
|
+
parents: [{ name: 'default.source1' }, { name: 'default.source2' }],
|
|
295
|
+
};
|
|
296
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source1, source2]);
|
|
297
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
298
|
+
|
|
299
|
+
renderWithContext(djNode);
|
|
300
|
+
|
|
301
|
+
await waitFor(() => {
|
|
302
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('covers SankeyLink hover (linkHovered=true) via mouseEnter on path', async () => {
|
|
307
|
+
const source = {
|
|
308
|
+
name: 'default.source1',
|
|
309
|
+
type: 'source',
|
|
310
|
+
current: { parents: [] },
|
|
311
|
+
};
|
|
312
|
+
const djNode = {
|
|
313
|
+
name: 'default.metric1',
|
|
314
|
+
type: 'metric',
|
|
315
|
+
parents: [{ name: 'default.source1' }],
|
|
316
|
+
};
|
|
317
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source]);
|
|
318
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
319
|
+
|
|
320
|
+
const { container } = renderWithContext(djNode);
|
|
321
|
+
|
|
322
|
+
await waitFor(() => {
|
|
323
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Trigger linkHovered=true to cover the 0.85 opacity branch (line 137)
|
|
327
|
+
const path = container.querySelector('path');
|
|
328
|
+
if (path) {
|
|
329
|
+
fireEvent.mouseEnter(path);
|
|
330
|
+
fireEvent.mouseLeave(path);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('covers ResizeObserver callback setting containerWidth (line 179)', async () => {
|
|
335
|
+
const djNode = {
|
|
336
|
+
name: 'default.metric1',
|
|
337
|
+
type: 'metric',
|
|
338
|
+
parents: [{ name: 'default.source1' }],
|
|
339
|
+
};
|
|
340
|
+
const source = {
|
|
341
|
+
name: 'default.source1',
|
|
342
|
+
type: 'source',
|
|
343
|
+
current: { parents: [] },
|
|
344
|
+
};
|
|
345
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source]);
|
|
346
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
347
|
+
|
|
348
|
+
renderWithContext(djNode);
|
|
349
|
+
|
|
350
|
+
// ResizeObserver mock fires immediately with width=800 → containerWidth>0 → Sankey renders
|
|
351
|
+
await waitFor(() => {
|
|
352
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('renders SankeyNode with rightmostType=metric when no cubes', async () => {
|
|
357
|
+
const source = {
|
|
358
|
+
name: 'default.source1',
|
|
359
|
+
type: 'source',
|
|
360
|
+
current: { parents: [] },
|
|
361
|
+
};
|
|
362
|
+
const transform = {
|
|
363
|
+
name: 'default.transform1',
|
|
364
|
+
type: 'transform',
|
|
365
|
+
current: { parents: [{ name: 'default.source1' }] },
|
|
366
|
+
};
|
|
367
|
+
const djNode = {
|
|
368
|
+
name: 'default.metric1',
|
|
369
|
+
type: 'metric',
|
|
370
|
+
parents: [{ name: 'default.transform1' }],
|
|
371
|
+
};
|
|
372
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source, transform]);
|
|
373
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
374
|
+
|
|
375
|
+
renderWithContext(djNode);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('includes dimension nodes in the flow graph (they have data lineage like transforms)', async () => {
|
|
383
|
+
const dimNode = {
|
|
384
|
+
name: 'default.dim1',
|
|
385
|
+
type: 'dimension',
|
|
386
|
+
current: { parents: [{ name: 'default.source1' }] },
|
|
387
|
+
};
|
|
388
|
+
const djNode = {
|
|
389
|
+
name: 'default.metric1',
|
|
390
|
+
type: 'metric',
|
|
391
|
+
parents: [{ name: 'default.source1' }],
|
|
392
|
+
};
|
|
393
|
+
const source = {
|
|
394
|
+
name: 'default.source1',
|
|
395
|
+
type: 'source',
|
|
396
|
+
current: { parents: [] },
|
|
397
|
+
};
|
|
398
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([dimNode, source]);
|
|
399
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
400
|
+
|
|
401
|
+
renderWithContext(djNode);
|
|
402
|
+
|
|
403
|
+
await waitFor(() => {
|
|
404
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('shows data flow for a dimension node viewed directly', async () => {
|
|
409
|
+
const djNode = {
|
|
410
|
+
name: 'default.dispatcher',
|
|
411
|
+
type: 'dimension',
|
|
412
|
+
parents: [{ name: 'default.dispatchers_source' }],
|
|
413
|
+
};
|
|
414
|
+
const source = {
|
|
415
|
+
name: 'default.dispatchers_source',
|
|
416
|
+
type: 'source',
|
|
417
|
+
current: { parents: [] },
|
|
418
|
+
};
|
|
419
|
+
mockDjClient.upstreamsGQL.mockResolvedValue([source]);
|
|
420
|
+
mockDjClient.downstreamsGQL.mockResolvedValue([]);
|
|
421
|
+
|
|
422
|
+
renderWithContext(djNode);
|
|
423
|
+
|
|
424
|
+
await waitFor(() => {
|
|
425
|
+
expect(screen.getByTestId('sankey')).toBeInTheDocument();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, waitFor, screen } from '@testing-library/react';
|
|
3
|
-
import NodeDependenciesTab from '../NodeDependenciesTab';
|
|
3
|
+
import NodeDependenciesTab, { NodeList } from '../NodeDependenciesTab';
|
|
4
4
|
|
|
5
5
|
describe('<NodeDependenciesTab />', () => {
|
|
6
6
|
const mockDjClient = {
|
|
@@ -135,6 +135,23 @@ describe('<NodeDependenciesTab />', () => {
|
|
|
135
135
|
mockDjClient.downstreamsGQL.mockReset();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
it('skips fetching when node is null (line 12 false branch)', () => {
|
|
139
|
+
render(<NodeDependenciesTab node={null} djClient={mockDjClient} />);
|
|
140
|
+
expect(mockDjClient.upstreamsGQL).not.toHaveBeenCalled();
|
|
141
|
+
expect(mockDjClient.downstreamsGQL).not.toHaveBeenCalled();
|
|
142
|
+
expect(mockDjClient.nodeDimensions).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('renders "None" for NodeList with empty array (lines 127-131)', () => {
|
|
146
|
+
render(<NodeList nodes={[]} />);
|
|
147
|
+
expect(screen.getByText('None')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders "None" for NodeList with null nodes', () => {
|
|
151
|
+
render(<NodeList nodes={null} />);
|
|
152
|
+
expect(screen.getByText('None')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
138
155
|
it('renders nodes with dimensions', async () => {
|
|
139
156
|
// Use mockResolvedValue since the component now uses .then()
|
|
140
157
|
mockDjClient.nodeDimensions.mockResolvedValue(mockDimensions);
|