diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx
new file mode 100644
index 00000000..738dd1e2
--- /dev/null
+++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportChart.test.tsx
@@ -0,0 +1,209 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { ReportChart } from '../ReportChart';
+import * as downloadUtils from '../report-download-utils';
+
+jest.mock('recharts', () => ({
+ ResponsiveContainer: ({ children }: any) => (
+
{children}
+ ),
+ BarChart: ({ children, data }: any) => (
+
+ {children}
+
+ ),
+ Bar: ({ dataKey, fill, name }: any) => (
+
+ ),
+ XAxis: ({ dataKey }: any) => ,
+ YAxis: () => ,
+ Tooltip: () => ,
+ Legend: () => ,
+ CartesianGrid: () => ,
+}));
+
+jest.mock('../report-download-utils', () => ({
+ exportReportToCSV: jest.fn(),
+ exportChartToPNG: jest.fn().mockResolvedValue(undefined),
+}));
+
+describe('ReportChart', () => {
+ const mockData = [{ name: 'Today', completed: 5, ongoing: 3 }];
+
+ const defaultProps = {
+ data: mockData,
+ title: 'Daily Report',
+ chartId: 'daily-report-chart',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Rendering', () => {
+ it('renders chart with title', () => {
+ render();
+ expect(screen.getByText('Daily Report')).toBeInTheDocument();
+ });
+
+ it('renders CSV export button', () => {
+ render();
+ const csvButton = screen.getByTitle('Download as CSV');
+ expect(csvButton).toBeInTheDocument();
+ });
+
+ it('renders PNG export button', () => {
+ render();
+ const pngButton = screen.getByTitle('Download as PNG');
+ expect(pngButton).toBeInTheDocument();
+ });
+
+ it('renders bar chart with data', () => {
+ render();
+ const barChart = screen.getByTestId('bar-chart');
+ expect(barChart).toBeInTheDocument();
+ expect(barChart.getAttribute('data-chart-data')).toBe(
+ JSON.stringify(mockData)
+ );
+ });
+
+ it('renders responsive container', () => {
+ render();
+ expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
+ });
+ });
+
+ describe('Chart Elements', () => {
+ it('displays completed bar with correct color', () => {
+ render();
+ const completedBar = screen.getByTestId('bar-completed');
+ expect(completedBar).toHaveAttribute('data-fill', '#E776CB');
+ expect(completedBar).toHaveAttribute('data-name', 'Completed');
+ });
+
+ it('displays ongoing bar with correct color', () => {
+ render();
+ const ongoingBar = screen.getByTestId('bar-ongoing');
+ expect(ongoingBar).toHaveAttribute('data-fill', '#5FD9FA');
+ expect(ongoingBar).toHaveAttribute('data-name', 'Ongoing');
+ });
+
+ it('renders chart axes', () => {
+ render();
+ expect(screen.getByTestId('x-axis')).toBeInTheDocument();
+ expect(screen.getByTestId('y-axis')).toBeInTheDocument();
+ });
+
+ it('renders legend', () => {
+ render();
+ expect(screen.getByTestId('legend')).toBeInTheDocument();
+ });
+
+ it('renders tooltip', () => {
+ render();
+ expect(screen.getByTestId('tooltip')).toBeInTheDocument();
+ });
+
+ it('renders cartesian grid', () => {
+ render();
+ expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument();
+ });
+ });
+
+ describe('CSV Export', () => {
+ it('triggers CSV export when button clicked', () => {
+ render();
+ const csvButton = screen.getByTitle('Download as CSV');
+
+ fireEvent.click(csvButton);
+
+ expect(downloadUtils.exportReportToCSV).toHaveBeenCalledWith(
+ mockData,
+ 'Daily Report'
+ );
+ });
+
+ it('does not disable CSV button during PNG export', async () => {
+ (downloadUtils.exportChartToPNG as jest.Mock).mockImplementation(
+ () => new Promise((resolve) => setTimeout(resolve, 100))
+ );
+
+ render();
+ const pngButton = screen.getByTitle('Download as PNG');
+ const csvButton = screen.getByTitle('Download as CSV');
+
+ fireEvent.click(pngButton);
+
+ expect(csvButton).toBeDisabled();
+ });
+ });
+
+ describe('PNG Export', () => {
+ it('triggers PNG export when button clicked', async () => {
+ render();
+ const pngButton = screen.getByTitle('Download as PNG');
+
+ fireEvent.click(pngButton);
+
+ await waitFor(() => {
+ expect(downloadUtils.exportChartToPNG).toHaveBeenCalledWith(
+ 'daily-report-chart',
+ 'Daily Report'
+ );
+ });
+ });
+
+ it('shows loading spinner during export', async () => {
+ (downloadUtils.exportChartToPNG as jest.Mock).mockImplementation(
+ () => new Promise((resolve) => setTimeout(resolve, 100))
+ );
+
+ render();
+ const pngButton = screen.getByTitle('Download as PNG');
+
+ fireEvent.click(pngButton);
+
+ expect(pngButton).toBeDisabled();
+
+ await waitFor(() => {
+ expect(downloadUtils.exportChartToPNG).toHaveBeenCalled();
+ });
+ });
+
+ it('re-enables button after export completes', async () => {
+ render();
+ const pngButton = screen.getByTitle('Download as PNG');
+
+ fireEvent.click(pngButton);
+
+ await waitFor(() => {
+ expect(pngButton).not.toBeDisabled();
+ });
+ });
+ });
+
+ describe('Multiple Data Points', () => {
+ it('handles multiple data entries', () => {
+ const multiData = [
+ { name: 'Week 1', completed: 10, ongoing: 5 },
+ { name: 'Week 2', completed: 8, ongoing: 7 },
+ { name: 'Week 3', completed: 12, ongoing: 3 },
+ ];
+
+ render();
+
+ const barChart = screen.getByTestId('bar-chart');
+ expect(barChart.getAttribute('data-chart-data')).toBe(
+ JSON.stringify(multiData)
+ );
+ });
+
+ it('handles zero values', () => {
+ const zeroData = [{ name: 'Today', completed: 0, ongoing: 0 }];
+
+ render();
+
+ const barChart = screen.getByTestId('bar-chart');
+ expect(barChart).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx
new file mode 100644
index 00000000..d8fb482c
--- /dev/null
+++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx
@@ -0,0 +1,244 @@
+import { render, screen } from '@testing-library/react';
+import { ReportsView } from '../ReportsView';
+import { Task } from '@/components/utils/types';
+
+jest.mock('../ReportChart', () => ({
+ ReportChart: jest.fn(({ title, data, chartId }) => (
+
+
{title}
+
{JSON.stringify(data)}
+
+ )),
+}));
+
+describe('ReportsView', () => {
+ const createMockTask = (overrides: Partial = {}): Task => ({
+ id: 1,
+ description: 'Test task',
+ project: 'Test',
+ tags: [],
+ status: 'pending',
+ uuid: 'test-uuid',
+ urgency: 0,
+ priority: 'M',
+ due: '',
+ start: '',
+ end: '',
+ entry: '',
+ wait: '',
+ modified: '',
+ depends: [],
+ rtype: '',
+ recur: '',
+ email: 'test@example.com',
+ ...overrides,
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Rendering', () => {
+ it('renders all three report charts', () => {
+ const tasks = [createMockTask()];
+ render();
+
+ expect(screen.getByText('Daily Report')).toBeInTheDocument();
+ expect(screen.getByText('Weekly Report')).toBeInTheDocument();
+ expect(screen.getByText('Monthly Report')).toBeInTheDocument();
+ });
+
+ it('renders with empty tasks array', () => {
+ render();
+
+ expect(screen.getByText('Daily Report')).toBeInTheDocument();
+ expect(screen.getByText('Weekly Report')).toBeInTheDocument();
+ expect(screen.getByText('Monthly Report')).toBeInTheDocument();
+ });
+ });
+
+ describe('Data Calculation', () => {
+ it('counts completed tasks correctly', () => {
+ const today = new Date().toISOString();
+ const tasks = [
+ createMockTask({ status: 'completed', modified: today }),
+ createMockTask({ status: 'completed', modified: today }),
+ createMockTask({ status: 'pending', modified: today }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(2);
+ expect(data[0].ongoing).toBe(1);
+ });
+
+ it('counts pending tasks as ongoing', () => {
+ const today = new Date().toISOString();
+ const tasks = [
+ createMockTask({ status: 'pending', modified: today }),
+ createMockTask({ status: 'pending', modified: today }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].ongoing).toBe(2);
+ expect(data[0].completed).toBe(0);
+ });
+
+ it('filters tasks by date range correctly', () => {
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+ const lastWeek = new Date(today);
+ lastWeek.setDate(lastWeek.getDate() - 8);
+
+ const tasks = [
+ createMockTask({ status: 'completed', modified: today.toISOString() }),
+ createMockTask({
+ status: 'completed',
+ modified: yesterday.toISOString(),
+ }),
+ createMockTask({
+ status: 'completed',
+ modified: lastWeek.toISOString(),
+ }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const weeklyData = screen.getByTestId('weekly-report-chart-data');
+
+ const daily = JSON.parse(dailyData.textContent || '[]');
+ const weekly = JSON.parse(weeklyData.textContent || '[]');
+
+ expect(daily[0].completed).toBe(1);
+ expect(weekly[0].completed).toBeGreaterThanOrEqual(2);
+ });
+
+ it('uses modified date when available', () => {
+ const today = new Date().toISOString();
+ const tasks = [
+ createMockTask({
+ status: 'completed',
+ modified: today,
+ due: '2020-01-01T00:00:00Z',
+ }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(1);
+ });
+
+ it('falls back to due date when modified is not available', () => {
+ const today = new Date().toISOString();
+ const tasks = [
+ createMockTask({
+ status: 'completed',
+ modified: '',
+ due: today,
+ }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(1);
+ });
+
+ it('excludes tasks without modified or due dates', () => {
+ const tasks = [
+ createMockTask({
+ status: 'completed',
+ modified: '',
+ due: '',
+ }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(0);
+ expect(data[0].ongoing).toBe(0);
+ });
+
+ it('handles mixed statuses correctly', () => {
+ const today = new Date().toISOString();
+ const tasks = [
+ createMockTask({ status: 'completed', modified: today }),
+ createMockTask({ status: 'pending', modified: today }),
+ createMockTask({ status: 'deleted', modified: today }),
+ createMockTask({ status: 'recurring', modified: today }),
+ ];
+
+ render();
+
+ const dailyData = screen.getByTestId('daily-report-chart-data');
+ const data = JSON.parse(dailyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(1);
+ expect(data[0].ongoing).toBe(1);
+ });
+ });
+
+ describe('Time Ranges', () => {
+ it('correctly identifies weekly date range', () => {
+ const today = new Date();
+ const startOfWeek = new Date(today);
+ startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
+
+ const taskInWeek = new Date(startOfWeek);
+ taskInWeek.setDate(taskInWeek.getDate() + 1);
+
+ const tasks = [
+ createMockTask({
+ status: 'completed',
+ modified: taskInWeek.toISOString(),
+ }),
+ ];
+
+ render();
+
+ const weeklyData = screen.getByTestId('weekly-report-chart-data');
+ const data = JSON.parse(weeklyData.textContent || '[]');
+
+ expect(data[0].completed).toBeGreaterThanOrEqual(1);
+ });
+
+ it('correctly identifies monthly date range', () => {
+ const today = new Date();
+ const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
+
+ const taskInMonth = new Date(startOfMonth);
+ taskInMonth.setDate(taskInMonth.getDate() + 5);
+
+ const tasks = [
+ createMockTask({
+ status: 'completed',
+ modified: taskInMonth.toISOString(),
+ }),
+ ];
+
+ render();
+
+ const monthlyData = screen.getByTestId('monthly-report-chart-data');
+ const data = JSON.parse(monthlyData.textContent || '[]');
+
+ expect(data[0].completed).toBe(1);
+ });
+ });
+});
diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/report-download-utils.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/report-download-utils.test.ts
new file mode 100644
index 00000000..20f81323
--- /dev/null
+++ b/frontend/src/components/HomeComponents/Tasks/__tests__/report-download-utils.test.ts
@@ -0,0 +1,260 @@
+import {
+ exportReportToCSV,
+ exportChartToPNG,
+ ReportData,
+} from '../report-download-utils';
+import html2canvas from 'html2canvas';
+import { toast } from 'react-toastify';
+
+jest.mock('html2canvas');
+jest.mock('react-toastify', () => ({
+ toast: {
+ success: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+describe('report-download-utils', () => {
+ let mockLink: Partial;
+ let consoleErrorSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ global.URL.createObjectURL = jest.fn(() => 'blob:mock-url');
+ global.URL.revokeObjectURL = jest.fn() as any;
+
+ mockLink = {
+ href: '',
+ download: '',
+ style: {} as CSSStyleDeclaration,
+ click: jest.fn(),
+ };
+
+ const originalCreateElement = document.createElement.bind(document);
+ jest
+ .spyOn(document, 'createElement')
+ .mockImplementation((tagName: string) => {
+ if (tagName === 'a') {
+ return mockLink as HTMLAnchorElement;
+ }
+ return originalCreateElement(tagName);
+ });
+
+ jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation(() => null as any);
+ jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation(() => null as any);
+ });
+
+ afterEach(() => {
+ consoleErrorSpy.mockRestore();
+ jest.restoreAllMocks();
+ });
+
+ describe('exportReportToCSV', () => {
+ const mockData: ReportData[] = [
+ { name: 'Today', completed: 5, ongoing: 3 },
+ { name: 'This Week', completed: 10, ongoing: 7 },
+ ];
+
+ it('triggers download and shows success toast', () => {
+ exportReportToCSV(mockData, 'Daily Report');
+
+ expect(mockLink.click).toHaveBeenCalled();
+ expect(toast.success).toHaveBeenCalledWith(
+ 'Daily Report Report exported to CSV successfully!',
+ expect.objectContaining({
+ position: 'bottom-right',
+ autoClose: 3000,
+ })
+ );
+ });
+
+ it('sets correct filename pattern', () => {
+ exportReportToCSV(mockData, 'Weekly Report');
+
+ expect(mockLink.download).toMatch(
+ /ccsync-weekly-report-report-\d{4}-\d{2}-\d{2}\.csv/
+ );
+ });
+
+ it('cleans up DOM and URL resources', () => {
+ exportReportToCSV(mockData, 'Monthly Report');
+
+ expect(document.body.appendChild).toHaveBeenCalled();
+ expect(document.body.removeChild).toHaveBeenCalled();
+ expect(global.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url');
+ });
+
+ it('handles empty data array gracefully', () => {
+ exportReportToCSV([], 'Empty Report');
+
+ expect(toast.success).toHaveBeenCalled();
+ expect(mockLink.click).toHaveBeenCalled();
+ });
+
+ it('handles special characters in report name', () => {
+ exportReportToCSV(mockData, 'Report With Spaces');
+
+ expect(mockLink.download).toMatch(
+ /ccsync-report-with-spaces-report-\d{4}-\d{2}-\d{2}\.csv/
+ );
+ });
+
+ it('creates blob with correct type', () => {
+ const blobSpy = jest.spyOn(global, 'Blob');
+ exportReportToCSV(mockData, 'Test Report');
+
+ expect(blobSpy).toHaveBeenCalledWith(expect.any(Array), {
+ type: 'text/csv;charset=utf-8;',
+ });
+ });
+ });
+
+ describe('exportChartToPNG', () => {
+ const mockCanvas = {
+ toBlob: jest.fn((callback) => {
+ callback(new Blob(['fake-image'], { type: 'image/png' }));
+ }),
+ };
+
+ beforeEach(() => {
+ (html2canvas as jest.Mock).mockResolvedValue(mockCanvas);
+ });
+
+ it('finds and processes DOM element', async () => {
+ const mockElement = document.createElement('div');
+ mockElement.id = 'test-chart';
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ await exportChartToPNG('test-chart', 'Test Report');
+
+ expect(document.getElementById).toHaveBeenCalledWith('test-chart');
+ expect(html2canvas).toHaveBeenCalledWith(mockElement, {
+ backgroundColor: '#1c1c1c',
+ scale: 2,
+ logging: false,
+ useCORS: true,
+ });
+ });
+
+ it('triggers download and shows success toast', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ await exportChartToPNG('chart-id', 'Daily Report');
+
+ expect(mockLink.click).toHaveBeenCalled();
+ expect(toast.success).toHaveBeenCalledWith(
+ 'Daily Report Report exported to PNG successfully!',
+ expect.objectContaining({
+ position: 'bottom-right',
+ autoClose: 3000,
+ })
+ );
+ });
+
+ it('sets correct filename pattern', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ await exportChartToPNG('chart-id', 'Weekly Report');
+
+ expect(mockLink.download).toMatch(
+ /ccsync-weekly-report-report-\d{4}-\d{2}-\d{2}\.png/
+ );
+ });
+
+ it('cleans up resources after export', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ await exportChartToPNG('chart-id', 'Report');
+
+ expect(document.body.removeChild).toHaveBeenCalled();
+ expect(global.URL.revokeObjectURL).toHaveBeenCalled();
+ });
+
+ it('shows error when element not found', async () => {
+ jest.spyOn(document, 'getElementById').mockReturnValue(null);
+
+ await exportChartToPNG('non-existent', 'Report');
+
+ expect(toast.error).toHaveBeenCalledWith(
+ 'Failed to export PNG. Please try again.',
+ expect.any(Object)
+ );
+ expect(html2canvas).not.toHaveBeenCalled();
+ });
+
+ it('handles html2canvas errors', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+ (html2canvas as jest.Mock).mockRejectedValue(new Error('Canvas error'));
+
+ await exportChartToPNG('chart-id', 'Report');
+
+ expect(toast.error).toHaveBeenCalledWith(
+ 'Failed to export PNG. Please try again.',
+ expect.objectContaining({
+ position: 'bottom-right',
+ })
+ );
+ });
+
+ it('handles canvas.toBlob failure', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ const failingCanvas = {
+ toBlob: jest.fn((callback) => callback(null)),
+ };
+ (html2canvas as jest.Mock).mockResolvedValue(failingCanvas);
+
+ await exportChartToPNG('chart-id', 'Report');
+
+ expect(toast.error).toHaveBeenCalled();
+ });
+
+ it('creates PNG blob correctly', async () => {
+ const mockElement = document.createElement('div');
+ jest.spyOn(document, 'getElementById').mockReturnValue(mockElement);
+
+ await exportChartToPNG('chart-id', 'Report');
+
+ expect(mockCanvas.toBlob).toHaveBeenCalledWith(
+ expect.any(Function),
+ 'image/png'
+ );
+ });
+ });
+
+ describe('Helper Functions', () => {
+ const mockData: ReportData[] = [{ name: 'Test', completed: 1, ongoing: 2 }];
+
+ it('generates date-stamped filenames', () => {
+ exportReportToCSV(mockData, 'Test Report');
+
+ const filename = mockLink.download as string;
+ expect(filename).toContain('ccsync-');
+ expect(filename).toContain('-report-');
+ expect(filename).toMatch(/\d{4}-\d{2}-\d{2}/);
+ });
+
+ it('converts spaces to hyphens in filenames', () => {
+ exportReportToCSV(mockData, 'My Test Report');
+
+ expect(mockLink.download).toContain('my-test-report');
+ });
+
+ it('uses lowercase for report type in filename', () => {
+ exportReportToCSV(mockData, 'DAILY REPORT');
+
+ expect(mockLink.download).toContain('daily-report');
+ });
+ });
+});