Skip to content

Commit d3cf78a

Browse files
committed
test: Add GroupContainer and dashboard container tests
GroupContainer.test.tsx (329 lines, 18 tests): - Collapsible: chevron show/hide, children toggle, onToggle callback - Bordered: border style present/absent - Collapsed tab summary: pipe-separated names, no summary when expanded or single-tab - Tab bar: renders at 2+ tabs, plain header at 1 tab - Tab delete: confirmation flow, rejected confirmation - Alert indicators: dot on collapsed header, per-tab dot in expanded bar dashboardSections.test.tsx (556 lines, 56 tests): - Schema validation: containers without type field, backward compat with extra fields, tabs, collapsible/bordered options - Tile grouping logic, tab filtering, container authoring operations - Group tab operations: creation, add/delete tabs, rename sync
1 parent d4fa22f commit d3cf78a

2 files changed

Lines changed: 859 additions & 26 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import * as React from 'react';
2+
import { MantineProvider } from '@mantine/core';
3+
import { fireEvent, render, screen } from '@testing-library/react';
4+
5+
import GroupContainer from '@/components/GroupContainer';
6+
7+
function renderGroupContainer(
8+
props: Partial<React.ComponentProps<typeof GroupContainer>> = {},
9+
) {
10+
const defaults: React.ComponentProps<typeof GroupContainer> = {
11+
container: {
12+
id: 'g1',
13+
title: 'Test Group',
14+
collapsed: false,
15+
tabs: [{ id: 'tab-1', title: 'Tab One' }],
16+
},
17+
collapsed: false,
18+
defaultCollapsed: false,
19+
onToggle: jest.fn(),
20+
children: () => <div data-testid="group-children">Content</div>,
21+
...props,
22+
};
23+
return render(
24+
<MantineProvider>
25+
<GroupContainer {...defaults} />
26+
</MantineProvider>,
27+
);
28+
}
29+
30+
describe('GroupContainer', () => {
31+
describe('collapsible behavior', () => {
32+
it('renders chevron when collapsible (default)', () => {
33+
renderGroupContainer();
34+
expect(screen.getByTestId('group-chevron-g1')).toBeInTheDocument();
35+
});
36+
37+
it('hides chevron when collapsible is false', () => {
38+
renderGroupContainer({
39+
container: {
40+
id: 'g1',
41+
title: 'Test',
42+
collapsed: false,
43+
collapsible: false,
44+
tabs: [{ id: 'tab-1', title: 'Tab One' }],
45+
},
46+
});
47+
expect(screen.queryByTestId('group-chevron-g1')).not.toBeInTheDocument();
48+
});
49+
50+
it('shows children when expanded', () => {
51+
renderGroupContainer({ collapsed: false });
52+
expect(screen.getByTestId('group-children')).toBeInTheDocument();
53+
});
54+
55+
it('hides children when collapsed', () => {
56+
renderGroupContainer({ collapsed: true });
57+
expect(screen.queryByTestId('group-children')).not.toBeInTheDocument();
58+
});
59+
60+
it('calls onToggle when chevron is clicked', () => {
61+
const onToggle = jest.fn();
62+
renderGroupContainer({ onToggle });
63+
fireEvent.click(screen.getByTestId('group-chevron-g1'));
64+
expect(onToggle).toHaveBeenCalledTimes(1);
65+
});
66+
});
67+
68+
describe('bordered behavior', () => {
69+
it('renders border by default', () => {
70+
renderGroupContainer();
71+
const container = screen.getByTestId('group-container-g1');
72+
expect(container.style.border).toContain('1px solid');
73+
});
74+
75+
it('hides border when bordered is false', () => {
76+
renderGroupContainer({
77+
container: {
78+
id: 'g1',
79+
title: 'Test',
80+
collapsed: false,
81+
bordered: false,
82+
tabs: [{ id: 'tab-1', title: 'Tab One' }],
83+
},
84+
});
85+
const container = screen.getByTestId('group-container-g1');
86+
expect(container.style.border).toBe('');
87+
});
88+
});
89+
90+
describe('collapsed tab summary', () => {
91+
it('shows all tab names when collapsed with multiple tabs', () => {
92+
renderGroupContainer({
93+
collapsed: true,
94+
container: {
95+
id: 'g1',
96+
title: 'My Group',
97+
collapsed: false,
98+
tabs: [
99+
{ id: 'tab-1', title: 'Overview' },
100+
{ id: 'tab-2', title: 'Details' },
101+
{ id: 'tab-3', title: 'Logs' },
102+
],
103+
},
104+
});
105+
expect(screen.getByText('Overview | Details | Logs')).toBeInTheDocument();
106+
});
107+
108+
it('does not show tab summary when expanded', () => {
109+
renderGroupContainer({
110+
collapsed: false,
111+
container: {
112+
id: 'g1',
113+
title: 'My Group',
114+
collapsed: false,
115+
tabs: [
116+
{ id: 'tab-1', title: 'Overview' },
117+
{ id: 'tab-2', title: 'Details' },
118+
],
119+
},
120+
});
121+
expect(screen.queryByText('Overview | Details')).not.toBeInTheDocument();
122+
});
123+
124+
it('shows header title for single-tab collapsed group (no pipe summary)', () => {
125+
renderGroupContainer({
126+
collapsed: true,
127+
container: {
128+
id: 'g1',
129+
title: 'My Group',
130+
collapsed: false,
131+
tabs: [{ id: 'tab-1', title: 'Only Tab' }],
132+
},
133+
});
134+
// Single tab: shows header title, no pipe-separated summary
135+
expect(screen.getByText('Only Tab')).toBeInTheDocument();
136+
expect(screen.queryByText(/\|/)).not.toBeInTheDocument();
137+
});
138+
});
139+
140+
describe('overflow menu conditional rendering', () => {
141+
// Mantine Menu renders dropdown items in a portal only when opened,
142+
// so we test the negative case (items that should NOT be in the DOM).
143+
it('hides default-collapsed toggle when collapsible is false', () => {
144+
renderGroupContainer({
145+
onToggleDefaultCollapsed: jest.fn(),
146+
container: {
147+
id: 'g1',
148+
title: 'Test',
149+
collapsed: false,
150+
collapsible: false,
151+
tabs: [{ id: 'tab-1', title: 'Tab' }],
152+
},
153+
});
154+
expect(
155+
screen.queryByTestId('group-toggle-default-g1'),
156+
).not.toBeInTheDocument();
157+
});
158+
});
159+
160+
describe('tab bar', () => {
161+
it('renders tab bar with 2+ tabs when expanded', () => {
162+
renderGroupContainer({
163+
container: {
164+
id: 'g1',
165+
title: 'Group',
166+
collapsed: false,
167+
tabs: [
168+
{ id: 'tab-1', title: 'First' },
169+
{ id: 'tab-2', title: 'Second' },
170+
],
171+
activeTabId: 'tab-1',
172+
},
173+
activeTabId: 'tab-1',
174+
onTabChange: jest.fn(),
175+
});
176+
expect(screen.getByRole('tab', { name: 'First' })).toBeInTheDocument();
177+
expect(screen.getByRole('tab', { name: 'Second' })).toBeInTheDocument();
178+
});
179+
180+
it('renders plain header with single tab', () => {
181+
renderGroupContainer({
182+
container: {
183+
id: 'g1',
184+
title: 'Group',
185+
collapsed: false,
186+
tabs: [{ id: 'tab-1', title: 'Only' }],
187+
},
188+
});
189+
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
190+
expect(screen.getByText('Only')).toBeInTheDocument();
191+
});
192+
});
193+
194+
describe('tab delete', () => {
195+
it('calls onDeleteTab with confirmation when confirm is provided', async () => {
196+
const onDeleteTab = jest.fn();
197+
const confirm = jest.fn().mockResolvedValue(true);
198+
renderGroupContainer({
199+
onDeleteTab,
200+
confirm,
201+
container: {
202+
id: 'g1',
203+
title: 'Group',
204+
collapsed: false,
205+
tabs: [
206+
{ id: 'tab-1', title: 'First' },
207+
{ id: 'tab-2', title: 'Second' },
208+
],
209+
activeTabId: 'tab-1',
210+
},
211+
activeTabId: 'tab-1',
212+
onTabChange: jest.fn(),
213+
});
214+
215+
// Hover over first tab to reveal delete button
216+
const firstTab = screen.getByRole('tab', { name: 'First' });
217+
fireEvent.mouseEnter(firstTab);
218+
const deleteBtn = screen.getByTestId('tab-delete-tab-1');
219+
fireEvent.click(deleteBtn);
220+
221+
// Wait for async confirm
222+
await screen.findByText('First');
223+
expect(confirm).toHaveBeenCalledTimes(1);
224+
expect(onDeleteTab).toHaveBeenCalledWith('tab-1');
225+
});
226+
227+
it('does not call onDeleteTab when confirm is rejected', async () => {
228+
const onDeleteTab = jest.fn();
229+
const confirm = jest.fn().mockResolvedValue(false);
230+
renderGroupContainer({
231+
onDeleteTab,
232+
confirm,
233+
container: {
234+
id: 'g1',
235+
title: 'Group',
236+
collapsed: false,
237+
tabs: [
238+
{ id: 'tab-1', title: 'First' },
239+
{ id: 'tab-2', title: 'Second' },
240+
],
241+
activeTabId: 'tab-1',
242+
},
243+
activeTabId: 'tab-1',
244+
onTabChange: jest.fn(),
245+
});
246+
247+
const firstTab = screen.getByRole('tab', { name: 'First' });
248+
fireEvent.mouseEnter(firstTab);
249+
const deleteBtn = screen.getByTestId('tab-delete-tab-1');
250+
fireEvent.click(deleteBtn);
251+
252+
// Wait a tick for the async confirm to settle
253+
await new Promise(r => setTimeout(r, 0));
254+
expect(confirm).toHaveBeenCalledTimes(1);
255+
expect(onDeleteTab).not.toHaveBeenCalled();
256+
});
257+
});
258+
259+
describe('alert indicators', () => {
260+
it('shows alert dot on collapsed group header when alertingTabIds is non-empty', () => {
261+
const { container: wrapper } = renderGroupContainer({
262+
collapsed: true,
263+
alertingTabIds: new Set(['tab-1']),
264+
container: {
265+
id: 'g1',
266+
title: 'Group',
267+
collapsed: false,
268+
tabs: [
269+
{ id: 'tab-1', title: 'Overview' },
270+
{ id: 'tab-2', title: 'Logs' },
271+
],
272+
},
273+
});
274+
// Alert dot is rendered as a small span with red background
275+
const dots = wrapper.querySelectorAll(
276+
'span[style*="border-radius: 50%"]',
277+
);
278+
expect(dots.length).toBeGreaterThan(0);
279+
});
280+
281+
it('does not show alert dot when alertingTabIds is empty', () => {
282+
const { container: wrapper } = renderGroupContainer({
283+
collapsed: true,
284+
alertingTabIds: new Set(),
285+
container: {
286+
id: 'g1',
287+
title: 'Group',
288+
collapsed: false,
289+
tabs: [
290+
{ id: 'tab-1', title: 'Overview' },
291+
{ id: 'tab-2', title: 'Logs' },
292+
],
293+
},
294+
});
295+
const dots = wrapper.querySelectorAll(
296+
'span[style*="border-radius: 50%"]',
297+
);
298+
expect(dots.length).toBe(0);
299+
});
300+
301+
it('shows alert dot on specific tab in expanded tab bar', () => {
302+
renderGroupContainer({
303+
collapsed: false,
304+
alertingTabIds: new Set(['tab-2']),
305+
container: {
306+
id: 'g1',
307+
title: 'Group',
308+
collapsed: false,
309+
tabs: [
310+
{ id: 'tab-1', title: 'Overview' },
311+
{ id: 'tab-2', title: 'Alerts' },
312+
],
313+
activeTabId: 'tab-1',
314+
},
315+
activeTabId: 'tab-1',
316+
onTabChange: jest.fn(),
317+
});
318+
// The "Alerts" tab should have a dot, "Overview" should not
319+
const alertsTab = screen.getByRole('tab', { name: 'Alerts' });
320+
const overviewTab = screen.getByRole('tab', { name: 'Overview' });
321+
expect(
322+
alertsTab.querySelector('span[style*="border-radius: 50%"]'),
323+
).toBeTruthy();
324+
expect(
325+
overviewTab.querySelector('span[style*="border-radius: 50%"]'),
326+
).toBeNull();
327+
});
328+
});
329+
});

0 commit comments

Comments
 (0)