Skip to content

Commit c3d28d4

Browse files
committed
test(Tasks): Add Priority tests with shared test utilities
1 parent c5b0bb8 commit c3d28d4

4 files changed

Lines changed: 239 additions & 1 deletion

File tree

frontend/jest.config.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ module.exports = {
1111
transformIgnorePatterns: ['/node_modules/(?!react-toastify)'],
1212
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
1313
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
14-
testMatch: ['**/__tests__/**/*.{ts,tsx}', '**/?(*.)+(spec|test).{ts,tsx}'],
14+
testMatch: [
15+
'**/__tests__/**/*.+(test|spec).{ts,tsx}',
16+
'**/?(*.)+(spec|test).{ts,tsx}',
17+
],
1518
transform: {
1619
'^.+\\.tsx?$': [
1720
'ts-jest',
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
render,
3+
screen,
4+
fireEvent,
5+
waitFor,
6+
within,
7+
} from '@testing-library/react';
8+
import { Tasks } from '../../Tasks';
9+
import { openTaskDialog, getRowAndClickEdit } from '../test-utils/helper';
10+
import { createMockProps } from '../test-utils/setup';
11+
12+
jest.mock('react-toastify', () =>
13+
require('../test-utils/setup').createToastMock()
14+
);
15+
jest.mock('../../tasks-utils', () =>
16+
require('../test-utils/setup').createTasksUtilsMock()
17+
);
18+
jest.mock('../../hooks', () =>
19+
require('../test-utils/setup').createHooksMock()
20+
);
21+
jest.mock(
22+
'@/components/ui/select',
23+
() => require('../test-utils/setup').selectMock
24+
);
25+
26+
describe('Priority Editing', () => {
27+
const { toast } = require('react-toastify');
28+
const hooks = require('../../hooks');
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
test('should save selected priority value to backend', async () => {
35+
render(<Tasks {...createMockProps()} />);
36+
await openTaskDialog('Task 12');
37+
38+
const priorityRow = getRowAndClickEdit('Priority:');
39+
const select = within(priorityRow).getByTestId('project-select');
40+
fireEvent.change(select, { target: { value: 'H' } });
41+
42+
const saveButton = screen.getByLabelText('save');
43+
fireEvent.click(saveButton);
44+
45+
await waitFor(() => {
46+
expect(hooks.modifyTaskOnBackend).toHaveBeenCalledWith(
47+
expect.objectContaining({ priority: 'H' })
48+
);
49+
});
50+
});
51+
52+
test('should send empty string when NONE priority is selected', async () => {
53+
render(<Tasks {...createMockProps()} />);
54+
await openTaskDialog('Task 12');
55+
const priorityRow = getRowAndClickEdit('Priority:');
56+
57+
const select = within(priorityRow).getByTestId('project-select');
58+
fireEvent.change(select, { target: { value: 'NONE' } });
59+
60+
const saveButton = screen.getByLabelText('save');
61+
fireEvent.click(saveButton);
62+
63+
await waitFor(() => {
64+
expect(hooks.modifyTaskOnBackend).toHaveBeenCalledWith(
65+
expect.objectContaining({
66+
priority: '',
67+
})
68+
);
69+
});
70+
});
71+
72+
test('should show error toast when save fails', async () => {
73+
hooks.modifyTaskOnBackend.mockRejectedValueOnce(new Error('Network error'));
74+
75+
render(<Tasks {...createMockProps()} />);
76+
await openTaskDialog('Task 12');
77+
const priorityRow = getRowAndClickEdit('Priority:');
78+
79+
const select = within(priorityRow).getByTestId('project-select');
80+
fireEvent.change(select, { target: { value: 'H' } });
81+
82+
const saveButton = screen.getByLabelText('save');
83+
fireEvent.click(saveButton);
84+
85+
await waitFor(() => {
86+
expect(toast.error).toHaveBeenCalledWith(
87+
expect.stringContaining('Failed to update priority')
88+
);
89+
});
90+
});
91+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { screen, fireEvent, within } from '@testing-library/react';
2+
3+
export const openTaskDialog = async (taskDescription: string) => {
4+
await screen.findByText(taskDescription);
5+
fireEvent.click(screen.getByText(taskDescription));
6+
await screen.findByText('Description:');
7+
};
8+
9+
export const getRowAndClickEdit = (fieldLabel: string) => {
10+
const row = screen.getByText(fieldLabel).closest('tr') as HTMLElement;
11+
const editButton = within(row).getByLabelText('edit');
12+
fireEvent.click(editButton);
13+
return row;
14+
};
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
export const createMockProps = () => ({
2+
origin: '',
3+
email: 'test@example.com',
4+
encryptionSecret: 'mockEncryptionSecret',
5+
UUID: 'mockUUID',
6+
isLoading: false,
7+
setIsLoading: jest.fn(),
8+
});
9+
10+
export const mockTasks = [
11+
...Array.from({ length: 12 }, (_, i) => ({
12+
id: i + 1,
13+
description: `Task ${i + 1}`,
14+
status: 'pending',
15+
project: i % 2 === 0 ? 'ProjectA' : 'ProjectB',
16+
tags: i % 3 === 0 ? ['tag1'] : ['tag2'],
17+
uuid: `uuid-${i + 1}`,
18+
due: i === 0 ? '20200101T120000Z' : undefined,
19+
})),
20+
{
21+
id: 13,
22+
description:
23+
'Task 13: Prepare quarterly financial analysis report for review',
24+
status: 'pending',
25+
project: 'Finance',
26+
tags: ['report', 'analysis'],
27+
uuid: 'uuid-corp-1',
28+
},
29+
{
30+
id: 14,
31+
description: 'Task 14: Schedule client onboarding meeting with Sales team',
32+
status: 'pending',
33+
project: 'Sales',
34+
tags: ['meeting', 'client'],
35+
uuid: 'uuid-corp-2',
36+
},
37+
{
38+
id: 15,
39+
description:
40+
'Task 15: Draft technical documentation for API integration module',
41+
status: 'pending',
42+
project: 'Engineering',
43+
tags: ['documentation', 'api'],
44+
uuid: 'uuid-corp-3',
45+
},
46+
{
47+
id: 16,
48+
description: 'Completed Task 1',
49+
status: 'completed',
50+
project: 'ProjectA',
51+
tags: ['completed'],
52+
uuid: 'uuid-completed-1',
53+
},
54+
{
55+
id: 17,
56+
description: 'Deleted Task 1',
57+
status: 'deleted',
58+
project: 'ProjectB',
59+
tags: ['deleted'],
60+
uuid: 'uuid-deleted-1',
61+
},
62+
];
63+
64+
export const createToastMock = () => ({
65+
toast: {
66+
success: jest.fn(),
67+
error: jest.fn(),
68+
},
69+
});
70+
71+
export const createTasksUtilsMock = () => {
72+
const originalModule = jest.requireActual('../../tasks-utils');
73+
return {
74+
...originalModule,
75+
markTaskAsCompleted: jest.fn(),
76+
bulkMarkTasksAsCompleted: jest.fn().mockResolvedValue(true),
77+
markTaskAsDeleted: jest.fn(),
78+
bulkMarkTasksAsDeleted: jest.fn().mockResolvedValue(true),
79+
getTimeSinceLastSync: jest
80+
.fn()
81+
.mockReturnValue('Last updated 5 minutes ago'),
82+
hashKey: jest.fn((key: string) => `mockHashedKey-${key}`),
83+
getPinnedTasks: jest.fn().mockReturnValue(new Set()),
84+
togglePinnedTask: jest.fn(),
85+
};
86+
};
87+
88+
export const createHooksMock = () => ({
89+
TasksDatabase: jest.fn(() => ({
90+
tasks: {
91+
where: jest.fn(() => ({
92+
equals: jest.fn(() => ({
93+
toArray: jest.fn().mockResolvedValue(mockTasks),
94+
delete: jest.fn().mockResolvedValue(undefined),
95+
})),
96+
})),
97+
bulkPut: jest.fn().mockResolvedValue(undefined),
98+
},
99+
transaction: jest.fn(async (_mode, _table, callback) => {
100+
await callback();
101+
return Promise.resolve();
102+
}),
103+
})),
104+
fetchTaskwarriorTasks: jest.fn().mockResolvedValue([]),
105+
addTaskToBackend: jest.fn().mockResolvedValue({}),
106+
editTaskOnBackend: jest.fn().mockResolvedValue({}),
107+
modifyTaskOnBackend: jest.fn().mockResolvedValue({}),
108+
});
109+
110+
export const selectMock = {
111+
Select: ({ children, onValueChange, value }: any) => (
112+
<select
113+
data-testid="project-select"
114+
value={value}
115+
onChange={(e) => onValueChange?.(e.target.value)}
116+
>
117+
{children}
118+
</select>
119+
),
120+
SelectTrigger: ({ children }: any) => children,
121+
SelectValue: ({ placeholder }: any) => (
122+
<option value="" disabled hidden>
123+
{placeholder}
124+
</option>
125+
),
126+
SelectContent: ({ children }: any) => children,
127+
SelectItem: ({ value, children }: any) => (
128+
<option value={value}>{children}</option>
129+
),
130+
};

0 commit comments

Comments
 (0)