Dashboard.test.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /**
  2. * Tests for the Dashboard component.
  3. * Tests drag-and-drop widget management, visibility toggles, and layout persistence.
  4. */
  5. import { describe, it, expect, beforeEach, vi } from 'vitest';
  6. import { screen, fireEvent, waitFor } from '@testing-library/react';
  7. import { render } from '../utils';
  8. import { Dashboard, type DashboardWidget } from '../../components/Dashboard';
  9. const mockWidgets: DashboardWidget[] = [
  10. {
  11. id: 'widget-1',
  12. title: 'Widget One',
  13. component: <div>Widget One Content</div>,
  14. defaultVisible: true,
  15. defaultSize: 2,
  16. },
  17. {
  18. id: 'widget-2',
  19. title: 'Widget Two',
  20. component: <div>Widget Two Content</div>,
  21. defaultVisible: true,
  22. defaultSize: 4,
  23. },
  24. {
  25. id: 'widget-3',
  26. title: 'Widget Three',
  27. component: <div>Widget Three Content</div>,
  28. defaultVisible: false, // Hidden by default
  29. defaultSize: 1,
  30. },
  31. ];
  32. // Create a working localStorage mock for these tests
  33. const localStorageData: Record<string, string> = {};
  34. const localStorageMock = {
  35. getItem: vi.fn((key: string) => localStorageData[key] || null),
  36. setItem: vi.fn((key: string, value: string) => {
  37. localStorageData[key] = value;
  38. }),
  39. removeItem: vi.fn((key: string) => {
  40. delete localStorageData[key];
  41. }),
  42. clear: vi.fn(() => {
  43. Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
  44. }),
  45. };
  46. describe('Dashboard', () => {
  47. beforeEach(() => {
  48. // Clear localStorage data and mocks before each test
  49. Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
  50. vi.clearAllMocks();
  51. Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
  52. });
  53. describe('rendering', () => {
  54. it('renders visible widgets', () => {
  55. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  56. expect(screen.getByText('Widget One')).toBeInTheDocument();
  57. expect(screen.getByText('Widget Two')).toBeInTheDocument();
  58. expect(screen.getByText('Widget One Content')).toBeInTheDocument();
  59. expect(screen.getByText('Widget Two Content')).toBeInTheDocument();
  60. });
  61. it('does not render hidden widgets', () => {
  62. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  63. // Widget Three is hidden by default
  64. expect(screen.queryByText('Widget Three')).not.toBeInTheDocument();
  65. expect(screen.queryByText('Widget Three Content')).not.toBeInTheDocument();
  66. });
  67. it('renders Reset Layout button when controls are shown', () => {
  68. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  69. expect(screen.getByText('Reset Layout')).toBeInTheDocument();
  70. });
  71. it('hides controls when hideControls is true', () => {
  72. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" hideControls />);
  73. expect(screen.queryByText('Reset Layout')).not.toBeInTheDocument();
  74. });
  75. it('shows hidden count button when widgets are hidden', () => {
  76. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  77. // Widget Three is hidden by default
  78. expect(screen.getByText('1 Hidden')).toBeInTheDocument();
  79. });
  80. });
  81. describe('visibility toggle', () => {
  82. it('hides a widget when hide button is clicked', async () => {
  83. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  84. // Find and click the hide button for Widget One
  85. const hideButtons = screen.getAllByTitle('Hide widget');
  86. fireEvent.click(hideButtons[0]);
  87. await waitFor(() => {
  88. expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
  89. });
  90. });
  91. it('shows hidden widgets panel when clicking hidden count button', async () => {
  92. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  93. const hiddenButton = screen.getByText('1 Hidden');
  94. fireEvent.click(hiddenButton);
  95. await waitFor(() => {
  96. expect(screen.getByText('Hidden widgets (click to show):')).toBeInTheDocument();
  97. expect(screen.getByText('Widget Three')).toBeInTheDocument();
  98. });
  99. });
  100. it('shows a hidden widget when clicked in the hidden panel', async () => {
  101. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  102. // Open hidden panel
  103. const hiddenButton = screen.getByText('1 Hidden');
  104. fireEvent.click(hiddenButton);
  105. await waitFor(() => {
  106. expect(screen.getByText('Widget Three')).toBeInTheDocument();
  107. });
  108. // Click to show Widget Three
  109. const showWidgetButton = screen.getByRole('button', { name: /Widget Three/i });
  110. fireEvent.click(showWidgetButton);
  111. await waitFor(() => {
  112. expect(screen.getByText('Widget Three Content')).toBeInTheDocument();
  113. });
  114. });
  115. });
  116. describe('reset layout', () => {
  117. it('resets layout to default when Reset Layout is clicked', async () => {
  118. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  119. // Hide Widget One
  120. const hideButtons = screen.getAllByTitle('Hide widget');
  121. fireEvent.click(hideButtons[0]);
  122. await waitFor(() => {
  123. expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
  124. });
  125. // Reset layout
  126. const resetButton = screen.getByText('Reset Layout');
  127. fireEvent.click(resetButton);
  128. await waitFor(() => {
  129. expect(screen.getByText('Widget One Content')).toBeInTheDocument();
  130. });
  131. });
  132. it('calls onResetLayout callback when reset', async () => {
  133. const onResetLayout = vi.fn();
  134. render(
  135. <Dashboard
  136. widgets={mockWidgets}
  137. storageKey="test-dashboard"
  138. onResetLayout={onResetLayout}
  139. />
  140. );
  141. const resetButton = screen.getByText('Reset Layout');
  142. fireEvent.click(resetButton);
  143. expect(onResetLayout).toHaveBeenCalled();
  144. });
  145. });
  146. describe('size toggle', () => {
  147. it('cycles widget size when size button is clicked', async () => {
  148. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
  149. // Widget One starts at size 2, should cycle to 4
  150. const sizeButtons = screen.getAllByTitle(/Size:/);
  151. fireEvent.click(sizeButtons[0]);
  152. // After click, size should change (verify by checking title updates)
  153. await waitFor(() => {
  154. // The button title should now show a different size
  155. expect(screen.getAllByTitle(/Size:/)[0]).toBeInTheDocument();
  156. });
  157. });
  158. });
  159. describe('localStorage persistence', () => {
  160. it('saves layout to localStorage when widget is hidden', async () => {
  161. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-persist" />);
  162. // Hide a widget to trigger a layout change
  163. const hideButtons = screen.getAllByTitle('Hide widget');
  164. fireEvent.click(hideButtons[0]);
  165. await waitFor(() => {
  166. // Verify setItem was called with the storage key
  167. expect(localStorageMock.setItem).toHaveBeenCalled();
  168. const calls = localStorageMock.setItem.mock.calls;
  169. const lastCall = calls[calls.length - 1];
  170. expect(lastCall[0]).toBe('test-dashboard-persist');
  171. const parsed = JSON.parse(lastCall[1]);
  172. expect(parsed.hidden).toContain('widget-1');
  173. });
  174. });
  175. it('loads saved layout from localStorage', () => {
  176. // Pre-set a layout in localStorage
  177. localStorageData['test-dashboard-load'] = JSON.stringify({
  178. order: ['widget-2', 'widget-1', 'widget-3'],
  179. hidden: ['widget-2'],
  180. sizes: { 'widget-1': 4, 'widget-2': 2, 'widget-3': 1 },
  181. });
  182. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-load" />);
  183. // Widget 2 should be hidden
  184. expect(screen.queryByText('Widget Two Content')).not.toBeInTheDocument();
  185. // Widget 1 should be visible
  186. expect(screen.getByText('Widget One Content')).toBeInTheDocument();
  187. });
  188. });
  189. describe('empty state', () => {
  190. it('shows empty message when all widgets are hidden', async () => {
  191. // Pre-set all widgets as hidden
  192. localStorageData['test-dashboard-empty'] = JSON.stringify({
  193. order: ['widget-1', 'widget-2', 'widget-3'],
  194. hidden: ['widget-1', 'widget-2', 'widget-3'],
  195. sizes: {},
  196. });
  197. render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-empty" />);
  198. expect(screen.getByText('All widgets are hidden.')).toBeInTheDocument();
  199. // There are multiple Reset Layout buttons (one in controls, one in empty state)
  200. const resetButtons = screen.getAllByRole('button', { name: 'Reset Layout' });
  201. expect(resetButtons.length).toBeGreaterThan(0);
  202. });
  203. });
  204. describe('custom render controls', () => {
  205. it('renders custom controls when renderControls is provided', () => {
  206. render(
  207. <Dashboard
  208. widgets={mockWidgets}
  209. storageKey="test-dashboard"
  210. renderControls={({ hiddenCount }) => (
  211. <div data-testid="custom-controls">Hidden: {hiddenCount}</div>
  212. )}
  213. />
  214. );
  215. expect(screen.getByTestId('custom-controls')).toBeInTheDocument();
  216. expect(screen.getByText('Hidden: 1')).toBeInTheDocument();
  217. });
  218. });
  219. });