Skip to content

Commit 82579ce

Browse files
committed
test: Add tests for container types, tabs, reordering, and grouping
46 tests total (30 existing + 16 new): tab container schema with inline tabs array, tabId on tiles, tab tile filtering, tabId cleanup on deletion, moving tiles between tabs and sections, section reorder, tile grouping.
1 parent 9fbcf29 commit 82579ce

1 file changed

Lines changed: 300 additions & 5 deletions

File tree

packages/app/src/__tests__/dashboardSections.test.tsx

Lines changed: 300 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,67 @@ describe('DashboardContainer schema', () => {
5151
}).success,
5252
).toBe(false);
5353
});
54+
55+
it('validates a tab container with tabs array', () => {
56+
const result = DashboardContainerSchema.safeParse({
57+
id: 'tab-1',
58+
type: 'tab',
59+
title: 'Overview Tabs',
60+
collapsed: false,
61+
tabs: [
62+
{ id: 'tab-a', title: 'Tab A' },
63+
{ id: 'tab-b', title: 'Tab B' },
64+
],
65+
activeTabId: 'tab-a',
66+
});
67+
expect(result.success).toBe(true);
68+
if (result.success) {
69+
expect(result.data.type).toBe('tab');
70+
expect(result.data.tabs).toHaveLength(2);
71+
expect(result.data.activeTabId).toBe('tab-a');
72+
}
73+
});
74+
75+
it('validates a group container', () => {
76+
const result = DashboardContainerSchema.safeParse({
77+
id: 'group-1',
78+
type: 'group',
79+
title: 'Key Metrics',
80+
collapsed: false,
81+
});
82+
expect(result.success).toBe(true);
83+
if (result.success) {
84+
expect(result.data.type).toBe('group');
85+
}
86+
});
87+
88+
it('validates a tab container with activeTabId', () => {
89+
const result = DashboardContainerSchema.safeParse({
90+
id: 'tab-1',
91+
type: 'tab',
92+
title: 'Tabs',
93+
collapsed: false,
94+
tabs: [{ id: 'sub-tab-1', title: 'First Tab' }],
95+
activeTabId: 'sub-tab-1',
96+
});
97+
expect(result.success).toBe(true);
98+
if (result.success) {
99+
expect(result.data.activeTabId).toBe('sub-tab-1');
100+
}
101+
});
102+
103+
it('rejects an invalid container type', () => {
104+
const result = DashboardContainerSchema.safeParse({
105+
id: 'c-1',
106+
type: 'invalid',
107+
title: 'Bad Type',
108+
collapsed: false,
109+
});
110+
expect(result.success).toBe(false);
111+
});
54112
});
55113

