diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index a018c6e0..d0d317fb 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -11,7 +11,10 @@ module.exports = { transformIgnorePatterns: ['/node_modules/(?!react-toastify)'], setupFilesAfterEnv: ['/jest.setup.ts'], collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], - testMatch: ['**/__tests__/**/*.{ts,tsx}', '**/?(*.)+(spec|test).{ts,tsx}'], + testMatch: [ + '**/__tests__/**/*.+(test|spec).{ts,tsx}', + '**/?(*.)+(spec|test).{ts,tsx}', + ], transform: { '^.+\\.tsx?$': [ 'ts-jest', diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks/Priority.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks/Priority.test.tsx new file mode 100644 index 00000000..c4bb0f7d --- /dev/null +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks/Priority.test.tsx @@ -0,0 +1,87 @@ +import { + render, + screen, + fireEvent, + waitFor, + within, +} from '@testing-library/react'; +import { Tasks } from '../../Tasks'; +import { openTaskDialog, getRowAndClickEdit } from '../test-utils/helper'; +import { createMockProps } from '../test-utils/setup'; + +jest.mock('react-toastify', () => + require('../test-utils/setup').createToastMock() +); +jest.mock('../../tasks-utils', () => + require('../test-utils/setup').createTasksUtilsMock() +); +jest.mock('../../hooks', () => + require('../test-utils/setup').createHooksMock() +); +jest.mock( + '@/components/ui/select', + () => require('../test-utils/setup').selectMock +); + +describe('Priority Editing', () => { + const { toast } = require('react-toastify'); + const hooks = require('../../hooks'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should save selected priority value to backend', async () => { + render(); + await openTaskDialog('Task 12'); + + const priorityRow = getRowAndClickEdit('Priority:'); + const select = within(priorityRow).getByTestId('project-select'); + fireEvent.change(select, { target: { value: 'H' } }); + + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); + + expect(hooks.modifyTaskOnBackend).toHaveBeenCalledWith( + expect.objectContaining({ priority: 'H' }) + ); + }); + + test('should send empty string when NONE priority is selected', async () => { + render(); + await openTaskDialog('Task 12'); + const priorityRow = getRowAndClickEdit('Priority:'); + + const select = within(priorityRow).getByTestId('project-select'); + fireEvent.change(select, { target: { value: 'NONE' } }); + + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); + + expect(hooks.modifyTaskOnBackend).toHaveBeenCalledWith( + expect.objectContaining({ + priority: '', + }) + ); + }); + + test('should show error toast when save fails', async () => { + hooks.modifyTaskOnBackend.mockRejectedValueOnce(new Error('Network error')); + + render(); + await openTaskDialog('Task 12'); + const priorityRow = getRowAndClickEdit('Priority:'); + + const select = within(priorityRow).getByTestId('project-select'); + fireEvent.change(select, { target: { value: 'H' } }); + + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to update priority') + ); + }); + }); +}); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/helper.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/helper.ts new file mode 100644 index 00000000..55d7e3e1 --- /dev/null +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/helper.ts @@ -0,0 +1,14 @@ +import { screen, fireEvent, within } from '@testing-library/react'; + +export const openTaskDialog = async (taskDescription: string) => { + await screen.findByText(taskDescription); + fireEvent.click(screen.getByText(taskDescription)); + await screen.findByText('Description:'); +}; + +export const getRowAndClickEdit = (fieldLabel: string) => { + const row = screen.getByText(fieldLabel).closest('tr') as HTMLElement; + const editButton = within(row).getByLabelText('edit'); + fireEvent.click(editButton); + return row; +}; diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/setup.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/setup.tsx new file mode 100644 index 00000000..309ce15c --- /dev/null +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/test-utils/setup.tsx @@ -0,0 +1,130 @@ +export const createMockProps = () => ({ + origin: '', + email: 'test@example.com', + encryptionSecret: 'mockEncryptionSecret', + UUID: 'mockUUID', + isLoading: false, + setIsLoading: jest.fn(), +}); + +export const mockTasks = [ + ...Array.from({ length: 12 }, (_, i) => ({ + id: i + 1, + description: `Task ${i + 1}`, + status: 'pending', + project: i % 2 === 0 ? 'ProjectA' : 'ProjectB', + tags: i % 3 === 0 ? ['tag1'] : ['tag2'], + uuid: `uuid-${i + 1}`, + due: i === 0 ? '20200101T120000Z' : undefined, + })), + { + id: 13, + description: + 'Task 13: Prepare quarterly financial analysis report for review', + status: 'pending', + project: 'Finance', + tags: ['report', 'analysis'], + uuid: 'uuid-corp-1', + }, + { + id: 14, + description: 'Task 14: Schedule client onboarding meeting with Sales team', + status: 'pending', + project: 'Sales', + tags: ['meeting', 'client'], + uuid: 'uuid-corp-2', + }, + { + id: 15, + description: + 'Task 15: Draft technical documentation for API integration module', + status: 'pending', + project: 'Engineering', + tags: ['documentation', 'api'], + uuid: 'uuid-corp-3', + }, + { + id: 16, + description: 'Completed Task 1', + status: 'completed', + project: 'ProjectA', + tags: ['completed'], + uuid: 'uuid-completed-1', + }, + { + id: 17, + description: 'Deleted Task 1', + status: 'deleted', + project: 'ProjectB', + tags: ['deleted'], + uuid: 'uuid-deleted-1', + }, +]; + +export const createToastMock = () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +}); + +export const createTasksUtilsMock = () => { + const originalModule = jest.requireActual('../../tasks-utils'); + return { + ...originalModule, + markTaskAsCompleted: jest.fn(), + bulkMarkTasksAsCompleted: jest.fn().mockResolvedValue(true), + markTaskAsDeleted: jest.fn(), + bulkMarkTasksAsDeleted: jest.fn().mockResolvedValue(true), + getTimeSinceLastSync: jest + .fn() + .mockReturnValue('Last updated 5 minutes ago'), + hashKey: jest.fn((key: string) => `mockHashedKey-${key}`), + getPinnedTasks: jest.fn().mockReturnValue(new Set()), + togglePinnedTask: jest.fn(), + }; +}; + +export const createHooksMock = () => ({ + TasksDatabase: jest.fn(() => ({ + tasks: { + where: jest.fn(() => ({ + equals: jest.fn(() => ({ + toArray: jest.fn().mockResolvedValue(mockTasks), + delete: jest.fn().mockResolvedValue(undefined), + })), + })), + bulkPut: jest.fn().mockResolvedValue(undefined), + }, + transaction: jest.fn(async (_mode, _table, callback) => { + await callback(); + return Promise.resolve(); + }), + })), + fetchTaskwarriorTasks: jest.fn().mockResolvedValue([]), + addTaskToBackend: jest.fn().mockResolvedValue({}), + editTaskOnBackend: jest.fn().mockResolvedValue({}), + modifyTaskOnBackend: jest.fn().mockResolvedValue({}), +}); + +export const selectMock = { + Select: ({ children, onValueChange, value }: any) => ( + + ), + SelectTrigger: ({ children }: any) => children, + SelectValue: ({ placeholder }: any) => ( + + ), + SelectContent: ({ children }: any) => children, + SelectItem: ({ value, children }: any) => ( + + ), +};