Skip to content
Open
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
5 changes: 4 additions & 1 deletion frontend/jest.config.cjs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated testMatch to only pick up .test.tsx or .spec.tsx files, prevents setup.tsx and helper.ts as test files

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ module.exports = {
transformIgnorePatterns: ['/node_modules/(?!react-toastify)'],
setupFilesAfterEnv: ['<rootDir>/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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<Tasks {...createMockProps()} />);
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(<Tasks {...createMockProps()} />);
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(<Tasks {...createMockProps()} />);
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')
);
});
});
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reusable helper functions like opening dialogs and clicking edit buttons, keeps test code DRY and readable

Original file line number Diff line number Diff line change
@@ -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;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shared mocks and test data for tests, will add more utilities as we add more test files (Recur, Reports, etc.)

Original file line number Diff line number Diff line change
@@ -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) => (
<select
data-testid="project-select"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test-id as per your suggestion

value={value}
onChange={(e) => onValueChange?.(e.target.value)}
>
{children}
</select>
),
SelectTrigger: ({ children }: any) => children,
SelectValue: ({ placeholder }: any) => (
<option value="" disabled hidden>
{placeholder}
</option>
),
SelectContent: ({ children }: any) => children,
SelectItem: ({ value, children }: any) => (
<option value={value}>{children}</option>
),
};
Loading