test(05-01): add failing tests for dashboard grid and clock widget
- DashboardGrid test: empty state, widget rendering, edit mode affordances - ClockWidget test: timezone rendering, showDate toggle behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
import { cleanup, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock next-intl
|
||||||
|
vi.mock('next-intl', () => ({
|
||||||
|
useTranslations: (ns: string) => (key: string) => {
|
||||||
|
const map: Record<string, Record<string, string>> = {
|
||||||
|
widgets: {
|
||||||
|
emptyHeading: 'No active widgets',
|
||||||
|
emptyBody: 'Click edit to add widgets to your dashboard.',
|
||||||
|
addWidget: 'Add widget',
|
||||||
|
catalogTitle: 'Add widget',
|
||||||
|
deleteTooltip: 'Remove widget',
|
||||||
|
'clock.name': 'Clock',
|
||||||
|
'clock.description': 'Shows the current time',
|
||||||
|
'search.name': 'Search',
|
||||||
|
'search.description': 'Quick web search',
|
||||||
|
'calendar.name': 'Calendar',
|
||||||
|
'calendar.description': 'Upcoming events',
|
||||||
|
'note.name': 'Notes',
|
||||||
|
'note.description': 'Free-text notes with Markdown',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
title: 'Dashboard',
|
||||||
|
editMode: 'Edit dashboard',
|
||||||
|
saveChanges: 'Save changes',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return map[ns]?.[key] ?? key;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock react-grid-layout Responsive to a simple passthrough for jsdom stability
|
||||||
|
vi.mock('react-grid-layout', () => ({
|
||||||
|
Responsive: ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => <div data-testid="responsive-grid">{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock dashboard-api
|
||||||
|
vi.mock('@/lib/dashboard-api', () => ({
|
||||||
|
fetchLayout: vi.fn().mockResolvedValue({ lg: [], md: [], sm: [], xs: [], xxs: [] }),
|
||||||
|
saveLayout: vi.fn().mockResolvedValue({}),
|
||||||
|
fetchWidgets: vi.fn().mockResolvedValue([]),
|
||||||
|
addWidget: vi.fn(),
|
||||||
|
removeWidget: vi.fn(),
|
||||||
|
updateWidgetConfig: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock dashboard-store
|
||||||
|
const mockStore = {
|
||||||
|
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
|
||||||
|
widgets: [] as Array<{ id: string; widgetType: string; config: Record<string, unknown> }>,
|
||||||
|
isEditMode: false,
|
||||||
|
isDirty: false,
|
||||||
|
setEditMode: vi.fn(),
|
||||||
|
updateLayouts: vi.fn(),
|
||||||
|
addWidget: vi.fn(),
|
||||||
|
removeWidget: vi.fn(),
|
||||||
|
loadDashboard: vi.fn(),
|
||||||
|
saveLayout: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('@/lib/stores/dashboard-store', () => ({
|
||||||
|
useDashboardStore: (selector?: (state: typeof mockStore) => unknown) => {
|
||||||
|
if (selector) return selector(mockStore);
|
||||||
|
return mockStore;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DashboardGrid', () => {
|
||||||
|
it('renders empty state heading when no widgets', async () => {
|
||||||
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
|
render(
|
||||||
|
<DashboardGrid
|
||||||
|
layouts={{ lg: [], md: [], sm: [], xs: [], xxs: [] }}
|
||||||
|
widgets={[]}
|
||||||
|
isEditMode={false}
|
||||||
|
onLayoutChange={vi.fn()}
|
||||||
|
onRemoveWidget={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('No active widgets')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders widget instance when widgets are provided', async () => {
|
||||||
|
const widgets = [
|
||||||
|
{ id: 'inst-1', widgetType: 'clock', config: { timezone: 'Europe/Berlin' } },
|
||||||
|
];
|
||||||
|
const layouts = {
|
||||||
|
lg: [{ i: 'inst-1', x: 0, y: 0, w: 2, h: 2 }],
|
||||||
|
md: [],
|
||||||
|
sm: [],
|
||||||
|
xs: [],
|
||||||
|
xxs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
|
render(
|
||||||
|
<DashboardGrid
|
||||||
|
layouts={layouts}
|
||||||
|
widgets={widgets}
|
||||||
|
isEditMode={false}
|
||||||
|
onLayoutChange={vi.fn()}
|
||||||
|
onRemoveWidget={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Widget is rendered inside grid, keyed by instance id
|
||||||
|
const grid = screen.getByTestId('responsive-grid');
|
||||||
|
expect(grid).toBeInTheDocument();
|
||||||
|
// The widget wrapper should be in the document
|
||||||
|
expect(grid.querySelector('[data-widget-id="inst-1"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows edit affordances (delete button) when isEditMode is true', async () => {
|
||||||
|
const widgets = [
|
||||||
|
{ id: 'inst-2', widgetType: 'clock', config: {} },
|
||||||
|
];
|
||||||
|
const layouts = {
|
||||||
|
lg: [{ i: 'inst-2', x: 0, y: 0, w: 2, h: 2 }],
|
||||||
|
md: [],
|
||||||
|
sm: [],
|
||||||
|
xs: [],
|
||||||
|
xxs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
|
render(
|
||||||
|
<DashboardGrid
|
||||||
|
layouts={layouts}
|
||||||
|
widgets={widgets}
|
||||||
|
isEditMode={true}
|
||||||
|
onLayoutChange={vi.fn()}
|
||||||
|
onRemoveWidget={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// In edit mode, delete button and drag handle should exist
|
||||||
|
expect(screen.getByLabelText('Remove widget')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { cleanup, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock next-intl
|
||||||
|
vi.mock('next-intl', () => ({
|
||||||
|
useTranslations: (ns: string) => (key: string) => {
|
||||||
|
const map: Record<string, Record<string, string>> = {
|
||||||
|
widgets: {
|
||||||
|
'clock.name': 'Clock',
|
||||||
|
'clock.description': 'Shows the current time',
|
||||||
|
'clock.dateHint': 'Show date',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return map[ns]?.[key] ?? key;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ClockWidget', () => {
|
||||||
|
it('renders a time string with timezone config', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-06-24T12:30:00Z'));
|
||||||
|
|
||||||
|
const { ClockWidget } = await import('./clock-widget');
|
||||||
|
render(
|
||||||
|
<ClockWidget
|
||||||
|
instanceId="clock-1"
|
||||||
|
config={{ timezone: 'Europe/Berlin', showDate: true }}
|
||||||
|
isEditMode={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render a time element
|
||||||
|
const timeEl = screen.getByRole('time');
|
||||||
|
expect(timeEl).toBeInTheDocument();
|
||||||
|
// Time text should be non-empty
|
||||||
|
expect(timeEl.textContent).toBeTruthy();
|
||||||
|
|
||||||
|
// Should render a date element when showDate is true
|
||||||
|
const dateEl = screen.getByTestId('clock-date');
|
||||||
|
expect(dateEl).toBeInTheDocument();
|
||||||
|
expect(dateEl.textContent).toBeTruthy();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render date element when showDate is false', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-06-24T12:30:00Z'));
|
||||||
|
|
||||||
|
const { ClockWidget } = await import('./clock-widget');
|
||||||
|
render(
|
||||||
|
<ClockWidget
|
||||||
|
instanceId="clock-2"
|
||||||
|
config={{ timezone: 'Europe/Berlin', showDate: false }}
|
||||||
|
isEditMode={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('clock-date')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user