diff --git a/frontend/__tests__/toast.test.tsx b/frontend/__tests__/toast.test.tsx
new file mode 100644
index 00000000..94136d2e
--- /dev/null
+++ b/frontend/__tests__/toast.test.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
+import { ToastProvider, toast } from '@/components/ui/toast';
+
+// Helper function to trigger animationEnd to complete react-toastify's exit animations
+function triggerAnimationEnd(node: HTMLElement | HTMLElement[]) {
+ if (Array.isArray(node)) {
+ node.forEach(el => {
+ if (el.parentNode) {
+ fireEvent.animationEnd(el.parentNode as HTMLElement);
+ }
+ });
+ } else if (node.parentNode) {
+ fireEvent.animationEnd(node.parentNode as HTMLElement);
+ }
+ jest.runAllTimers();
+}
+
+describe('Toast Component', () => {
+ beforeEach(() => {
+ // Use fake timers for reliable timing tests
+ jest.useFakeTimers();
+ // Clear all toasts and mocks before each test
+ jest.clearAllMocks();
+ render();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('auto-dismisses after the configured 3000ms timeout', async () => {
+ // Trigger a toast within act to handle async updates
+ act(() => {
+ toast.success('Test success message');
+ });
+
+ // Verify the toast is initially present
+ const toastElement = await screen.findByText('Test success message');
+ expect(toastElement).toBeInTheDocument();
+
+ // Fast-forward time by 3000ms (the configured autoClose time)
+ act(() => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ // Trigger animation end to complete the exit transition
+ triggerAnimationEnd(toastElement);
+
+ // Verify the toast is removed after the timeout
+ await waitFor(() => {
+ expect(screen.queryByText('Test success message')).not.toBeInTheDocument();
+ });
+ });
+
+ it('allows manual dismiss via the close button', async () => {
+ // Trigger a toast within act
+ act(() => {
+ toast.error('Test error message');
+ });
+
+ // Verify the toast is present
+ const toastElement = await screen.findByText('Test error message');
+ expect(toastElement).toBeInTheDocument();
+
+ // Find and click the close button (react-toastify uses aria-label="close")
+ const closeButton = screen.getByRole('button', { name: /close/i });
+ expect(closeButton).toBeInTheDocument();
+
+ act(() => {
+ fireEvent.click(closeButton);
+ });
+
+ // Trigger animation end to complete the exit transition
+ triggerAnimationEnd(toastElement);
+
+ // Verify the toast is removed immediately
+ await waitFor(() => {
+ expect(screen.queryByText('Test error message')).not.toBeInTheDocument();
+ });
+ });
+
+ it('stacks multiple simultaneous toasts correctly instead of replacing them', async () => {
+ // Trigger multiple toasts in sequence within act
+ act(() => {
+ toast.info('First toast message');
+ toast.success('Second toast message');
+ toast.warning('Third toast message');
+ });
+
+ // Verify all three toasts are present in the document (stacked)
+ const firstToast = await screen.findByText('First toast message');
+ const secondToast = screen.getByText('Second toast message');
+ const thirdToast = screen.getByText('Third toast message');
+
+ expect(firstToast).toBeInTheDocument();
+ expect(secondToast).toBeInTheDocument();
+ expect(thirdToast).toBeInTheDocument();
+
+ // Verify we have exactly 3 toasts in the DOM (they stack, not replace)
+ const toastElements = screen.getAllByRole('alert');
+ expect(toastElements.length).toBe(3);
+
+ // Fast-forward time to clear all toasts
+ act(() => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ // Trigger animation ends for all toasts
+ triggerAnimationEnd(toastElements);
+
+ // Verify all toasts are removed
+ await waitFor(() => {
+ expect(screen.queryByText('First toast message')).not.toBeInTheDocument();
+ expect(screen.queryByText('Second toast message')).not.toBeInTheDocument();
+ expect(screen.queryByText('Third toast message')).not.toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/features/Dashboard/DashboardCharts.spec.tsx b/frontend/features/Dashboard/DashboardCharts.spec.tsx
new file mode 100644
index 00000000..77ade16a
--- /dev/null
+++ b/frontend/features/Dashboard/DashboardCharts.spec.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import DashboardCharts from './DashboardCharts';
+
+// Mock recharts to avoid issues with canvas/SVG in Jest
+jest.mock('recharts', () => ({
+ PieChart: () =>
PieChart
,
+ Pie: () => null,
+ Cell: () => null,
+ BarChart: () => BarChart
,
+ Bar: () => null,
+ XAxis: () => null,
+ YAxis: () => null,
+ Tooltip: () => null,
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Legend: () => null,
+}));
+
+// Mock data matching useReportsSummary's return type (ReportsSummary)
+const mockReportsSummary = {
+ total: 25,
+ byStatus: {
+ active: 15,
+ assigned: 8,
+ maintenance: 2,
+ },
+ byCategory: [
+ { name: 'Laptops', count: 10 },
+ { name: 'Phones', count: 8 },
+ { name: 'Furniture', count: 7 },
+ ],
+ byDepartment: [
+ { name: 'Engineering', count: 12 },
+ { name: 'Marketing', count: 8 },
+ { name: 'Sales', count: 5 },
+ ],
+ recent: [],
+};
+
+describe('DashboardCharts', () => {
+ it('renders correctly with the provided data summary', () => {
+ render();
+
+ // Verify all chart sections are present
+ expect(screen.getByText('Status Distribution')).toBeInTheDocument();
+ expect(screen.getByText('Assets by Category')).toBeInTheDocument();
+ expect(screen.getByText('Assets by Department')).toBeInTheDocument();
+
+ // Verify status data is displayed in table toggle view (default is chart, but toggle exists)
+ expect(screen.getByText('active')).toBeInTheDocument();
+ expect(screen.getByText('15')).toBeInTheDocument();
+ expect(screen.getByText('assigned')).toBeInTheDocument();
+ expect(screen.getByText('8')).toBeInTheDocument();
+ expect(screen.getByText('maintenance')).toBeInTheDocument();
+ expect(screen.getByText('2')).toBeInTheDocument();
+ });
+
+ it('toggles to table view when "Show data table" is clicked for status distribution', () => {
+ render();
+
+ // Find and click the toggle button for status donut
+ const toggleButton = screen.getAllByText('Show data table')[0];
+ fireEvent.click(toggleButton);
+
+ // Verify table is displayed instead of chart
+ expect(screen.getByText('Status')).toBeInTheDocument();
+ expect(screen.getByText('Count')).toBeInTheDocument();
+ expect(screen.getByText('active')).toBeInTheDocument();
+ expect(screen.getByText('15')).toBeInTheDocument();
+
+ // Verify button text toggles back to "Show chart"
+ expect(screen.getByText('Show chart')).toBeInTheDocument();
+ });
+
+ it('toggles to table view when "Show data table" is clicked for category chart', () => {
+ render();
+
+ // Find and click the toggle button for category bar chart
+ const toggleButton = screen.getAllByText('Show data table')[1];
+ fireEvent.click(toggleButton);
+
+ // Verify table is displayed
+ expect(screen.getByText('Category')).toBeInTheDocument();
+ expect(screen.getByText('Count')).toBeInTheDocument();
+ expect(screen.getByText('Laptops')).toBeInTheDocument();
+ expect(screen.getByText('10')).toBeInTheDocument();
+ });
+
+ it('shows "No category data" message when category data is empty', () => {
+ const emptyData = {
+ ...mockReportsSummary,
+ byCategory: [],
+ };
+
+ render();
+ expect(screen.getByText('No category data')).toBeInTheDocument();
+ });
+
+ it('shows "No department data" message when department data is empty', () => {
+ const emptyData = {
+ ...mockReportsSummary,
+ byDepartment: [],
+ };
+
+ render();
+ expect(screen.getByText('No department data')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/features/Dashboard/DateRangeSelector.spec.tsx b/frontend/features/Dashboard/DateRangeSelector.spec.tsx
new file mode 100644
index 00000000..5f223b6f
--- /dev/null
+++ b/frontend/features/Dashboard/DateRangeSelector.spec.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { DateRangeSelector } from './DateRangeSelector';
+
+// Mock Next.js navigation hooks
+const mockPush = jest.fn();
+const mockUseSearchParams = jest.fn();
+const mockUsePathname = jest.fn(() => '/dashboard');
+
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: mockPush,
+ }),
+ usePathname: () => mockUsePathname(),
+ useSearchParams: () => mockUseSearchParams(),
+}));
+
+describe('DateRangeSelector', () => {
+ beforeEach(() => {
+ // Clear all mocks before each test
+ jest.clearAllMocks();
+ // Default search params (30d preset)
+ mockUseSearchParams.mockReturnValue(new URLSearchParams('preset=30d'));
+ });
+
+ it('renders all preset buttons correctly', () => {
+ render();
+
+ // Verify all preset buttons are present
+ expect(screen.getByText('7 days')).toBeInTheDocument();
+ expect(screen.getByText('30 days')).toBeInTheDocument();
+ expect(screen.getByText('90 days')).toBeInTheDocument();
+ expect(screen.getByText('1 year')).toBeInTheDocument();
+
+ // Verify the 30d button is active (aria-pressed=true)
+ const activeButton = screen.getByText('30 days');
+ expect(activeButton).toHaveAttribute('aria-pressed', 'true');
+ });
+
+ it('calls onChange and updates URL search params when a preset is clicked', () => {
+ const mockOnChange = jest.fn();
+ render();
+
+ // Click on "7 days" preset
+ const sevenDayButton = screen.getByText('7 days');
+ fireEvent.click(sevenDayButton);
+
+ // Verify router.push was called with correct search params
+ expect(mockPush).toHaveBeenCalled();
+ const pushedUrl = mockPush.mock.calls[0][0];
+ expect(pushedUrl).toContain('preset=7d');
+ expect(pushedUrl).toContain('from=');
+ expect(pushedUrl).toContain('to=');
+
+ // Verify onChange was called with the correct date range
+ expect(mockOnChange).toHaveBeenCalled();
+ const range = mockOnChange.mock.calls[0][0];
+ expect(range.from).toBeDefined();
+ expect(range.to).toBeDefined();
+ });
+
+ it('updates to custom range when date inputs are changed', () => {
+ const mockOnChange = jest.fn();
+ render();
+
+ // Get the date inputs
+ const fromInput = screen.getByLabelText('Start date');
+ const toInput = screen.getByLabelText('End date');
+
+ // Change the from date
+ fireEvent.change(fromInput, { target: { value: '2024-01-01' } });
+
+ // Verify it switched to custom preset
+ expect(mockPush).toHaveBeenCalled();
+ let pushedUrl = mockPush.mock.calls[0][0];
+ expect(pushedUrl).toContain('preset=custom');
+ expect(pushedUrl).toContain('from=2024-01-01');
+
+ // Change the to date
+ fireEvent.change(toInput, { target: { value: '2024-01-31' } });
+
+ // Verify URL is updated with new to date
+ pushedUrl = mockPush.mock.calls[1][0];
+ expect(pushedUrl).toContain('preset=custom');
+ expect(pushedUrl).toContain('to=2024-01-31');
+
+ // Verify onChange was called twice (once for each change)
+ expect(mockOnChange).toHaveBeenCalledTimes(2);
+ });
+
+ it('maintains custom preset when custom dates are already in URL', () => {
+ // Set search params to custom range
+ mockUseSearchParams.mockReturnValue(new URLSearchParams('preset=custom&from=2024-01-01&to=2024-01-31'));
+
+ render();
+
+ // Verify the date inputs have the correct values
+ const fromInput = screen.getByLabelText('Start date') as HTMLInputElement;
+ const toInput = screen.getByLabelText('End date') as HTMLInputElement;
+
+ expect(fromInput.value).toBe('2024-01-01');
+ expect(toInput.value).toBe('2024-01-31');
+ });
+
+ it('correctly marks the active preset button based on URL params', () => {
+ // Set search params to 90d
+ mockUseSearchParams.mockReturnValue(new URLSearchParams('preset=90d'));
+
+ render();
+
+ // Verify 90 days button is active
+ const activeButton = screen.getByText('90 days');
+ expect(activeButton).toHaveAttribute('aria-pressed', 'true');
+
+ // Other buttons should not be active
+ expect(screen.getByText('7 days')).toHaveAttribute('aria-pressed', 'false');
+ expect(screen.getByText('30 days')).toHaveAttribute('aria-pressed', 'false');
+ expect(screen.getByText('1 year')).toHaveAttribute('aria-pressed', 'false');
+ });
+});
\ No newline at end of file
diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js
index a9874b1b..d4abab8d 100644
--- a/frontend/jest.setup.js
+++ b/frontend/jest.setup.js
@@ -1 +1,2 @@
// jest.setup.js
+import '@testing-library/jest-dom';
\ No newline at end of file