datajunction-ui 0.0.75 → 0.0.77
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,295 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { ActiveBranchesSection } from '../ActiveBranchesSection';
|
|
5
|
+
import DJClientContext from '../../../providers/djclient';
|
|
6
|
+
|
|
7
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
8
|
+
jest.mock('../../../utils/date', () => ({
|
|
9
|
+
formatRelativeTime: () => '2d ago',
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('<ActiveBranchesSection />', () => {
|
|
13
|
+
const mockDjClient = {
|
|
14
|
+
listNodesForLanding: jest.fn().mockResolvedValue({
|
|
15
|
+
data: { findNodesPaginated: { edges: [] } },
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const renderWithContext = props => {
|
|
20
|
+
return render(
|
|
21
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
22
|
+
<MemoryRouter>
|
|
23
|
+
<ActiveBranchesSection {...props} />
|
|
24
|
+
</MemoryRouter>
|
|
25
|
+
</DJClientContext.Provider>,
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const mockOwnedNodes = [
|
|
30
|
+
{
|
|
31
|
+
name: 'myproject.main.users',
|
|
32
|
+
gitInfo: {
|
|
33
|
+
repo: 'myorg/myrepo',
|
|
34
|
+
branch: 'main',
|
|
35
|
+
defaultBranch: 'main',
|
|
36
|
+
parentNamespace: 'myproject',
|
|
37
|
+
},
|
|
38
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'myproject.feature-1.orders',
|
|
42
|
+
gitInfo: {
|
|
43
|
+
repo: 'myorg/myrepo',
|
|
44
|
+
branch: 'feature-1',
|
|
45
|
+
defaultBranch: 'main',
|
|
46
|
+
parentNamespace: 'myproject',
|
|
47
|
+
},
|
|
48
|
+
current: { updatedAt: '2024-01-02T10:00:00Z' },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'otherproject.main.products',
|
|
52
|
+
gitInfo: {
|
|
53
|
+
repo: 'myorg/otherrepo',
|
|
54
|
+
branch: 'main',
|
|
55
|
+
defaultBranch: 'main',
|
|
56
|
+
parentNamespace: 'otherproject',
|
|
57
|
+
},
|
|
58
|
+
current: { updatedAt: '2024-01-03T10:00:00Z' },
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const mockRecentlyEdited = [
|
|
63
|
+
{
|
|
64
|
+
name: 'myproject.feature-1.orders',
|
|
65
|
+
gitInfo: {
|
|
66
|
+
repo: 'myorg/myrepo',
|
|
67
|
+
branch: 'feature-1',
|
|
68
|
+
defaultBranch: 'main',
|
|
69
|
+
parentNamespace: 'myproject',
|
|
70
|
+
},
|
|
71
|
+
current: { updatedAt: '2024-01-02T10:00:00Z' },
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
it('should render loading state', () => {
|
|
76
|
+
renderWithContext({
|
|
77
|
+
ownedNodes: [],
|
|
78
|
+
recentlyEdited: [],
|
|
79
|
+
loading: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText('Git Namespaces')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should render empty state when no branches', async () => {
|
|
86
|
+
renderWithContext({
|
|
87
|
+
ownedNodes: [],
|
|
88
|
+
recentlyEdited: [],
|
|
89
|
+
loading: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(
|
|
94
|
+
screen.getByText('No git-managed namespaces.'),
|
|
95
|
+
).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should group nodes by base namespace', async () => {
|
|
100
|
+
renderWithContext({
|
|
101
|
+
ownedNodes: mockOwnedNodes,
|
|
102
|
+
recentlyEdited: mockRecentlyEdited,
|
|
103
|
+
loading: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Should show both base namespaces
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(screen.getByText('myproject')).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText('otherproject')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should show repo and branch names', async () => {
|
|
114
|
+
renderWithContext({
|
|
115
|
+
ownedNodes: mockOwnedNodes,
|
|
116
|
+
recentlyEdited: mockRecentlyEdited,
|
|
117
|
+
loading: false,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Should show repos
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByText('myorg/otherrepo')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Should show branches (there can be multiple "main" branches)
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
129
|
+
expect(screen.getByText('feature-1')).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle nodes without git info', async () => {
|
|
134
|
+
const nodesWithoutGit = [
|
|
135
|
+
{
|
|
136
|
+
name: 'default.node1',
|
|
137
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
138
|
+
},
|
|
139
|
+
...mockOwnedNodes,
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
renderWithContext({
|
|
143
|
+
ownedNodes: nodesWithoutGit,
|
|
144
|
+
recentlyEdited: [],
|
|
145
|
+
loading: false,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Should still render the git-managed nodes
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(screen.getByText('myproject')).toBeInTheDocument();
|
|
151
|
+
expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should render with many namespaces without crashing', async () => {
|
|
156
|
+
const manyNamespaces = Array.from({ length: 10 }, (_, i) => ({
|
|
157
|
+
name: `project${i}.main.node`,
|
|
158
|
+
gitInfo: {
|
|
159
|
+
repo: `org/repo${i}`,
|
|
160
|
+
branch: 'main',
|
|
161
|
+
defaultBranch: 'main',
|
|
162
|
+
parentNamespace: `project${i}`,
|
|
163
|
+
},
|
|
164
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
renderWithContext({
|
|
168
|
+
ownedNodes: manyNamespaces,
|
|
169
|
+
recentlyEdited: [],
|
|
170
|
+
loading: false,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Should render component successfully
|
|
174
|
+
expect(screen.getByText('Git Namespaces')).toBeInTheDocument();
|
|
175
|
+
// Should show some of the namespaces (maxDisplay is 3)
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(screen.getByText('project0')).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle nodes without parentNamespace (fallback parsing)', async () => {
|
|
182
|
+
const nodesWithoutParent = [
|
|
183
|
+
{
|
|
184
|
+
name: 'repo1.branch1.node1',
|
|
185
|
+
gitInfo: {
|
|
186
|
+
repo: 'myorg/myrepo',
|
|
187
|
+
branch: 'branch1',
|
|
188
|
+
defaultBranch: 'main',
|
|
189
|
+
// No parentNamespace - should fallback to parsing
|
|
190
|
+
},
|
|
191
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
renderWithContext({
|
|
196
|
+
ownedNodes: nodesWithoutParent,
|
|
197
|
+
recentlyEdited: [],
|
|
198
|
+
loading: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
// Should extract "repo1" as base namespace
|
|
203
|
+
expect(screen.getByText('repo1')).toBeInTheDocument();
|
|
204
|
+
expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should track most recent activity across multiple nodes', async () => {
|
|
209
|
+
const nodesWithDifferentTimes = [
|
|
210
|
+
{
|
|
211
|
+
name: 'myproject.main.node1',
|
|
212
|
+
gitInfo: {
|
|
213
|
+
repo: 'myorg/myrepo',
|
|
214
|
+
branch: 'main',
|
|
215
|
+
defaultBranch: 'main',
|
|
216
|
+
parentNamespace: 'myproject',
|
|
217
|
+
},
|
|
218
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'myproject.main.node2',
|
|
222
|
+
gitInfo: {
|
|
223
|
+
repo: 'myorg/myrepo',
|
|
224
|
+
branch: 'main',
|
|
225
|
+
defaultBranch: 'main',
|
|
226
|
+
parentNamespace: 'myproject',
|
|
227
|
+
},
|
|
228
|
+
current: { updatedAt: '2024-01-05T10:00:00Z' }, // More recent
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
renderWithContext({
|
|
233
|
+
ownedNodes: nodesWithDifferentTimes,
|
|
234
|
+
recentlyEdited: [],
|
|
235
|
+
loading: false,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await waitFor(() => {
|
|
239
|
+
expect(screen.getByText('myproject')).toBeInTheDocument();
|
|
240
|
+
// Should show the more recent update time
|
|
241
|
+
expect(screen.getByText(/updated 2d ago/)).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle API errors when fetching counts', async () => {
|
|
246
|
+
const consoleErrorSpy = jest
|
|
247
|
+
.spyOn(console, 'error')
|
|
248
|
+
.mockImplementation(() => {});
|
|
249
|
+
|
|
250
|
+
mockDjClient.listNodesForLanding.mockRejectedValueOnce(
|
|
251
|
+
new Error('API error'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
renderWithContext({
|
|
255
|
+
ownedNodes: mockOwnedNodes,
|
|
256
|
+
recentlyEdited: [],
|
|
257
|
+
loading: false,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await waitFor(() => {
|
|
261
|
+
expect(screen.getByText('myproject')).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Should still render despite API error
|
|
265
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
266
|
+
consoleErrorSpy.mockRestore();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should show default branch even when user has no nodes on it', async () => {
|
|
270
|
+
const nodesOnlyFeatureBranch = [
|
|
271
|
+
{
|
|
272
|
+
name: 'myproject.feature.node1',
|
|
273
|
+
gitInfo: {
|
|
274
|
+
repo: 'myorg/myrepo',
|
|
275
|
+
branch: 'feature',
|
|
276
|
+
defaultBranch: 'main',
|
|
277
|
+
parentNamespace: 'myproject',
|
|
278
|
+
},
|
|
279
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
renderWithContext({
|
|
284
|
+
ownedNodes: nodesOnlyFeatureBranch,
|
|
285
|
+
recentlyEdited: [],
|
|
286
|
+
loading: false,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
// Should show both the feature branch and the default main branch
|
|
291
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
292
|
+
expect(screen.getByText('feature')).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { CollectionsSection } from '../CollectionsSection';
|
|
5
|
+
import DJClientContext from '../../../providers/djclient';
|
|
6
|
+
|
|
7
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
8
|
+
|
|
9
|
+
describe('<CollectionsSection />', () => {
|
|
10
|
+
const mockDjClient = {
|
|
11
|
+
listAllCollections: jest.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mockCollections = [
|
|
15
|
+
{
|
|
16
|
+
name: 'my_collection',
|
|
17
|
+
description: 'My test collection',
|
|
18
|
+
nodeCount: 5,
|
|
19
|
+
createdBy: { username: 'test.user@example.com' },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'other_collection',
|
|
23
|
+
description: 'Another collection',
|
|
24
|
+
nodeCount: 10,
|
|
25
|
+
createdBy: { username: 'other.user@example.com' },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const mockCurrentUser = {
|
|
30
|
+
username: 'test.user@example.com',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const renderWithContext = props => {
|
|
38
|
+
return render(
|
|
39
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
40
|
+
<MemoryRouter>
|
|
41
|
+
<CollectionsSection {...props} />
|
|
42
|
+
</MemoryRouter>
|
|
43
|
+
</DJClientContext.Provider>,
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
it('should render loading state', () => {
|
|
48
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
49
|
+
data: { listCollections: [] },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
renderWithContext({
|
|
53
|
+
collections: [],
|
|
54
|
+
loading: true,
|
|
55
|
+
currentUser: mockCurrentUser,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(screen.getByText('Collections')).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should fetch and display all collections', async () => {
|
|
62
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
63
|
+
data: { listCollections: mockCollections },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
renderWithContext({
|
|
67
|
+
collections: [],
|
|
68
|
+
loading: false,
|
|
69
|
+
currentUser: mockCurrentUser,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(mockDjClient.listAllCollections).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(screen.getByText('my_collection')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('other_collection')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should show owner as "you" for current user collections', async () => {
|
|
83
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
84
|
+
data: { listCollections: mockCollections },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
renderWithContext({
|
|
88
|
+
collections: [],
|
|
89
|
+
loading: false,
|
|
90
|
+
currentUser: mockCurrentUser,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(screen.getByText('by you')).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should show username for other user collections', async () => {
|
|
99
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
100
|
+
data: { listCollections: mockCollections },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
renderWithContext({
|
|
104
|
+
collections: [],
|
|
105
|
+
loading: false,
|
|
106
|
+
currentUser: mockCurrentUser,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(screen.getByText('by other.user')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should display node counts', async () => {
|
|
115
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
116
|
+
data: { listCollections: mockCollections },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
renderWithContext({
|
|
120
|
+
collections: [],
|
|
121
|
+
loading: false,
|
|
122
|
+
currentUser: mockCurrentUser,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(screen.getByText('5 nodes')).toBeInTheDocument();
|
|
127
|
+
expect(screen.getByText('10 nodes')).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should render empty state when no collections', async () => {
|
|
132
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
133
|
+
data: { listCollections: [] },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
renderWithContext({
|
|
137
|
+
collections: [],
|
|
138
|
+
loading: false,
|
|
139
|
+
currentUser: mockCurrentUser,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(screen.getByText('No collections yet')).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should display user and other collections', async () => {
|
|
148
|
+
const collections = [
|
|
149
|
+
{
|
|
150
|
+
name: 'other_first',
|
|
151
|
+
description: 'Other collection',
|
|
152
|
+
createdBy: { username: 'other@example.com' },
|
|
153
|
+
nodeCount: 1,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'my_first',
|
|
157
|
+
description: 'My collection',
|
|
158
|
+
createdBy: { username: 'test.user@example.com' },
|
|
159
|
+
nodeCount: 2,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
164
|
+
data: { listCollections: collections },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
renderWithContext({
|
|
168
|
+
collections: [],
|
|
169
|
+
loading: false,
|
|
170
|
+
currentUser: mockCurrentUser,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
// Both collections should be displayed
|
|
175
|
+
expect(screen.getByText('my_first')).toBeInTheDocument();
|
|
176
|
+
expect(screen.getByText('other_first')).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should limit display to 8 collections', async () => {
|
|
181
|
+
const manyCollections = Array.from({ length: 12 }, (_, i) => ({
|
|
182
|
+
name: `collection_${i}`,
|
|
183
|
+
nodeCount: i,
|
|
184
|
+
createdBy: { username: 'test@example.com' },
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
188
|
+
data: { listCollections: manyCollections },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
renderWithContext({
|
|
192
|
+
collections: [],
|
|
193
|
+
loading: false,
|
|
194
|
+
currentUser: mockCurrentUser,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
const links = screen.getAllByRole('link');
|
|
199
|
+
// Should have at most 9 links (8 collections + 1 "Create Collection")
|
|
200
|
+
expect(links.length).toBeLessThanOrEqual(9);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle API errors gracefully', async () => {
|
|
205
|
+
mockDjClient.listAllCollections.mockRejectedValue(new Error('API error'));
|
|
206
|
+
|
|
207
|
+
const fallbackCollections = [
|
|
208
|
+
{
|
|
209
|
+
name: 'fallback_collection',
|
|
210
|
+
nodeCount: 1,
|
|
211
|
+
createdBy: { username: 'test@example.com' },
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
renderWithContext({
|
|
216
|
+
collections: fallbackCollections,
|
|
217
|
+
loading: false,
|
|
218
|
+
currentUser: mockCurrentUser,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
// Should fall back to props collections
|
|
223
|
+
expect(screen.getByText('fallback_collection')).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle collections without createdBy', async () => {
|
|
228
|
+
const collectionsWithoutOwner = [
|
|
229
|
+
{
|
|
230
|
+
name: 'no_owner',
|
|
231
|
+
nodeCount: 1,
|
|
232
|
+
createdBy: null,
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
237
|
+
data: { listCollections: collectionsWithoutOwner },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
renderWithContext({
|
|
241
|
+
collections: [],
|
|
242
|
+
loading: false,
|
|
243
|
+
currentUser: mockCurrentUser,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(screen.getByText('by unknown')).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle hover interactions on collection cards', async () => {
|
|
252
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
253
|
+
data: { listCollections: mockCollections },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
renderWithContext({
|
|
257
|
+
collections: [],
|
|
258
|
+
loading: false,
|
|
259
|
+
currentUser: mockCurrentUser,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
expect(screen.getByText('my_collection')).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const collectionLink = screen.getByText('my_collection').closest('a');
|
|
267
|
+
|
|
268
|
+
// Test mouse enter - should change styles
|
|
269
|
+
if (collectionLink) {
|
|
270
|
+
fireEvent.mouseEnter(collectionLink);
|
|
271
|
+
expect(collectionLink).toBeInTheDocument();
|
|
272
|
+
|
|
273
|
+
// Test mouse leave - should reset styles
|
|
274
|
+
fireEvent.mouseLeave(collectionLink);
|
|
275
|
+
expect(collectionLink).toBeInTheDocument();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|