Skip to content

Commit 6ea279b

Browse files
authored
Merge pull request #1359 from Real-Dev-Squad/develop
Dev to Main Sync
2 parents 279087e + f1b64ac commit 6ea279b

File tree

10 files changed

+699
-11
lines changed

10 files changed

+699
-11
lines changed

__mocks__/db/extensionRequests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExtensionRequest } from '@/components/ExtensionRequest/ExtensionStatusModal';
1+
import { ExtensionRequest } from '@/interfaces/task.type';
22

33
export const now = Date.now();
44

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { ExtensionRequestForm } from '@/components/ExtensionRequest/ExtensionRequestForm';
4+
import { toast } from '@/helperFunctions/toast';
5+
6+
const mockCreateExtensionRequest = jest.fn();
7+
8+
jest.mock('@/app/services/tasksApi', () => ({
9+
useCreateExtensionRequestMutation: () => [
10+
mockCreateExtensionRequest,
11+
{ isLoading: false },
12+
],
13+
}));
14+
15+
jest.mock('@/helperFunctions/toast', () => ({
16+
toast: jest.fn(),
17+
ToastTypes: {
18+
SUCCESS: 'SUCCESS',
19+
ERROR: 'ERROR',
20+
},
21+
}));
22+
23+
describe('ExtensionRequestForm Component', () => {
24+
let mockOnClose: () => void;
25+
26+
beforeEach(() => {
27+
mockOnClose = jest.fn();
28+
29+
jest.clearAllMocks();
30+
});
31+
32+
const getDefaultProps = () => ({
33+
isOpen: true,
34+
onClose: mockOnClose,
35+
taskId: '123',
36+
assignee: 'john',
37+
oldEndsOn: new Date().getTime(),
38+
});
39+
40+
test('should render all fields correctly', () => {
41+
render(<ExtensionRequestForm {...getDefaultProps()} />);
42+
43+
expect(screen.getByTestId('extension-form-modal')).toBeInTheDocument();
44+
expect(screen.getByTestId('form-heading')).toHaveTextContent(
45+
'Extension Request Form'
46+
);
47+
expect(screen.getByTestId('reason-input')).toBeInTheDocument();
48+
expect(screen.getByTestId('new-eta-input')).toBeInTheDocument();
49+
expect(screen.getByTestId('title-input')).toBeInTheDocument();
50+
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
51+
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
52+
});
53+
54+
test(' should handle input change correctly', () => {
55+
render(<ExtensionRequestForm {...getDefaultProps()} />);
56+
57+
fireEvent.change(screen.getByTestId('reason-input'), {
58+
target: { value: 'Need more time', name: 'reason' },
59+
});
60+
fireEvent.change(screen.getByTestId('title-input'), {
61+
target: { value: 'Extension Title', name: 'title' },
62+
});
63+
64+
expect(screen.getByTestId('reason-input')).toHaveValue(
65+
'Need more time'
66+
);
67+
expect(screen.getByTestId('title-input')).toHaveValue(
68+
'Extension Title'
69+
);
70+
});
71+
72+
test('should disable submit button if newEndsOn <= oldEndsOn', () => {
73+
const props = {
74+
...getDefaultProps(),
75+
oldEndsOn: new Date('2025-01-01T10:00:00').getTime(),
76+
};
77+
render(<ExtensionRequestForm {...props} />);
78+
79+
fireEvent.change(screen.getByTestId('new-eta-input'), {
80+
target: {
81+
name: 'newEndsOn',
82+
value: '2024-01-01T10:00',
83+
},
84+
});
85+
86+
expect(screen.getByTestId('eta-error')).toBeInTheDocument();
87+
expect(screen.getByTestId('submit-button')).toBeDisabled();
88+
});
89+
90+
test('should submits form with correct data', async () => {
91+
const mockEta = '2025-12-31T18:29';
92+
const mockEtaTimestampInSeconds = Math.floor(
93+
new Date(mockEta).getTime() / 1000
94+
);
95+
96+
mockCreateExtensionRequest.mockReturnValueOnce({
97+
unwrap: () => Promise.resolve(),
98+
});
99+
100+
render(<ExtensionRequestForm {...getDefaultProps()} />);
101+
102+
fireEvent.change(screen.getByTestId('reason-input'), {
103+
target: { value: 'Valid reason', name: 'reason' },
104+
});
105+
106+
fireEvent.change(screen.getByTestId('title-input'), {
107+
target: { value: 'Valid title', name: 'title' },
108+
});
109+
110+
fireEvent.change(screen.getByTestId('new-eta-input'), {
111+
target: { value: mockEta, name: 'newEndsOn' },
112+
});
113+
114+
fireEvent.submit(screen.getByTestId('extension-form'));
115+
116+
await waitFor(() => {
117+
expect(mockCreateExtensionRequest).toHaveBeenCalledTimes(1);
118+
expect(mockCreateExtensionRequest).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
reason: 'Valid reason',
121+
title: 'Valid title',
122+
taskId: '123',
123+
assignee: 'john',
124+
newEndsOn: mockEtaTimestampInSeconds,
125+
})
126+
);
127+
});
128+
});
129+
130+
test('should not render the form when isOpen is false', () => {
131+
const props = { ...getDefaultProps(), isOpen: false };
132+
render(<ExtensionRequestForm {...props} />);
133+
expect(
134+
screen.queryByTestId('extension-form-modal')
135+
).not.toBeInTheDocument();
136+
});
137+
138+
test('should call onClose when cancel button clicked', () => {
139+
render(<ExtensionRequestForm {...getDefaultProps()} />);
140+
fireEvent.click(screen.getByTestId('cancel-button'));
141+
expect(mockOnClose).toHaveBeenCalledTimes(1);
142+
});
143+
144+
test('should show toast error on submit failure', async () => {
145+
mockCreateExtensionRequest.mockReturnValueOnce({
146+
unwrap: () => Promise.reject(new Error('Error submitting')),
147+
});
148+
149+
render(<ExtensionRequestForm {...getDefaultProps()} />);
150+
151+
fireEvent.change(screen.getByTestId('reason-input'), {
152+
target: { value: 'Valid reason', name: 'reason' },
153+
});
154+
fireEvent.change(screen.getByTestId('title-input'), {
155+
target: { value: 'Valid title', name: 'title' },
156+
});
157+
158+
fireEvent.submit(screen.getByTestId('extension-form'));
159+
160+
await waitFor(() => {
161+
expect(toast).toHaveBeenCalledWith('ERROR', expect.any(String));
162+
});
163+
});
164+
165+
test('should show toast success on successful form submission', async () => {
166+
mockCreateExtensionRequest.mockReturnValueOnce({
167+
unwrap: () => Promise.resolve(),
168+
});
169+
170+
render(<ExtensionRequestForm {...getDefaultProps()} />);
171+
172+
fireEvent.change(screen.getByTestId('reason-input'), {
173+
target: { value: 'Valid reason', name: 'reason' },
174+
});
175+
fireEvent.change(screen.getByTestId('title-input'), {
176+
target: { value: 'Valid title', name: 'title' },
177+
});
178+
fireEvent.change(screen.getByTestId('new-eta-input'), {
179+
target: { value: '2025-12-31T18:29', name: 'newEndsOn' },
180+
});
181+
182+
fireEvent.submit(screen.getByTestId('extension-form'));
183+
184+
await waitFor(() => {
185+
expect(toast).toHaveBeenCalledWith('SUCCESS', expect.any(String));
186+
expect(mockOnClose).toHaveBeenCalledTimes(1);
187+
});
188+
});
189+
190+
test('should handle new ETA date input change correctly', () => {
191+
const oldEndsOn = new Date('2025-01-01T10:00:00').getTime();
192+
const props = { ...getDefaultProps(), oldEndsOn };
193+
194+
render(<ExtensionRequestForm {...props} />);
195+
196+
const newEtaInput = screen.getByTestId('new-eta-input');
197+
const testDate = '2025-02-01T12:00';
198+
199+
fireEvent.change(newEtaInput, {
200+
target: {
201+
name: 'newEndsOn',
202+
value: testDate,
203+
},
204+
});
205+
206+
expect((newEtaInput as HTMLInputElement).value).toContain('2025-02-01');
207+
expect(screen.queryByTestId('eta-error')).not.toBeInTheDocument();
208+
expect(screen.getByTestId('submit-button')).toBeEnabled();
209+
});
210+
});

