Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/frontend-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ jobs:
- name: Install dependencies
run: npm ci
working-directory: frontend

# 1. RUN ONLY TESTS ON PUSH
- name: Run frontend tests
if: github.event_name == 'push'
run: npm test
working-directory: frontend

# 2. RUN COVERAGE ON PRs
- name: Run jest with coverage
if: github.event_name == 'pull_request'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Task } from '@/components/utils/types';
import { handleLogout, deleteAllTasks } from '../navbar-utils';
import { toast } from 'react-toastify';

// Mock external dependencies
// Toast mock
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
Expand All @@ -11,18 +12,29 @@ jest.mock('react-toastify', () => ({
},
}));

// Dexie mock
jest.mock('dexie', () => {
return jest.fn().mockImplementation(() => ({
const mockCount = jest.fn();
const mockDelete = jest.fn();

const DexieMock = jest.fn().mockImplementation(() => ({
version: jest.fn().mockReturnThis(),
stores: jest.fn().mockReturnThis(),
table: jest.fn().mockReturnValue({
where: jest.fn().mockReturnThis(),
equals: jest.fn().mockReturnThis(),
delete: jest.fn().mockResolvedValue(undefined), // simulates delete success
count: mockCount,
delete: mockDelete,
}),
}));

return Object.assign(DexieMock, {
__mockCount: mockCount,
__mockDelete: mockDelete,
});
});

// URL mock
jest.mock('@/components/utils/URLs.ts', () => ({
url: {
backendURL: 'http://localhost:3000/',
Expand All @@ -32,12 +44,26 @@ jest.mock('@/components/utils/URLs.ts', () => ({
global.fetch = jest.fn();

describe('navbar-utils', () => {
const mockToast = toast as jest.Mocked<typeof toast>;
const Dexie = require('dexie');
const mockCount = Dexie.__mockCount as jest.Mock;
const mockDelete = Dexie.__mockDelete as jest.Mock;

beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});

beforeEach(() => {
mockToast.info.mockReturnValue('toast-id' as any);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('handleLogout', () => {
it('should call fetch with correct URL and redirect on success', async () => {
it('calls fetch with correct URL and redirects on success', async () => {
(fetch as jest.Mock).mockResolvedValue({ ok: true });

await handleLogout();
Expand All @@ -46,47 +72,78 @@ describe('navbar-utils', () => {
method: 'POST',
credentials: 'include',
});

expect(window.location.href).toBe('http://localhost/');
});

it('should log an error if fetch fails', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
it('logs error when response is not ok', async () => {
const spy = jest.spyOn(console, 'error').mockImplementation();

(fetch as jest.Mock).mockResolvedValue({ ok: false });

await handleLogout();

expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to logout');
consoleErrorSpy.mockRestore();
expect(spy).toHaveBeenCalledWith('Failed to logout');
spy.mockRestore();
});

it('should log an error if fetch throws an exception', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
it('logs error when fetch throws exception', async () => {
const spy = jest.spyOn(console, 'error').mockImplementation();

(fetch as jest.Mock).mockRejectedValue(new Error('Network error'));

await handleLogout();

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error logging out:',
expect.any(Error)
);
consoleErrorSpy.mockRestore();
expect(spy).toHaveBeenCalledWith('Error logging out:', expect.any(Error));
spy.mockRestore();
});
});

describe('deleteAllTasks', () => {
it('should delete tasks without error', async () => {
const props = {
imgurl: '',
email: '[email protected]',
encryptionSecret: '',
origin: '',
UUID: '',
tasks: [] as Task[] | null,
};

await expect(deleteAllTasks(props)).resolves.toBeUndefined();
const props = {
imgurl: '',
email: '[email protected]',
encryptionSecret: '',
origin: '',
UUID: '',
tasks: [] as Task[] | null,
};

it('shows error toast when no tasks exist', async () => {
mockCount.mockResolvedValueOnce(0);

await deleteAllTasks(props);

expect(mockToast.info).toHaveBeenCalled();
expect(mockToast.update).toHaveBeenCalledWith(
'toast-id',
expect.objectContaining({ type: 'error' })
);
});

it('deletes tasks and shows success toast when tasks exist', async () => {
mockCount.mockResolvedValueOnce(3);
mockDelete.mockResolvedValueOnce(undefined);

await deleteAllTasks(props);

expect(mockDelete).toHaveBeenCalled();
expect(mockToast.update).toHaveBeenCalledWith(
'toast-id',
expect.objectContaining({ type: 'success' })
);
});

it('shows error toast when deletion fails', async () => {
mockCount.mockResolvedValueOnce(2);
mockDelete.mockRejectedValueOnce(new Error('DB error'));

await deleteAllTasks(props);

expect(mockToast.update).toHaveBeenCalledWith(
'toast-id',
expect.objectContaining({ type: 'error' })
);
});
});
});
42 changes: 21 additions & 21 deletions frontend/src/components/HomeComponents/Tasks/ReportsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@ export const ReportsView: React.FC<ReportsViewProps> = ({ tasks }) => {
);

const countStatuses = (filterDate: Date) => {
return tasks
.filter((task) => {
return tasks.reduce(
(acc, task) => {
if (task.status === 'pending' && isOverdue(task.due ?? '')) {
acc.overdue += 1;
return acc;
}

const taskDateStr = task.end || task.due || task.entry;
if (!taskDateStr) return false;
if (!taskDateStr) return acc;

const parsedDate = parseTaskwarriorDate(taskDateStr);
if (!parsedDate) return false;
if (!parsedDate) return acc;

const modifiedDate = getStartOfDay(parsedDate);
return modifiedDate >= filterDate;
})
.reduce(
(acc, task) => {
if (task.status === 'completed') {
acc.completed += 1;
} else if (task.status === 'pending') {
if (isOverdue(task.due)) {
acc.overdue += 1;
} else {
acc.ongoing += 1;
}
}
return acc;
},
{ completed: 0, ongoing: 0, overdue: 0 }
);
if (modifiedDate < filterDate) return acc;

if (task.status === 'completed') {
acc.completed += 1;
} else if (task.status === 'pending') {
acc.ongoing += 1;
}

return acc;
},
{ completed: 0, ongoing: 0, overdue: 0 }
);
};

const dailyData = [{ name: 'Today', ...countStatuses(today) }];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa
ongoing: 1
</span>
<span>
overdue: 0
overdue: 3
</span>
</div>
<div
Expand Down Expand Up @@ -766,7 +766,7 @@ exports[`ReportsView Component using Snapshot renders correctly with only severa
ongoing: 1
</span>
<span>
overdue: 1
overdue: 3
</span>
</div>
<div
Expand Down
Loading