/**
* Tests for the Dashboard component.
* Tests drag-and-drop widget management, visibility toggles, and layout persistence.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { render } from '../utils';
import { Dashboard, type DashboardWidget } from '../../components/Dashboard';
const mockWidgets: DashboardWidget[] = [
{
id: 'widget-1',
title: 'Widget One',
component:
Widget One Content
,
defaultVisible: true,
defaultSize: 2,
},
{
id: 'widget-2',
title: 'Widget Two',
component: Widget Two Content
,
defaultVisible: true,
defaultSize: 4,
},
{
id: 'widget-3',
title: 'Widget Three',
component: Widget Three Content
,
defaultVisible: false, // Hidden by default
defaultSize: 1,
},
];
// Create a working localStorage mock for these tests
const localStorageData: Record = {};
const localStorageMock = {
getItem: vi.fn((key: string) => localStorageData[key] || null),
setItem: vi.fn((key: string, value: string) => {
localStorageData[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete localStorageData[key];
}),
clear: vi.fn(() => {
Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
}),
};
describe('Dashboard', () => {
beforeEach(() => {
// Clear localStorage data and mocks before each test
Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
vi.clearAllMocks();
Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
});
describe('rendering', () => {
it('renders visible widgets', () => {
render();
expect(screen.getByText('Widget One')).toBeInTheDocument();
expect(screen.getByText('Widget Two')).toBeInTheDocument();
expect(screen.getByText('Widget One Content')).toBeInTheDocument();
expect(screen.getByText('Widget Two Content')).toBeInTheDocument();
});
it('does not render hidden widgets', () => {
render();
// Widget Three is hidden by default
expect(screen.queryByText('Widget Three')).not.toBeInTheDocument();
expect(screen.queryByText('Widget Three Content')).not.toBeInTheDocument();
});
it('renders Reset Layout button when controls are shown', () => {
render();
expect(screen.getByText('Reset Layout')).toBeInTheDocument();
});
it('hides controls when hideControls is true', () => {
render();
expect(screen.queryByText('Reset Layout')).not.toBeInTheDocument();
});
it('shows hidden count button when widgets are hidden', () => {
render();
// Widget Three is hidden by default
expect(screen.getByText('1 Hidden')).toBeInTheDocument();
});
});
describe('visibility toggle', () => {
it('hides a widget when hide button is clicked', async () => {
render();
// Find and click the hide button for Widget One
const hideButtons = screen.getAllByTitle('Hide widget');
fireEvent.click(hideButtons[0]);
await waitFor(() => {
expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
});
});
it('shows hidden widgets panel when clicking hidden count button', async () => {
render();
const hiddenButton = screen.getByText('1 Hidden');
fireEvent.click(hiddenButton);
await waitFor(() => {
expect(screen.getByText('Hidden widgets (click to show):')).toBeInTheDocument();
expect(screen.getByText('Widget Three')).toBeInTheDocument();
});
});
it('shows a hidden widget when clicked in the hidden panel', async () => {
render();
// Open hidden panel
const hiddenButton = screen.getByText('1 Hidden');
fireEvent.click(hiddenButton);
await waitFor(() => {
expect(screen.getByText('Widget Three')).toBeInTheDocument();
});
// Click to show Widget Three
const showWidgetButton = screen.getByRole('button', { name: /Widget Three/i });
fireEvent.click(showWidgetButton);
await waitFor(() => {
expect(screen.getByText('Widget Three Content')).toBeInTheDocument();
});
});
});
describe('reset layout', () => {
it('resets layout to default when Reset Layout is clicked', async () => {
render();
// Hide Widget One
const hideButtons = screen.getAllByTitle('Hide widget');
fireEvent.click(hideButtons[0]);
await waitFor(() => {
expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
});
// Reset layout
const resetButton = screen.getByText('Reset Layout');
fireEvent.click(resetButton);
await waitFor(() => {
expect(screen.getByText('Widget One Content')).toBeInTheDocument();
});
});
it('calls onResetLayout callback when reset', async () => {
const onResetLayout = vi.fn();
render(
);
const resetButton = screen.getByText('Reset Layout');
fireEvent.click(resetButton);
expect(onResetLayout).toHaveBeenCalled();
});
});
describe('size toggle', () => {
it('cycles widget size when size button is clicked', async () => {
render();
// Widget One starts at size 2, should cycle to 4
const sizeButtons = screen.getAllByTitle(/Size:/);
fireEvent.click(sizeButtons[0]);
// After click, size should change (verify by checking title updates)
await waitFor(() => {
// The button title should now show a different size
expect(screen.getAllByTitle(/Size:/)[0]).toBeInTheDocument();
});
});
});
describe('localStorage persistence', () => {
it('saves layout to localStorage when widget is hidden', async () => {
render();
// Hide a widget to trigger a layout change
const hideButtons = screen.getAllByTitle('Hide widget');
fireEvent.click(hideButtons[0]);
await waitFor(() => {
// Verify setItem was called with the storage key
expect(localStorageMock.setItem).toHaveBeenCalled();
const calls = localStorageMock.setItem.mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[0]).toBe('test-dashboard-persist');
const parsed = JSON.parse(lastCall[1]);
expect(parsed.hidden).toContain('widget-1');
});
});
it('loads saved layout from localStorage', () => {
// Pre-set a layout in localStorage
localStorageData['test-dashboard-load'] = JSON.stringify({
order: ['widget-2', 'widget-1', 'widget-3'],
hidden: ['widget-2'],
sizes: { 'widget-1': 4, 'widget-2': 2, 'widget-3': 1 },
});
render();
// Widget 2 should be hidden
expect(screen.queryByText('Widget Two Content')).not.toBeInTheDocument();
// Widget 1 should be visible
expect(screen.getByText('Widget One Content')).toBeInTheDocument();
});
});
describe('empty state', () => {
it('shows empty message when all widgets are hidden', async () => {
// Pre-set all widgets as hidden
localStorageData['test-dashboard-empty'] = JSON.stringify({
order: ['widget-1', 'widget-2', 'widget-3'],
hidden: ['widget-1', 'widget-2', 'widget-3'],
sizes: {},
});
render();
expect(screen.getByText('All widgets are hidden.')).toBeInTheDocument();
// There are multiple Reset Layout buttons (one in controls, one in empty state)
const resetButtons = screen.getAllByRole('button', { name: 'Reset Layout' });
expect(resetButtons.length).toBeGreaterThan(0);
});
});
describe('custom render controls', () => {
it('renders custom controls when renderControls is provided', () => {
render(
(
Hidden: {hiddenCount}
)}
/>
);
expect(screen.getByTestId('custom-controls')).toBeInTheDocument();
expect(screen.getByText('Hidden: 1')).toBeInTheDocument();
});
});
});