__tests__/Unit/Components/ExtensionRequest/ExtensionStatusModal.test.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
import { useGetSelfExtensionRequestsQuery } from '@/app/services/tasksApi';
88
import { mockExtensionRequests } from '../../../../__mocks__/db/extensionRequests';
99

10-
jest.mock(
11-
'@/app/services/tasksApi',
12-
() => ({
13-
useGetSelfExtensionRequestsQuery: jest.fn(),
14-
}),
15-
{ virtual: true }
16-
);
10+
jest.mock('@/app/services/tasksApi', () => ({
11+
useGetSelfExtensionRequestsQuery: jest.fn(),
12+
useCreateExtensionRequestMutation: jest.fn(() => [
13+
jest.fn(),
14+
{ isLoading: false },
15+
]),
16+
}));
1717

1818
const mockQuery = useGetSelfExtensionRequestsQuery as jest.Mock;
1919
const defaultProps = {
@@ -67,6 +67,7 @@ describe('ExtensionStatusModal Component', () => {
6767
fireEvent.keyDown(document, { key: 'Enter' });
6868
expect(defaultProps.onClose).not.toHaveBeenCalled();
6969
});
70+
7071
test('should render close button correctly', () => {
7172
setupTest({ isLoading: false, data: { allExtensionRequests: [] } });
7273

@@ -123,4 +124,51 @@ describe('ExtensionStatusModal Component', () => {
123124
const result = formatToRelativeTime(timestamp);
124125
expect(result).toBe('3 years ago');
125126
});
127+
128+
test('should open extension request form when request extension button is clicked', () => {
129+
const onCloseMock = jest.fn();
130+
mockQuery.mockReturnValue({
131+
isLoading: false,
132+
data: { allExtensionRequests: [] },
133+
});
134+
135+
const { rerender } = render(
136+
<ExtensionStatusModal {...defaultProps} onClose={onCloseMock} />
137+
);
138+
139+
const requestButton = screen.getByTestId('request-extension-button');
140+
fireEvent.click(requestButton);
141+
142+
expect(onCloseMock).toHaveBeenCalledTimes(1);
143+
144+
rerender(
145+
<ExtensionStatusModal
146+
{...defaultProps}
147+
isOpen={false}
148+
onClose={onCloseMock}
149+
/>
150+
);
151+
152+
expect(screen.getByTestId('extension-form-modal')).toBeInTheDocument();
153+
expect(screen.getByTestId('form-heading')).toHaveTextContent(
154+
'Extension Request Form'
155+
);
156+
expect(screen.getByTestId('reason-input')).toBeInTheDocument();
157+
expect(screen.getByTestId('new-eta-input')).toBeInTheDocument();
158+
expect(screen.getByTestId('title-input')).toBeInTheDocument();
159+
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
160+
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
161+
});
162+
163+
test('should handle initialOldEndsOn prop correctly', () => {
164+
const initialDate = new Date('2024-01-15T10:00:00Z');
165+
const propsWithInitialDate = {
166+
...defaultProps,
167+
initialOldEndsOn: initialDate,
168+
};
169+
170+
render(<ExtensionStatusModal {...propsWithInitialDate} />);
171+
172+
expect(screen.getByTestId('modal-title')).toBeInTheDocument();
173+
});
126174
});

src/app/services/tasksApi.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import task, {
33
TasksResponseType,
44
GetAllTaskParamType,
55
ExtensionRequestsResponse,
6+
ExtensionRequestCreatePayload,
7+
ExtensionRequestCreateResponse,
68
} from '@/interfaces/task.type';
79
import { api } from './api';
810
import { MINE_TASKS_URL, TASKS_URL } from '@/constants/url';
@@ -114,6 +116,22 @@ export const tasksApi = api.injectEndpoints({
114116
}),
115117
providesTags: ['Extension_Requests'],
116118
}),
119+
createExtensionRequest: builder.mutation<
120+
ExtensionRequestCreateResponse,
121+
ExtensionRequestCreatePayload & { dev?: boolean }
122+
>({
123+
query: (payload) => {
124+
const { dev, ...requestPayload } = payload;
125+
126+
return {
127+
url: '/extension-requests',
128+
method: 'POST',
129+
params: { dev: dev ?? true },
130+
body: requestPayload,
131+
};
132+
},
133+
invalidatesTags: ['Extension_Requests'],
134+
}),
117135
}),
118136
overrideExisting: true,
119137
});
@@ -125,4 +143,5 @@ export const {
125143
useUpdateTaskMutation,
126144
useUpdateSelfTaskMutation,
127145
useGetSelfExtensionRequestsQuery,
146+
useCreateExtensionRequestMutation,
128147
} = tasksApi;

0 commit comments

Comments
 (0)