datajunction-ui 0.0.74 → 0.0.76
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/DashboardCard.jsx +93 -0
- package/src/app/components/NodeComponents.jsx +173 -0
- package/src/app/components/NodeListActions.jsx +8 -3
- package/src/app/components/__tests__/NodeComponents.test.jsx +262 -0
- package/src/app/hooks/__tests__/useWorkspaceData.test.js +533 -0
- package/src/app/hooks/useWorkspaceData.js +357 -0
- package/src/app/index.tsx +6 -0
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +344 -0
- package/src/app/pages/MyWorkspacePage/CollectionsSection.jsx +188 -0
- package/src/app/pages/MyWorkspacePage/Loadable.jsx +6 -0
- package/src/app/pages/MyWorkspacePage/MaterializationsSection.jsx +190 -0
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +342 -0
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +632 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +185 -0
- package/src/app/pages/MyWorkspacePage/NodeList.jsx +46 -0
- package/src/app/pages/MyWorkspacePage/NotificationsSection.jsx +133 -0
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +209 -0
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +295 -0
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +278 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +238 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +389 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +347 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +272 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NodeList.test.jsx +162 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +204 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +556 -0
- package/src/app/pages/MyWorkspacePage/index.jsx +150 -0
- package/src/app/services/DJService.js +323 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { MyWorkspacePage } from '../index';
|
|
5
|
+
import * as useWorkspaceData from '../../../hooks/useWorkspaceData';
|
|
6
|
+
|
|
7
|
+
// Mock CSS imports
|
|
8
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
9
|
+
jest.mock('../../../../styles/settings.css', () => ({}));
|
|
10
|
+
|
|
11
|
+
// Mock child components to test integration
|
|
12
|
+
jest.mock('../NotificationsSection', () => ({
|
|
13
|
+
NotificationsSection: ({ notifications, loading }) => (
|
|
14
|
+
<div data-testid="notifications-section">
|
|
15
|
+
{loading ? 'Loading...' : `${notifications.length} notifications`}
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('../NeedsAttentionSection', () => ({
|
|
21
|
+
NeedsAttentionSection: ({ loading }) => (
|
|
22
|
+
<div data-testid="needs-attention-section">
|
|
23
|
+
{loading ? 'Loading...' : 'Needs Attention'}
|
|
24
|
+
</div>
|
|
25
|
+
),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('../MyNodesSection', () => ({
|
|
29
|
+
MyNodesSection: ({ ownedNodes, loading }) => (
|
|
30
|
+
<div data-testid="my-nodes-section">
|
|
31
|
+
{loading ? 'Loading...' : `${ownedNodes.length} owned nodes`}
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
jest.mock('../CollectionsSection', () => ({
|
|
37
|
+
CollectionsSection: ({ collections, loading }) => (
|
|
38
|
+
<div data-testid="collections-section">
|
|
39
|
+
{loading ? 'Loading...' : `${collections.length} collections`}
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
jest.mock('../MaterializationsSection', () => ({
|
|
45
|
+
MaterializationsSection: ({ nodes, loading }) => (
|
|
46
|
+
<div data-testid="materializations-section">
|
|
47
|
+
{loading ? 'Loading...' : `${nodes.length} materializations`}
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
jest.mock('../ActiveBranchesSection', () => ({
|
|
53
|
+
ActiveBranchesSection: ({ loading }) => (
|
|
54
|
+
<div data-testid="active-branches-section">
|
|
55
|
+
{loading ? 'Loading...' : 'Active Branches'}
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
describe('<MyWorkspacePage />', () => {
|
|
61
|
+
const mockCurrentUser = {
|
|
62
|
+
username: 'test.user@example.com',
|
|
63
|
+
email: 'test.user@example.com',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const mockOwnedNodes = [
|
|
67
|
+
{
|
|
68
|
+
name: 'default.test_metric',
|
|
69
|
+
type: 'METRIC',
|
|
70
|
+
current: { displayName: 'Test Metric', updatedAt: '2024-01-01' },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'default.test_dimension',
|
|
74
|
+
type: 'DIMENSION',
|
|
75
|
+
current: { displayName: 'Test Dimension', updatedAt: '2024-01-02' },
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const mockCollections = [
|
|
80
|
+
{
|
|
81
|
+
name: 'test_collection',
|
|
82
|
+
description: 'Test Collection',
|
|
83
|
+
nodeCount: 5,
|
|
84
|
+
createdBy: 'test.user@example.com',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const mockNotifications = [
|
|
89
|
+
{
|
|
90
|
+
entity_name: 'default.test_metric',
|
|
91
|
+
entity_type: 'node',
|
|
92
|
+
activity_type: 'update',
|
|
93
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
94
|
+
user: 'test.user@example.com',
|
|
95
|
+
node_type: 'metric',
|
|
96
|
+
display_name: 'Test Metric',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const mockMaterializations = [
|
|
101
|
+
{
|
|
102
|
+
name: 'default.test_cube',
|
|
103
|
+
type: 'CUBE',
|
|
104
|
+
current: {
|
|
105
|
+
displayName: 'Test Cube',
|
|
106
|
+
availability: {
|
|
107
|
+
validThroughTs: Date.now() - 1000 * 60 * 60, // 1 hour ago
|
|
108
|
+
},
|
|
109
|
+
materializations: [{ name: 'mat1', schedule: '@daily' }],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const mockNeedsAttention = {
|
|
115
|
+
nodesMissingDescription: [],
|
|
116
|
+
invalidNodes: [],
|
|
117
|
+
staleDrafts: [],
|
|
118
|
+
orphanedDimensions: [],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
jest.clearAllMocks();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should render loading state', () => {
|
|
126
|
+
jest.spyOn(useWorkspaceData, 'useCurrentUser').mockReturnValue({
|
|
127
|
+
data: null,
|
|
128
|
+
loading: true,
|
|
129
|
+
error: null,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
render(
|
|
133
|
+
<MemoryRouter>
|
|
134
|
+
<MyWorkspacePage />
|
|
135
|
+
</MemoryRouter>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
139
|
+
// Should show loading icon when user is loading
|
|
140
|
+
expect(screen.queryByTestId('collections-section')).not.toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should render all sections when data is loaded', () => {
|
|
144
|
+
// Mock all hooks with data
|
|
145
|
+
jest.spyOn(useWorkspaceData, 'useCurrentUser').mockReturnValue({
|
|
146
|
+
data: mockCurrentUser,
|
|
147
|
+
loading: false,
|
|
148
|
+
error: null,
|
|
149
|
+
});
|
|
150
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceOwnedNodes').mockReturnValue({
|
|
151
|
+
data: mockOwnedNodes,
|
|
152
|
+
loading: false,
|
|
153
|
+
error: null,
|
|
154
|
+
});
|
|
155
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceRecentlyEdited').mockReturnValue({
|
|
156
|
+
data: [],
|
|
157
|
+
loading: false,
|
|
158
|
+
error: null,
|
|
159
|
+
});
|
|
160
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceWatchedNodes').mockReturnValue({
|
|
161
|
+
data: [],
|
|
162
|
+
loading: false,
|
|
163
|
+
error: null,
|
|
164
|
+
});
|
|
165
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceCollections').mockReturnValue({
|
|
166
|
+
data: mockCollections,
|
|
167
|
+
loading: false,
|
|
168
|
+
error: null,
|
|
169
|
+
});
|
|
170
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNotifications').mockReturnValue({
|
|
171
|
+
data: mockNotifications,
|
|
172
|
+
loading: false,
|
|
173
|
+
error: null,
|
|
174
|
+
});
|
|
175
|
+
jest
|
|
176
|
+
.spyOn(useWorkspaceData, 'useWorkspaceMaterializations')
|
|
177
|
+
.mockReturnValue({
|
|
178
|
+
data: mockMaterializations,
|
|
179
|
+
loading: false,
|
|
180
|
+
error: null,
|
|
181
|
+
});
|
|
182
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNeedsAttention').mockReturnValue({
|
|
183
|
+
data: mockNeedsAttention,
|
|
184
|
+
loading: false,
|
|
185
|
+
error: null,
|
|
186
|
+
});
|
|
187
|
+
jest.spyOn(useWorkspaceData, 'usePersonalNamespace').mockReturnValue({
|
|
188
|
+
exists: true,
|
|
189
|
+
loading: false,
|
|
190
|
+
error: null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
render(
|
|
194
|
+
<MemoryRouter>
|
|
195
|
+
<MyWorkspacePage />
|
|
196
|
+
</MemoryRouter>,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Check all sections are rendered
|
|
200
|
+
expect(screen.getByTestId('collections-section')).toHaveTextContent(
|
|
201
|
+
'1 collections',
|
|
202
|
+
);
|
|
203
|
+
expect(screen.getByTestId('my-nodes-section')).toHaveTextContent(
|
|
204
|
+
'2 owned nodes',
|
|
205
|
+
);
|
|
206
|
+
expect(screen.getByTestId('notifications-section')).toHaveTextContent(
|
|
207
|
+
'1 notifications',
|
|
208
|
+
);
|
|
209
|
+
expect(screen.getByTestId('materializations-section')).toHaveTextContent(
|
|
210
|
+
'1 materializations',
|
|
211
|
+
);
|
|
212
|
+
expect(screen.getByTestId('needs-attention-section')).toBeInTheDocument();
|
|
213
|
+
expect(screen.getByTestId('active-branches-section')).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should pass correct props to sections', () => {
|
|
217
|
+
jest.spyOn(useWorkspaceData, 'useCurrentUser').mockReturnValue({
|
|
218
|
+
data: mockCurrentUser,
|
|
219
|
+
loading: false,
|
|
220
|
+
error: null,
|
|
221
|
+
});
|
|
222
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceOwnedNodes').mockReturnValue({
|
|
223
|
+
data: mockOwnedNodes,
|
|
224
|
+
loading: false,
|
|
225
|
+
error: null,
|
|
226
|
+
});
|
|
227
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceRecentlyEdited').mockReturnValue({
|
|
228
|
+
data: [],
|
|
229
|
+
loading: false,
|
|
230
|
+
error: null,
|
|
231
|
+
});
|
|
232
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceWatchedNodes').mockReturnValue({
|
|
233
|
+
data: [],
|
|
234
|
+
loading: false,
|
|
235
|
+
error: null,
|
|
236
|
+
});
|
|
237
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceCollections').mockReturnValue({
|
|
238
|
+
data: mockCollections,
|
|
239
|
+
loading: false,
|
|
240
|
+
error: null,
|
|
241
|
+
});
|
|
242
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNotifications').mockReturnValue({
|
|
243
|
+
data: mockNotifications,
|
|
244
|
+
loading: false,
|
|
245
|
+
error: null,
|
|
246
|
+
});
|
|
247
|
+
jest
|
|
248
|
+
.spyOn(useWorkspaceData, 'useWorkspaceMaterializations')
|
|
249
|
+
.mockReturnValue({
|
|
250
|
+
data: mockMaterializations,
|
|
251
|
+
loading: false,
|
|
252
|
+
error: null,
|
|
253
|
+
});
|
|
254
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNeedsAttention').mockReturnValue({
|
|
255
|
+
data: mockNeedsAttention,
|
|
256
|
+
loading: false,
|
|
257
|
+
error: null,
|
|
258
|
+
});
|
|
259
|
+
jest.spyOn(useWorkspaceData, 'usePersonalNamespace').mockReturnValue({
|
|
260
|
+
exists: true,
|
|
261
|
+
loading: false,
|
|
262
|
+
error: null,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
render(
|
|
266
|
+
<MemoryRouter>
|
|
267
|
+
<MyWorkspacePage />
|
|
268
|
+
</MemoryRouter>,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Verify sections receive correct data
|
|
272
|
+
expect(screen.getByTestId('collections-section')).toBeInTheDocument();
|
|
273
|
+
expect(screen.getByTestId('my-nodes-section')).toBeInTheDocument();
|
|
274
|
+
expect(screen.getByTestId('notifications-section')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should calculate stale materializations correctly', () => {
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
const staleNode = {
|
|
280
|
+
name: 'default.stale_cube',
|
|
281
|
+
type: 'CUBE',
|
|
282
|
+
current: {
|
|
283
|
+
displayName: 'Stale Cube',
|
|
284
|
+
availability: {
|
|
285
|
+
validThroughTs: now - 1000 * 60 * 60 * 80, // 80 hours ago (> 72 hours)
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
jest.spyOn(useWorkspaceData, 'useCurrentUser').mockReturnValue({
|
|
291
|
+
data: mockCurrentUser,
|
|
292
|
+
loading: false,
|
|
293
|
+
error: null,
|
|
294
|
+
});
|
|
295
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceOwnedNodes').mockReturnValue({
|
|
296
|
+
data: [],
|
|
297
|
+
loading: false,
|
|
298
|
+
error: null,
|
|
299
|
+
});
|
|
300
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceRecentlyEdited').mockReturnValue({
|
|
301
|
+
data: [],
|
|
302
|
+
loading: false,
|
|
303
|
+
error: null,
|
|
304
|
+
});
|
|
305
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceWatchedNodes').mockReturnValue({
|
|
306
|
+
data: [],
|
|
307
|
+
loading: false,
|
|
308
|
+
error: null,
|
|
309
|
+
});
|
|
310
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceCollections').mockReturnValue({
|
|
311
|
+
data: [],
|
|
312
|
+
loading: false,
|
|
313
|
+
error: null,
|
|
314
|
+
});
|
|
315
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNotifications').mockReturnValue({
|
|
316
|
+
data: [],
|
|
317
|
+
loading: false,
|
|
318
|
+
error: null,
|
|
319
|
+
});
|
|
320
|
+
jest
|
|
321
|
+
.spyOn(useWorkspaceData, 'useWorkspaceMaterializations')
|
|
322
|
+
.mockReturnValue({
|
|
323
|
+
data: [staleNode],
|
|
324
|
+
loading: false,
|
|
325
|
+
error: null,
|
|
326
|
+
});
|
|
327
|
+
jest.spyOn(useWorkspaceData, 'useWorkspaceNeedsAttention').mockReturnValue({
|
|
328
|
+
data: mockNeedsAttention,
|
|
329
|
+
loading: false,
|
|
330
|
+
error: null,
|
|
331
|
+
});
|
|
332
|
+
jest.spyOn(useWorkspaceData, 'usePersonalNamespace').mockReturnValue({
|
|
333
|
+
exists: true,
|
|
334
|
+
loading: false,
|
|
335
|
+
error: null,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
render(
|
|
339
|
+
<MemoryRouter>
|
|
340
|
+
<MyWorkspacePage />
|
|
341
|
+
</MemoryRouter>,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// The stale materialization should be passed to NeedsAttentionSection
|
|
345
|
+
expect(screen.getByTestId('needs-attention-section')).toBeInTheDocument();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { NeedsAttentionSection } from '../NeedsAttentionSection';
|
|
5
|
+
|
|
6
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
7
|
+
|
|
8
|
+
describe('<NeedsAttentionSection />', () => {
|
|
9
|
+
const defaultProps = {
|
|
10
|
+
nodesMissingDescription: [],
|
|
11
|
+
invalidNodes: [],
|
|
12
|
+
staleDrafts: [],
|
|
13
|
+
staleMaterializations: [],
|
|
14
|
+
orphanedDimensions: [],
|
|
15
|
+
username: 'test.user@example.com',
|
|
16
|
+
hasItems: false,
|
|
17
|
+
loading: false,
|
|
18
|
+
personalNamespace: 'users.test.user',
|
|
19
|
+
hasPersonalNamespace: true,
|
|
20
|
+
namespaceLoading: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it('should render loading state', () => {
|
|
24
|
+
render(
|
|
25
|
+
<MemoryRouter>
|
|
26
|
+
<NeedsAttentionSection {...defaultProps} loading={true} />
|
|
27
|
+
</MemoryRouter>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(screen.queryByText('✓ All good!')).not.toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render all categories', () => {
|
|
34
|
+
render(
|
|
35
|
+
<MemoryRouter>
|
|
36
|
+
<NeedsAttentionSection {...defaultProps} />
|
|
37
|
+
</MemoryRouter>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(screen.getByText(/Invalid/)).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText(/Stale Drafts/)).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText(/Stale Materializations/)).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText(/No Description/)).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText(/Orphaned Dimensions/)).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should show "All good!" when category has no items', () => {
|
|
48
|
+
render(
|
|
49
|
+
<MemoryRouter>
|
|
50
|
+
<NeedsAttentionSection {...defaultProps} />
|
|
51
|
+
</MemoryRouter>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const allGood = screen.getAllByText('✓ All good!');
|
|
55
|
+
expect(allGood.length).toBe(5); // All 5 categories should show "All good!"
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should display invalid nodes', () => {
|
|
59
|
+
const invalidNodes = [
|
|
60
|
+
{
|
|
61
|
+
name: 'default.invalid_metric',
|
|
62
|
+
type: 'METRIC',
|
|
63
|
+
current: { displayName: 'Invalid Metric' },
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<MemoryRouter>
|
|
69
|
+
<NeedsAttentionSection
|
|
70
|
+
{...defaultProps}
|
|
71
|
+
invalidNodes={invalidNodes}
|
|
72
|
+
hasItems={true}
|
|
73
|
+
/>
|
|
74
|
+
</MemoryRouter>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText('❌ Invalid')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('(1)')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should display stale drafts', () => {
|
|
82
|
+
const staleDrafts = [
|
|
83
|
+
{
|
|
84
|
+
name: 'default.stale_draft',
|
|
85
|
+
type: 'METRIC',
|
|
86
|
+
current: { displayName: 'Stale Draft' },
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<MemoryRouter>
|
|
92
|
+
<NeedsAttentionSection
|
|
93
|
+
{...defaultProps}
|
|
94
|
+
staleDrafts={staleDrafts}
|
|
95
|
+
hasItems={true}
|
|
96
|
+
/>
|
|
97
|
+
</MemoryRouter>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('⏰ Stale Drafts')).toBeInTheDocument();
|
|
101
|
+
expect(screen.getByText('(1)')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should display nodes missing description', () => {
|
|
105
|
+
const nodesMissingDescription = [
|
|
106
|
+
{
|
|
107
|
+
name: 'default.no_desc_metric',
|
|
108
|
+
type: 'METRIC',
|
|
109
|
+
current: { displayName: 'No Description Metric' },
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
render(
|
|
114
|
+
<MemoryRouter>
|
|
115
|
+
<NeedsAttentionSection
|
|
116
|
+
{...defaultProps}
|
|
117
|
+
nodesMissingDescription={nodesMissingDescription}
|
|
118
|
+
hasItems={true}
|
|
119
|
+
/>
|
|
120
|
+
</MemoryRouter>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('📝 No Description')).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText('(1)')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should show "View all" link for categories with items', () => {
|
|
128
|
+
const invalidNodes = [
|
|
129
|
+
{
|
|
130
|
+
name: 'default.invalid_metric',
|
|
131
|
+
type: 'METRIC',
|
|
132
|
+
current: { displayName: 'Invalid Metric' },
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<MemoryRouter>
|
|
138
|
+
<NeedsAttentionSection
|
|
139
|
+
{...defaultProps}
|
|
140
|
+
invalidNodes={invalidNodes}
|
|
141
|
+
hasItems={true}
|
|
142
|
+
/>
|
|
143
|
+
</MemoryRouter>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const viewAllLinks = screen.getAllByText('View all →');
|
|
147
|
+
expect(viewAllLinks.length).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not show "View all" link for empty categories', () => {
|
|
151
|
+
render(
|
|
152
|
+
<MemoryRouter>
|
|
153
|
+
<NeedsAttentionSection {...defaultProps} />
|
|
154
|
+
</MemoryRouter>,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(screen.queryByText('View all →')).not.toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should limit nodes to 10 per category', () => {
|
|
161
|
+
const manyNodes = Array.from({ length: 15 }, (_, i) => ({
|
|
162
|
+
name: `default.node_${i}`,
|
|
163
|
+
type: 'METRIC',
|
|
164
|
+
current: { displayName: `Node ${i}` },
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
render(
|
|
168
|
+
<MemoryRouter>
|
|
169
|
+
<NeedsAttentionSection
|
|
170
|
+
{...defaultProps}
|
|
171
|
+
invalidNodes={manyNodes}
|
|
172
|
+
hasItems={true}
|
|
173
|
+
/>
|
|
174
|
+
</MemoryRouter>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Should only display 10 nodes (via NodeChip component)
|
|
178
|
+
// The 11th node should not be displayed
|
|
179
|
+
expect(screen.getByText('❌ Invalid')).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByText('(15)')).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should show personal namespace prompt when namespace does not exist', () => {
|
|
184
|
+
render(
|
|
185
|
+
<MemoryRouter>
|
|
186
|
+
<NeedsAttentionSection {...defaultProps} hasPersonalNamespace={false} />
|
|
187
|
+
</MemoryRouter>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(screen.getByText('Set up your namespace')).toBeInTheDocument();
|
|
191
|
+
expect(screen.getByText('users.test.user')).toBeInTheDocument();
|
|
192
|
+
expect(screen.getByText('Create →')).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should not show personal namespace prompt when namespace exists', () => {
|
|
196
|
+
render(
|
|
197
|
+
<MemoryRouter>
|
|
198
|
+
<NeedsAttentionSection {...defaultProps} hasPersonalNamespace={true} />
|
|
199
|
+
</MemoryRouter>,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(screen.queryByText('Set up your namespace')).not.toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not show personal namespace prompt when loading', () => {
|
|
206
|
+
render(
|
|
207
|
+
<MemoryRouter>
|
|
208
|
+
<NeedsAttentionSection
|
|
209
|
+
{...defaultProps}
|
|
210
|
+
hasPersonalNamespace={false}
|
|
211
|
+
namespaceLoading={true}
|
|
212
|
+
/>
|
|
213
|
+
</MemoryRouter>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(screen.queryByText('Set up your namespace')).not.toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should link to correct filter URLs', () => {
|
|
220
|
+
const invalidNodes = [
|
|
221
|
+
{
|
|
222
|
+
name: 'default.invalid_metric',
|
|
223
|
+
type: 'METRIC',
|
|
224
|
+
current: { displayName: 'Invalid Metric' },
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
render(
|
|
229
|
+
<MemoryRouter>
|
|
230
|
+
<NeedsAttentionSection
|
|
231
|
+
{...defaultProps}
|
|
232
|
+
invalidNodes={invalidNodes}
|
|
233
|
+
hasItems={true}
|
|
234
|
+
/>
|
|
235
|
+
</MemoryRouter>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const viewAllLink = screen.getByText('View all →').closest('a');
|
|
239
|
+
expect(viewAllLink).toHaveAttribute(
|
|
240
|
+
'href',
|
|
241
|
+
'/?ownedBy=test.user@example.com&statuses=INVALID',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should display correct count for each category', () => {
|
|
246
|
+
render(
|
|
247
|
+
<MemoryRouter>
|
|
248
|
+
<NeedsAttentionSection
|
|
249
|
+
{...defaultProps}
|
|
250
|
+
invalidNodes={[{ name: 'node1', type: 'METRIC', current: {} }]}
|
|
251
|
+
staleDrafts={[
|
|
252
|
+
{ name: 'node2', type: 'METRIC', current: {} },
|
|
253
|
+
{ name: 'node3', type: 'METRIC', current: {} },
|
|
254
|
+
]}
|
|
255
|
+
nodesMissingDescription={[
|
|
256
|
+
{ name: 'node4', type: 'METRIC', current: {} },
|
|
257
|
+
{ name: 'node5', type: 'METRIC', current: {} },
|
|
258
|
+
{ name: 'node6', type: 'METRIC', current: {} },
|
|
259
|
+
]}
|
|
260
|
+
hasItems={true}
|
|
261
|
+
/>
|
|
262
|
+
</MemoryRouter>,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(screen.getByText('❌ Invalid')).toBeInTheDocument();
|
|
266
|
+
expect(screen.getByText('(1)')).toBeInTheDocument();
|
|
267
|
+
expect(screen.getByText('⏰ Stale Drafts')).toBeInTheDocument();
|
|
268
|
+
expect(screen.getByText('(2)')).toBeInTheDocument();
|
|
269
|
+
expect(screen.getByText('📝 No Description')).toBeInTheDocument();
|
|
270
|
+
expect(screen.getByText('(3)')).toBeInTheDocument();
|
|
271
|
+
});
|
|
272
|
+
});
|