56-
describe('Tile schema with containerId', () => {
114+
describe('Tile schema with containerId and tabId', () => {
57115
const baseTile = {
58116
id: 'tile-1',
59117
x: 0,
@@ -79,6 +137,7 @@ describe('Tile schema with containerId', () => {
79137
expect(result.success).toBe(true);
80138
if (result.success) {
81139
expect(result.data.containerId).toBeUndefined();
140+
expect(result.data.tabId).toBeUndefined();
82141
}
83142
});
84143

@@ -92,6 +151,30 @@ describe('Tile schema with containerId', () => {
92151
expect(result.data.containerId).toBe('section-1');
93152
}
94153
});
154+
155+
it('validates a tile with containerId and tabId', () => {
156+
const result = TileSchema.safeParse({
157+
...baseTile,
158+
containerId: 'tab-container-1',
159+
tabId: 'tab-a',
160+
});
161+
expect(result.success).toBe(true);
162+
if (result.success) {
163+
expect(result.data.containerId).toBe('tab-container-1');
164+
expect(result.data.tabId).toBe('tab-a');
165+
}
166+
});
167+
168+
it('validates a tile with tabId but no containerId', () => {
169+
const result = TileSchema.safeParse({
170+
...baseTile,
171+
tabId: 'orphan-tab',
172+
});
173+
expect(result.success).toBe(true);
174+
if (result.success) {
175+
expect(result.data.tabId).toBe('orphan-tab');
176+
}
177+
});
95178
});
96179

97180
describe('Dashboard schema with sections', () => {
@@ -193,11 +276,58 @@ describe('Dashboard schema with sections', () => {
193276
expect(result.data.containers![0].title).toBe('Infrastructure');
194277
}
195278
});
279+
280+
it('validates a dashboard with tab container and tiles using tabId', () => {
281+
const result = DashboardSchema.safeParse({
282+
...baseDashboard,
283+
tiles: [
284+
{
285+
id: 'tile-1',
286+
x: 0,
287+
y: 0,
288+
w: 8,
289+
h: 10,
290+
containerId: 'tc1',
291+
tabId: 'tab-a',
292+
config: {
293+
source: 'source-1',
294+
select: [
295+
{
296+
aggFn: 'count',
297+
aggCondition: '',
298+
valueExpression: '',
299+
},
300+
],
301+
where: '',
302+
from: { databaseName: 'default', tableName: 'logs' },
303+
},
304+
},
305+
],
306+
containers: [
307+
{
308+
id: 'tc1',
309+
type: 'tab',
310+
title: 'My Tabs',
311+
collapsed: false,
312+
tabs: [
313+
{ id: 'tab-a', title: 'Tab A' },
314+
{ id: 'tab-b', title: 'Tab B' },
315+
],
316+
activeTabId: 'tab-a',
317+
},
318+
],
319+
});
320+
expect(result.success).toBe(true);
321+
if (result.success) {
322+
expect(result.data.tiles[0].tabId).toBe('tab-a');
323+
expect(result.data.containers![0].tabs).toHaveLength(2);
324+
}
325+
});
196326
});
197327

198328
describe('section tile grouping logic', () => {
199329
// Test the grouping logic used in DBDashboardPage
200-
type SimpleTile = { id: string; containerId?: string };
330+
type SimpleTile = { id: string; containerId?: string; tabId?: string };
201331
type SimpleSection = { id: string; title: string; collapsed: boolean };
202332

203333
function groupTilesBySection(tiles: SimpleTile[], sections: SimpleSection[]) {
@@ -289,10 +419,29 @@ describe('section tile grouping logic', () => {
289419
expect(ungrouped.map(t => t.id)).toEqual(['b', 'c']);
290420
expect(bySectionId.get('s1')).toHaveLength(1);
291421
});
422+
423+
it('filters tab container tiles by tabId', () => {
424+
const tiles: SimpleTile[] = [
425+
{ id: 'a', containerId: 'tc1', tabId: 'tab-1' },
426+
{ id: 'b', containerId: 'tc1', tabId: 'tab-2' },
427+
{ id: 'c', containerId: 'tc1', tabId: 'tab-1' },
428+
];
429+
const sections: SimpleSection[] = [
430+
{ id: 'tc1', title: 'Tab Container', collapsed: false },
431+
];
432+
const { bySectionId } = groupTilesBySection(tiles, sections);
433+
const allTabTiles = bySectionId.get('tc1') ?? [];
434+
expect(allTabTiles).toHaveLength(3);
435+
// Filter by tabId (as done in DBDashboardPage)
436+
const tab1Tiles = allTabTiles.filter(t => t.tabId === 'tab-1');
437+
const tab2Tiles = allTabTiles.filter(t => t.tabId === 'tab-2');
438+
expect(tab1Tiles).toHaveLength(2);
439+
expect(tab2Tiles).toHaveLength(1);
440+
});
292441
});
293442

294443
describe('section authoring operations', () => {
295-
type SimpleTile = { id: string; containerId?: string };
444+
type SimpleTile = { id: string; containerId?: string; tabId?: string };
296445
type SimpleSection = { id: string; title: string; collapsed: boolean };
297446
type SimpleDashboard = {
298447
tiles: SimpleTile[];
@@ -324,7 +473,9 @@ describe('section authoring operations', () => {
324473
...dashboard,
325474
containers: dashboard.containers?.filter(s => s.id !== containerId),
326475
tiles: dashboard.tiles.map(t =>
327-
t.containerId === containerId ? { ...t, containerId: undefined } : t,
476+
t.containerId === containerId
477+
? { ...t, containerId: undefined, tabId: undefined }
478+
: t,
328479
),
329480
};
330481
}
@@ -345,11 +496,12 @@ describe('section authoring operations', () => {
345496
dashboard: SimpleDashboard,
346497
tileId: string,
347498
containerId: string | undefined,
499+
tabId?: string,
348500
) {
349501
return {
350502
...dashboard,
351503
tiles: dashboard.tiles.map(t =>
352-
t.id === tileId ? { ...t, containerId } : t,
504+
t.id === tileId ? { ...t, containerId, tabId } : t,
353505
),
354506
};
355507
}
@@ -462,6 +614,25 @@ describe('section authoring operations', () => {
462614
expect(result.tiles.find(t => t.id === 'd')?.containerId).toBeUndefined();
463615
});
464616

617+
it('clears tabId when deleting a tab container', () => {
618+
const dashboard: SimpleDashboard = {
619+
tiles: [
620+
{ id: 'a', containerId: 'tc1', tabId: 'tab-1' },
621+
{ id: 'b', containerId: 'tc1', tabId: 'tab-2' },
622+
{ id: 'c', containerId: 's1' },
623+
],
624+
containers: [
625+
{ id: 'tc1', title: 'Tab Container', collapsed: false },
626+
{ id: 's1', title: 'Section', collapsed: false },
627+
],
628+
};
629+
const result = deleteSection(dashboard, 'tc1');
630+
expect(result.tiles.find(t => t.id === 'a')?.containerId).toBeUndefined();
631+
expect(result.tiles.find(t => t.id === 'a')?.tabId).toBeUndefined();
632+
expect(result.tiles.find(t => t.id === 'b')?.tabId).toBeUndefined();
633+
expect(result.tiles.find(t => t.id === 'c')?.containerId).toBe('s1');
634+
});
635+
465636
it('handles deleting the last section', () => {
466637
const dashboard: SimpleDashboard = {
467638
tiles: [{ id: 'a', containerId: 's1' }],
@@ -524,5 +695,129 @@ describe('section authoring operations', () => {
524695
const result = moveTileToSection(dashboard, 'a', undefined);
525696
expect(result.tiles[0].containerId).toBeUndefined();
526697
});
698+
699+
it('moves a tile to a specific tab', () => {
700+
const dashboard: SimpleDashboard = {
701+
tiles: [{ id: 'a' }],
702+
containers: [{ id: 'tc1', title: 'Tab Container', collapsed: false }],
703+
};
704+
const result = moveTileToSection(dashboard, 'a', 'tc1', 'tab-1');
705+
expect(result.tiles[0].containerId).toBe('tc1');
706+
expect(result.tiles[0].tabId).toBe('tab-1');
707+
});
708+
709+
it('clears tabId when moving from tab to regular section', () => {
710+
const dashboard: SimpleDashboard = {
711+
tiles: [{ id: 'a', containerId: 'tc1', tabId: 'tab-1' }],
712+
containers: [
713+
{ id: 'tc1', title: 'Tab Container', collapsed: false },
714+
{ id: 's1', title: 'Section', collapsed: false },
715+
],
716+
};
717+
const result = moveTileToSection(dashboard, 'a', 's1');
718+
expect(result.tiles[0].containerId).toBe('s1');
719+
expect(result.tiles[0].tabId).toBeUndefined();
720+
});
721+
});
722+
723+
describe('reorder sections', () => {
724+
function reorderSections(
725+
dashboard: SimpleDashboard,
726+
fromIndex: number,
727+
toIndex: number,
728+
) {
729+
if (!dashboard.containers) return dashboard;
730+
const containers = [...dashboard.containers];
731+
const [removed] = containers.splice(fromIndex, 1);
732+
containers.splice(toIndex, 0, removed);
733+
return { ...dashboard, containers };
734+
}
735+
736+
it('moves a section from first to last', () => {
737+
const dashboard: SimpleDashboard = {
738+
tiles: [],
739+
containers: [
740+
{ id: 's1', title: 'First', collapsed: false },
741+
{ id: 's2', title: 'Second', collapsed: false },
742+
{ id: 's3', title: 'Third', collapsed: false },
743+
],
744+
};
745+
const result = reorderSections(dashboard, 0, 2);
746+
expect(result.containers!.map(c => c.id)).toEqual(['s2', 's3', 's1']);
747+
});
748+
749+
it('moves a section from last to first', () => {
750+
const dashboard: SimpleDashboard = {
751+
tiles: [],
752+
containers: [
753+
{ id: 's1', title: 'First', collapsed: false },
754+
{ id: 's2', title: 'Second', collapsed: false },
755+
{ id: 's3', title: 'Third', collapsed: false },
756+
],
757+
};
758+
const result = reorderSections(dashboard, 2, 0);
759+
expect(result.containers!.map(c => c.id)).toEqual(['s3', 's1', 's2']);
760+
});
761+
762+
it('does not affect tiles when sections are reordered', () => {
763+
const dashboard: SimpleDashboard = {
764+
tiles: [
765+
{ id: 'a', containerId: 's1' },
766+
{ id: 'b', containerId: 's2' },
767+
],
768+
containers: [
769+
{ id: 's1', title: 'First', collapsed: false },
770+
{ id: 's2', title: 'Second', collapsed: false },
771+
],
772+
};
773+
const result = reorderSections(dashboard, 0, 1);
774+
expect(result.tiles).toEqual(dashboard.tiles);
775+
expect(result.containers!.map(c => c.id)).toEqual(['s2', 's1']);
776+
});
777+
});
778+
779+
describe('group selected tiles', () => {
780+
function groupTilesIntoSection(
781+
dashboard: SimpleDashboard,
782+
tileIds: string[],
783+
newSection: SimpleSection,
784+
) {
785+
const containers = [...(dashboard.containers ?? []), newSection];
786+
const tiles = dashboard.tiles.map(t =>
787+
tileIds.includes(t.id) ? { ...t, containerId: newSection.id } : t,
788+
);
789+
return { ...dashboard, containers, tiles };
790+
}
791+
792+
it('groups selected tiles into a new section', () => {
793+
const dashboard: SimpleDashboard = {
794+
tiles: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
795+
};
796+
const result = groupTilesIntoSection(dashboard, ['a', 'c'], {
797+
id: 'new-s',
798+
title: 'New Section',
799+
collapsed: false,
800+
});
801+
expect(result.containers).toHaveLength(1);
802+
expect(result.tiles.find(t => t.id === 'a')?.containerId).toBe('new-s');
803+
expect(result.tiles.find(t => t.id === 'b')?.containerId).toBeUndefined();
804+
expect(result.tiles.find(t => t.id === 'c')?.containerId).toBe('new-s');
805+
});
806+
807+
it('preserves existing sections when grouping', () => {
808+
const dashboard: SimpleDashboard = {
809+
tiles: [{ id: 'a', containerId: 's1' }, { id: 'b' }, { id: 'c' }],
810+
containers: [{ id: 's1', title: 'Existing', collapsed: false }],
811+
};
812+
const result = groupTilesIntoSection(dashboard, ['b', 'c'], {
813+
id: 'new-s',
814+
title: 'Grouped',
815+
collapsed: false,
816+
});
817+
expect(result.containers).toHaveLength(2);
818+
expect(result.tiles.find(t => t.id === 'a')?.containerId).toBe('s1');
819+
expect(result.tiles.find(t => t.id === 'b')?.containerId).toBe('new-s');
820+
expect(result.tiles.find(t => t.id === 'c')?.containerId).toBe('new-s');
821+
});
527822
});
528823
});

0 commit comments

Comments
 (0)