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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NodeComponents.jsx +4 -0
  3. package/src/app/components/Tab.jsx +11 -16
  4. package/src/app/components/__tests__/Tab.test.jsx +4 -2
  5. package/src/app/hooks/useWorkspaceData.js +226 -0
  6. package/src/app/index.tsx +17 -1
  7. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
  8. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
  9. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
  10. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
  11. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
  12. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
  13. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
  14. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
  15. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
  16. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
  17. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
  18. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
  20. package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
  21. package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
  22. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
  23. package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
  24. package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
  25. package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
  26. package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
  27. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
  28. package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
  29. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
  30. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
  31. package/src/app/pages/NodePage/index.jsx +15 -8
  32. package/src/app/services/DJService.js +73 -6
  33. package/src/app/services/__tests__/DJService.test.jsx +591 -0
  34. package/src/styles/index.css +32 -0
@@ -11,7 +11,6 @@ export function NeedsAttentionSection({
11
11
  staleMaterializations,
12
12
  orphanedDimensions,
13
13
  username,
14
- hasItems,
15
14
  loading,
16
15
  personalNamespace,
17
16
  hasPersonalNamespace,
@@ -55,129 +54,116 @@ export function NeedsAttentionSection({
55
54
  },
56
55
  ];
57
56
 
58
- const categoriesList = categories.map(cat => (
59
- <div
60
- key={cat.id}
61
- className="settings-card"
62
- style={{ padding: '0.5rem 0.75rem', minWidth: 0 }}
63
- >
64
- <div
65
- style={{
66
- display: 'flex',
67
- justifyContent: 'space-between',
68
- alignItems: 'center',
69
- marginBottom: '0.3rem',
70
- }}
71
- >
72
- <span style={{ fontSize: '11px', fontWeight: '600', color: '#555' }}>
73
- {cat.icon} {cat.label}
74
- <span
75
- style={{
76
- color: cat.nodes.length > 0 ? '#dc3545' : '#666',
77
- marginLeft: '4px',
78
- }}
79
- >
80
- ({cat.nodes.length})
81
- </span>
82
- </span>
83
- {cat.nodes.length > 0 && (
84
- <Link to={cat.viewAllLink} style={{ fontSize: '10px' }}>
85
- View all →
86
- </Link>
87
- )}
88
- </div>
89
- {cat.nodes.length > 0 ? (
90
- <div style={{ display: 'flex', gap: '0.3rem', overflow: 'hidden' }}>
91
- {cat.nodes.slice(0, 10).map(node => (
92
- <NodeChip key={node.name} node={node} />
93
- ))}
94
- </div>
95
- ) : (
96
- <div style={{ fontSize: '10px', color: '#28a745' }}>✓ All good!</div>
97
- )}
98
- </div>
99
- ));
100
-
101
57
  return (
102
58
  <section style={{ minWidth: 0, width: '100%' }}>
103
59
  <h2 className="settings-section-title">Needs Attention</h2>
104
- <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
60
+ <div className="settings-card" style={{ padding: '0.25rem 0.75rem' }}>
105
61
  {loading ? (
106
- <div
107
- className="settings-card"
108
- style={{ textAlign: 'center', padding: '1rem' }}
109
- >
62
+ <div style={{ textAlign: 'center', padding: '1rem' }}>
110
63
  <LoadingIcon />
111
64
  </div>
112
65
  ) : (
113
- <div
114
- style={{
115
- display: 'flex',
116
- flexDirection: 'column',
117
- width: '100%',
118
- gap: '0.5rem',
119
- }}
120
- >
121
- {categoriesList}
122
- {/* Personal namespace prompt if missing */}
123
- {!namespaceLoading && !hasPersonalNamespace && (
66
+ <>
67
+ {categories.map((cat, i) => (
124
68
  <div
69
+ key={cat.id}
125
70
  style={{
126
- padding: '0.75rem',
127
- backgroundColor: 'var(--card-bg, #f8f9fa)',
128
- border: '1px dashed var(--border-color, #dee2e6)',
129
- borderRadius: '6px',
130
- textAlign: 'center',
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ gap: '0.5rem',
74
+ padding: '0.35rem 0',
75
+ borderBottom:
76
+ i < categories.length - 1
77
+ ? '1px solid var(--border-color, #f0f0f0)'
78
+ : 'none',
79
+ minWidth: 0,
131
80
  }}
132
81
  >
133
- <div style={{ fontSize: '16px', marginBottom: '0.25rem' }}>
134
- 📁
135
- </div>
136
- <div
82
+ {/* Label + count */}
83
+ <span
137
84
  style={{
138
85
  fontSize: '11px',
139
- fontWeight: '500',
140
- marginBottom: '0.25rem',
141
- }}
142
- >
143
- Set up your namespace
144
- </div>
145
- <p
146
- style={{
147
- fontSize: '10px',
148
- color: '#666',
149
- marginBottom: '0.5rem',
86
+ fontWeight: 600,
87
+ color: '#555',
88
+ whiteSpace: 'nowrap',
89
+ flexShrink: 0,
150
90
  }}
151
91
  >
152
- Create{' '}
153
- <code
92
+ {cat.label}{' '}
93
+ <span
94
+ style={{
95
+ color: cat.nodes.length > 0 ? '#dc3545' : '#28a745',
96
+ fontWeight: 500,
97
+ }}
98
+ >
99
+ ({cat.nodes.length})
100
+ </span>
101
+ </span>
102
+
103
+ {/* Chips with right-side fade */}
104
+ {cat.nodes.length > 0 ? (
105
+ <div
154
106
  style={{
155
- backgroundColor: '#e9ecef',
156
- padding: '1px 4px',
157
- borderRadius: '3px',
158
- fontSize: '9px',
107
+ flex: 1,
108
+ overflow: 'hidden',
109
+ maskImage:
110
+ 'linear-gradient(to right, black 75%, transparent 100%)',
111
+ WebkitMaskImage:
112
+ 'linear-gradient(to right, black 75%, transparent 100%)',
159
113
  }}
160
114
  >
161
- {personalNamespace}
162
- </code>
163
- </p>
115
+ <div
116
+ style={{
117
+ display: 'flex',
118
+ gap: '0.25rem',
119
+ flexWrap: 'nowrap',
120
+ }}
121
+ >
122
+ {cat.nodes.slice(0, 20).map(node => (
123
+ <NodeChip key={node.name} node={node} />
124
+ ))}
125
+ </div>
126
+ </div>
127
+ ) : (
128
+ <span style={{ fontSize: '10px', color: '#28a745', flex: 1 }}>
129
+ All good
130
+ </span>
131
+ )}
132
+
133
+ {/* Arrow link — always visible, outside the fade */}
134
+ {cat.nodes.length > 0 && (
135
+ <Link
136
+ to={cat.viewAllLink}
137
+ style={{ fontSize: '11px', flexShrink: 0 }}
138
+ >
139
+
140
+ </Link>
141
+ )}
142
+ </div>
143
+ ))}
144
+
145
+ {!namespaceLoading && !hasPersonalNamespace && (
146
+ <div
147
+ style={{
148
+ display: 'flex',
149
+ alignItems: 'center',
150
+ gap: '0.5rem',
151
+ padding: '0.35rem 0',
152
+ fontSize: '11px',
153
+ }}
154
+ >
155
+ <span style={{ fontWeight: 600, color: '#555', flexShrink: 0 }}>
156
+ Personal namespace
157
+ </span>
164
158
  <Link
165
159
  to={`/namespaces/${personalNamespace}`}
166
- style={{
167
- display: 'inline-block',
168
- padding: '3px 8px',
169
- fontSize: '10px',
170
- backgroundColor: '#28a745',
171
- color: '#fff',
172
- borderRadius: '4px',
173
- textDecoration: 'none',
174
- }}
160
+ style={{ fontSize: '10px' }}
175
161
  >
176
- Create →
162
+ Create {personalNamespace}
177
163
  </Link>
178
164
  </div>
179
165
  )}
180
- </div>
166
+ </>
181
167
  )}
182
168
  </div>
183
169
  </section>
@@ -24,10 +24,8 @@ function capitalize(str) {
24
24
  }
25
25
 
26
26
  // Type Card Component
27
- function TypeCard({ type, nodes, count, username, activeTab }) {
28
- const maxDisplay = 10;
29
- const displayNodes = nodes.slice(0, maxDisplay);
30
- const remaining = count - maxDisplay;
27
+ function TypeCard({ type, nodes, hasMore, username, activeTab }) {
28
+ const displayNodes = nodes;
31
29
 
32
30
  // Build filter URL based on active tab and type
33
31
  const getFilterUrl = () => {
@@ -50,9 +48,7 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
50
48
  return (
51
49
  <div className="type-group-card">
52
50
  <div className="type-group-header">
53
- <span className="type-group-title">
54
- {capitalize(type)}s ({count})
55
- </span>
51
+ <span className="type-group-title">{capitalize(type)}s</span>
56
52
  </div>
57
53
 
58
54
  <div className="type-group-nodes">
@@ -102,7 +98,7 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
102
98
  display: 'flex',
103
99
  alignItems: 'center',
104
100
  gap: '4px',
105
- maxWidth: '200px',
101
+ maxWidth: '40%',
106
102
  }}
107
103
  >
108
104
  {isDefaultBranch && (
@@ -166,9 +162,9 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
166
162
  })}
167
163
  </div>
168
164
 
169
- {remaining > 0 && (
165
+ {hasMore && (
170
166
  <Link to={getFilterUrl()} className="type-group-more">
171
- +{remaining} more
167
+ More
172
168
  </Link>
173
169
  )}
174
170
  </div>
@@ -199,7 +195,7 @@ export function TypeGroupGrid({ groupedData, username, activeTab }) {
199
195
  key={group.type}
200
196
  type={group.type}
201
197
  nodes={group.nodes}
202
- count={group.count}
198
+ hasMore={group.hasMore}
203
199
  username={username}
204
200
  activeTab={activeTab}
205
201
  />
@@ -242,15 +242,87 @@ describe('<ActiveBranchesSection />', () => {
242
242
  });
243
243
  });
244
244
 
245
- it('should handle API errors when fetching counts', async () => {
246
- const consoleErrorSpy = jest
247
- .spyOn(console, 'error')
248
- .mockImplementation(() => {});
245
+ it('should show "+N more git namespaces" when more than 3 namespaces', async () => {
246
+ const manyNamespaces = Array.from({ length: 5 }, (_, i) => ({
247
+ name: `project${i}.main.node`,
248
+ gitInfo: {
249
+ repo: `org/repo${i}`,
250
+ branch: 'main',
251
+ defaultBranch: 'main',
252
+ parentNamespace: `project${i}`,
253
+ },
254
+ current: { updatedAt: `2024-01-0${i + 1}T10:00:00Z` },
255
+ }));
249
256
 
250
- mockDjClient.listNodesForLanding.mockRejectedValueOnce(
251
- new Error('API error'),
252
- );
257
+ renderWithContext({
258
+ ownedNodes: manyNamespaces,
259
+ recentlyEdited: [],
260
+ loading: false,
261
+ });
262
+
263
+ await waitFor(() => {
264
+ expect(screen.getByText('+2 more git namespaces')).toBeInTheDocument();
265
+ });
266
+ });
267
+
268
+ it('should handle node with no dots in fullNamespace (dotIdx <= 0 fallback)', async () => {
269
+ // Node name "ns.node" → fullNamespace = "ns" → dotIdx = -1 → baseNamespace = "ns"
270
+ const singleSegmentNode = [
271
+ {
272
+ name: 'ns.node',
273
+ gitInfo: {
274
+ repo: 'myorg/myrepo',
275
+ branch: 'main',
276
+ defaultBranch: 'main',
277
+ // No parentNamespace — fallback to parsing
278
+ },
279
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
280
+ },
281
+ ];
253
282
 
283
+ renderWithContext({
284
+ ownedNodes: singleSegmentNode,
285
+ recentlyEdited: [],
286
+ loading: false,
287
+ });
288
+
289
+ await waitFor(() => {
290
+ // fullNamespace = "ns", dotIdx = -1, baseNamespace = "ns"
291
+ expect(screen.getByText('ns')).toBeInTheDocument();
292
+ });
293
+ });
294
+
295
+ it('should sort branches with null lastActivity after those with activity', async () => {
296
+ // Creates two branches: one with activity, one without — sort should put active first
297
+ const nodesWithMixedActivity = [
298
+ {
299
+ name: 'myproject.feature.node1',
300
+ gitInfo: {
301
+ repo: 'myorg/myrepo',
302
+ branch: 'feature',
303
+ defaultBranch: 'main',
304
+ parentNamespace: 'myproject',
305
+ isDefaultBranch: false,
306
+ },
307
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
308
+ },
309
+ // main branch will be added by the "ensure default branch present" logic with null activity
310
+ ];
311
+
312
+ renderWithContext({
313
+ ownedNodes: nodesWithMixedActivity,
314
+ recentlyEdited: [],
315
+ loading: false,
316
+ });
317
+
318
+ await waitFor(() => {
319
+ // Both branches should be present
320
+ expect(screen.getByText('feature')).toBeInTheDocument();
321
+ expect(screen.getAllByText('main').length).toBeGreaterThan(0);
322
+ });
323
+ });
324
+
325
+ it('should render without errors when given valid props', async () => {
254
326
  renderWithContext({
255
327
  ownedNodes: mockOwnedNodes,
256
328
  recentlyEdited: [],
@@ -260,10 +332,6 @@ describe('<ActiveBranchesSection />', () => {
260
332
  await waitFor(() => {
261
333
  expect(screen.getByText('myproject')).toBeInTheDocument();
262
334
  });
263
-
264
- // Should still render despite API error
265
- expect(consoleErrorSpy).toHaveBeenCalled();
266
- consoleErrorSpy.mockRestore();
267
335
  });
268
336
 
269
337
  it('should show default branch even when user has no nodes on it', async () => {
@@ -201,6 +201,28 @@ describe('<CollectionsSection />', () => {
201
201
  });
202
202
  });
203
203
 
204
+ it('should log response when fetching all collections succeeds', async () => {
205
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
206
+ mockDjClient.listAllCollections.mockResolvedValue({
207
+ data: { listCollections: mockCollections },
208
+ });
209
+
210
+ renderWithContext({
211
+ collections: [],
212
+ loading: false,
213
+ currentUser: mockCurrentUser,
214
+ });
215
+
216
+ await waitFor(() => {
217
+ expect(consoleSpy).toHaveBeenCalledWith(
218
+ 'All collections response:',
219
+ expect.anything(),
220
+ );
221
+ });
222
+
223
+ consoleSpy.mockRestore();
224
+ });
225
+
204
226
  it('should handle API errors gracefully', async () => {
205
227
  mockDjClient.listAllCollections.mockRejectedValue(new Error('API error'));
206
228
 
@@ -190,6 +190,63 @@ describe('<MaterializationsSection />', () => {
190
190
  expect(screen.getByText('+5 more')).toBeInTheDocument();
191
191
  });
192
192
 
193
+ it('should sort nodes with null validThroughTs after those with timestamps', () => {
194
+ const pendingNode = createMockNode('pending_cube', null);
195
+ const freshNode = createMockNode('fresh_cube', 12);
196
+
197
+ render(
198
+ <MemoryRouter>
199
+ <MaterializationsSection
200
+ nodes={[pendingNode, freshNode]}
201
+ loading={false}
202
+ />
203
+ </MemoryRouter>,
204
+ );
205
+
206
+ const allCubes = screen.getAllByText(/cube/);
207
+ // fresh (has timestamp) should appear before pending (null timestamp)
208
+ expect(allCubes[0]).toHaveTextContent('fresh_cube');
209
+ expect(allCubes[1]).toHaveTextContent('pending_cube');
210
+ });
211
+
212
+ it('should keep relative order when both nodes have null validThroughTs', () => {
213
+ const pending1 = createMockNode('pending_a', null);
214
+ const pending2 = createMockNode('pending_b', null);
215
+
216
+ render(
217
+ <MemoryRouter>
218
+ <MaterializationsSection nodes={[pending1, pending2]} loading={false} />
219
+ </MemoryRouter>,
220
+ );
221
+
222
+ // Both pending — neither crashes, both appear
223
+ expect(screen.getByText('pending_a')).toBeInTheDocument();
224
+ expect(screen.getByText('pending_b')).toBeInTheDocument();
225
+ });
226
+
227
+ it('should show "just now" for materializations updated less than 1 hour ago', () => {
228
+ const justNowTs = now - 5 * 60 * 1000; // 5 minutes ago
229
+ const justNowNode = {
230
+ name: 'just_now_cube',
231
+ type: 'CUBE',
232
+ current: {
233
+ displayName: 'just_now_cube',
234
+ availability: {
235
+ validThroughTs: justNowTs,
236
+ },
237
+ materializations: [{ name: 'mat1', schedule: '@hourly' }],
238
+ },
239
+ };
240
+
241
+ render(
242
+ <MemoryRouter>
243
+ <MaterializationsSection nodes={[justNowNode]} loading={false} />
244
+ </MemoryRouter>,
245
+ );
246
+
247
+ expect(screen.getByText('🟢 just now')).toBeInTheDocument();
248
+ });
249
+
193
250
  it('should handle undefined nodes gracefully', () => {
194
251
  render(
195
252
  <MemoryRouter>
@@ -18,7 +18,7 @@ jest.mock('../TypeGroupGrid', () => ({
18
18
  <div data-testid="type-group-grid">
19
19
  {groupedData.map(group => (
20
20
  <div key={group.type}>
21
- {group.type}: {group.count} nodes
21
+ {group.type}: {group.nodes.length} nodes
22
22
  </div>
23
23
  ))}
24
24
  </div>
@@ -26,6 +26,11 @@ jest.mock('../TypeGroupGrid', () => ({
26
26
  }));
27
27
 
28
28
  describe('<MyNodesSection />', () => {
29
+ beforeEach(() => {
30
+ // Default to list view for most tests; Group by Type tests manage localStorage themselves
31
+ localStorage.setItem('workspace_groupByType', 'false');
32
+ });
33
+
29
34
  const mockOwnedNodes = [
30
35
  {
31
36
  name: 'default.owned_metric',
@@ -102,10 +107,10 @@ describe('<MyNodesSection />', () => {
102
107
  </MemoryRouter>,
103
108
  );
104
109
 
105
- expect(screen.getByText('Owned (1)')).toBeInTheDocument();
110
+ expect(screen.getByText('Owned')).toBeInTheDocument();
106
111
  // Should only count watched nodes that aren't owned (1 watched-only)
107
112
  expect(screen.getByText('Watched (1)')).toBeInTheDocument();
108
- expect(screen.getByText('Recent Edits (1)')).toBeInTheDocument();
113
+ expect(screen.getByText('Recent Edits')).toBeInTheDocument();
109
114
  });
110
115
 
111
116
  it('should show owned nodes by default', () => {
@@ -160,7 +165,7 @@ describe('<MyNodesSection />', () => {
160
165
  </MemoryRouter>,
161
166
  );
162
167
 
163
- fireEvent.click(screen.getByText('Recent Edits (1)'));
168
+ fireEvent.click(screen.getByText('Recent Edits'));
164
169
 
165
170
  expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
166
171
  });
@@ -183,7 +188,7 @@ describe('<MyNodesSection />', () => {
183
188
  expect(screen.getByText('No watched nodes')).toBeInTheDocument();
184
189
  });
185
190
 
186
- it('should limit displayed nodes to 8', () => {
191
+ it('should limit displayed nodes to 8 in list view', () => {
187
192
  const manyNodes = Array.from({ length: 15 }, (_, i) => ({
188
193
  name: `default.metric_${i}`,
189
194
  type: 'METRIC',
@@ -227,6 +232,45 @@ describe('<MyNodesSection />', () => {
227
232
  );
228
233
  });
229
234
 
235
+ it('should auto-switch to edited tab when no owned nodes but has recently edited', async () => {
236
+ const { waitFor } = await import('@testing-library/react');
237
+
238
+ render(
239
+ <MemoryRouter>
240
+ <MyNodesSection
241
+ ownedNodes={[]}
242
+ watchedNodes={[]}
243
+ recentlyEdited={mockRecentlyEdited}
244
+ username="test.user@example.com"
245
+ loading={false}
246
+ />
247
+ </MemoryRouter>,
248
+ );
249
+
250
+ // After effect fires, should auto-switch to edited tab
251
+ await waitFor(() => {
252
+ expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
253
+ });
254
+ });
255
+
256
+ it('should show "No recent edits" when on edited tab with no recent edits', () => {
257
+ render(
258
+ <MemoryRouter>
259
+ <MyNodesSection
260
+ ownedNodes={mockOwnedNodes}
261
+ watchedNodes={[]}
262
+ recentlyEdited={[]}
263
+ username="test.user@example.com"
264
+ loading={false}
265
+ />
266
+ </MemoryRouter>,
267
+ );
268
+
269
+ fireEvent.click(screen.getByText('Recent Edits'));
270
+
271
+ expect(screen.getByText('No recent edits')).toBeInTheDocument();
272
+ });
273
+
230
274
  it('should filter watched nodes to exclude owned nodes', () => {
231
275
  const ownedNodes = [
232
276
  { name: 'node1', type: 'METRIC', current: {} },
@@ -290,7 +334,7 @@ describe('<MyNodesSection />', () => {
290
334
  expect(screen.queryByLabelText('Group by Type')).not.toBeInTheDocument();
291
335
  });
292
336
 
293
- it('should toggle between list and grouped view', () => {
337
+ it('should toggle between grouped and list view', () => {
294
338
  render(
295
339
  <MemoryRouter>
296
340
  <MyNodesSection
@@ -303,17 +347,17 @@ describe('<MyNodesSection />', () => {
303
347
  </MemoryRouter>,
304
348
  );
305
349
 
306
- // Initially should show list view
307
- expect(screen.getByTestId('node-list')).toBeInTheDocument();
308
- expect(screen.queryByTestId('type-group-grid')).not.toBeInTheDocument();
350
+ // Initially should show grouped view (default is true)
351
+ expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
352
+ expect(screen.queryByTestId('node-list')).not.toBeInTheDocument();
309
353
 
310
- // Click toggle
354
+ // Click toggle to switch to list view
311
355
  const toggle = screen.getByLabelText('Group by Type');
312
356
  fireEvent.click(toggle);
313
357
 
314
- // Should now show grouped view
315
- expect(screen.queryByTestId('node-list')).not.toBeInTheDocument();
316
- expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
358
+ // Should now show list view
359
+ expect(screen.queryByTestId('type-group-grid')).not.toBeInTheDocument();
360
+ expect(screen.getByTestId('node-list')).toBeInTheDocument();
317
361
  });
318
362
 
319
363
  it('should persist toggle state to localStorage', () => {
@@ -332,10 +376,10 @@ describe('<MyNodesSection />', () => {
332
376
  const toggle = screen.getByLabelText('Group by Type');
333
377
  fireEvent.click(toggle);
334
378
 
335
- expect(localStorage.getItem('workspace_groupByType')).toBe('true');
379
+ expect(localStorage.getItem('workspace_groupByType')).toBe('false');
336
380
 
337
381
  fireEvent.click(toggle);
338
- expect(localStorage.getItem('workspace_groupByType')).toBe('false');
382
+ expect(localStorage.getItem('workspace_groupByType')).toBe('true');
339
383
  });
340
384
 
341
385
  it('should load toggle state from localStorage', () => {
@@ -378,9 +422,7 @@ describe('<MyNodesSection />', () => {
378
422
  </MemoryRouter>,
379
423
  );
380
424
 
381
- const toggle = screen.getByLabelText('Group by Type');
382
- fireEvent.click(toggle);
383
-
425
+ // Default is grouped view (localStorage cleared by beforeEach → null → true)
384
426
  expect(screen.getByText('metric: 2 nodes')).toBeInTheDocument();
385
427
  expect(screen.getByText('dimension: 1 nodes')).toBeInTheDocument();
386
428
  expect(screen.getByText('source: 1 nodes')).toBeInTheDocument();