Skip to content

Commit 872fa4c

Browse files
ayesha-warisAyesha Waris
andauthored
test: added test cases to improve test coverage (#1254)
Co-authored-by: Ayesha Waris <[email protected]>
1 parent cdf19f4 commit 872fa4c

File tree

4 files changed

+375
-0
lines changed

4 files changed

+375
-0
lines changed

src/profile/data/sagas.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,67 @@ describe('RootSaga', () => {
163163
expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' })));
164164
expect(gen.next().value).toBeUndefined();
165165
});
166+
167+
it('should reset profile if error has no processedData', () => {
168+
const action = profileActions.saveProfile('formid', 'user1');
169+
const gen = handleSaveProfile(action);
170+
171+
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
172+
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
173+
174+
const err = new Error('oops');
175+
const result = gen.throw(err);
176+
expect(result.value).toEqual(put(profileActions.saveProfileReset()));
177+
});
178+
});
179+
180+
describe('handleSaveProfilePhoto', () => {
181+
it('should save profile photo successfully', () => {
182+
const action = profileActions.saveProfilePhoto('user1', { some: 'formdata' });
183+
const gen = handleSaveProfilePhoto(action);
184+
const fakePhoto = { url: 'photo.jpg' };
185+
186+
expect(gen.next().value).toEqual(put(profileActions.saveProfilePhotoBegin()));
187+
expect(gen.next().value).toEqual(call(ProfileApiService.postProfilePhoto, 'user1', { some: 'formdata' }));
188+
expect(gen.next(fakePhoto).value).toEqual(put(profileActions.saveProfilePhotoSuccess(fakePhoto)));
189+
expect(gen.next().value).toEqual(put(profileActions.saveProfilePhotoReset()));
190+
expect(gen.next().value).toBeUndefined();
191+
});
192+
193+
it('should reset photo state on error', () => {
194+
const action = profileActions.saveProfilePhoto('user1', {});
195+
const gen = handleSaveProfilePhoto(action);
196+
197+
expect(gen.next().value).toEqual(put(profileActions.saveProfilePhotoBegin()));
198+
199+
const err = new Error('fail');
200+
201+
expect(gen.throw(err).value).toEqual(put(profileActions.saveProfilePhotoReset()));
202+
expect(gen.next().done).toBe(true);
203+
});
204+
});
205+
206+
describe('handleDeleteProfilePhoto', () => {
207+
it('should delete profile photo successfully', () => {
208+
const action = profileActions.deleteProfilePhoto('user1');
209+
const gen = handleDeleteProfilePhoto(action);
210+
const fakeResult = { ok: true };
211+
212+
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoBegin()));
213+
expect(gen.next().value).toEqual(call(ProfileApiService.deleteProfilePhoto, 'user1'));
214+
expect(gen.next(fakeResult).value).toEqual(put(profileActions.deleteProfilePhotoSuccess(fakeResult)));
215+
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoReset()));
216+
expect(gen.next().value).toBeUndefined();
217+
});
218+
it('should reset photo state on error', () => {
219+
const action = profileActions.saveProfilePhoto('user1', {});
220+
const gen = handleSaveProfilePhoto(action);
221+
222+
expect(gen.next().value).toEqual(put(profileActions.saveProfilePhotoBegin()));
223+
const err = new Error('fail');
224+
expect(gen.throw(err).value).toEqual(put(profileActions.saveProfilePhotoReset()));
225+
226+
expect(gen.next().done).toBe(true);
227+
});
166228
});
167229
});

src/profile/data/services.test.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
2+
import { logError } from '@edx/frontend-platform/logging';
3+
import {
4+
getAccount,
5+
patchProfile,
6+
postProfilePhoto,
7+
deleteProfilePhoto,
8+
getPreferences,
9+
patchPreferences,
10+
getCourseCertificates,
11+
getCountryList,
12+
} from './services';
13+
14+
import { FIELD_LABELS } from './constants';
15+
16+
import { camelCaseObject, snakeCaseObject, convertKeyNames } from '../utils';
17+
18+
// --- Mocks ---
19+
jest.mock('@edx/frontend-platform', () => ({
20+
ensureConfig: jest.fn(),
21+
getConfig: jest.fn(() => ({ LMS_BASE_URL: 'http://fake-lms' })),
22+
}));
23+
24+
jest.mock('@edx/frontend-platform/auth', () => ({
25+
getAuthenticatedHttpClient: jest.fn(),
26+
}));
27+
28+
jest.mock('@edx/frontend-platform/logging', () => ({
29+
logError: jest.fn(),
30+
}));
31+
32+
jest.mock('../utils', () => ({
33+
camelCaseObject: jest.fn((obj) => obj),
34+
snakeCaseObject: jest.fn((obj) => obj),
35+
convertKeyNames: jest.fn((obj) => obj),
36+
}));
37+
38+
const mockHttpClient = {
39+
get: jest.fn(),
40+
patch: jest.fn(),
41+
post: jest.fn(),
42+
delete: jest.fn(),
43+
};
44+
45+
beforeEach(() => {
46+
jest.clearAllMocks();
47+
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
48+
});
49+
50+
// --- Tests ---
51+
describe('services', () => {
52+
describe('getAccount', () => {
53+
it('should return processed account data', async () => {
54+
const mockData = { name: 'John Doe', socialLinks: [] };
55+
mockHttpClient.get.mockResolvedValue({ data: mockData });
56+
57+
const result = await getAccount('john');
58+
expect(result).toMatchObject(mockData);
59+
expect(mockHttpClient.get).toHaveBeenCalledWith(
60+
'http://fake-lms/api/user/v1/accounts/john',
61+
);
62+
});
63+
});
64+
65+
describe('patchProfile', () => {
66+
it('should patch and return processed data', async () => {
67+
const mockData = { bio: 'New Bio' };
68+
mockHttpClient.patch.mockResolvedValue({ data: mockData });
69+
70+
const result = await patchProfile('john', { bio: 'New Bio' });
71+
expect(result).toMatchObject(mockData);
72+
expect(snakeCaseObject).toHaveBeenCalledWith({ bio: 'New Bio' });
73+
});
74+
75+
it('should throw processed error on failure', async () => {
76+
const error = { response: { data: { some: 'error' } } };
77+
mockHttpClient.patch.mockRejectedValue(error);
78+
79+
await expect(patchProfile('john', {})).rejects.toMatchObject(error);
80+
});
81+
});
82+
83+
describe('postProfilePhoto', () => {
84+
it('should post photo and return updated profile image', async () => {
85+
mockHttpClient.post.mockResolvedValue({});
86+
mockHttpClient.get.mockResolvedValue({
87+
data: { profileImage: { url: 'img.png' } },
88+
});
89+
90+
const result = await postProfilePhoto('john', new FormData());
91+
expect(result).toEqual({ url: 'img.png' });
92+
});
93+
94+
it('should throw error if API fails', async () => {
95+
const error = { response: { data: { error: 'fail' } } };
96+
mockHttpClient.post.mockRejectedValue(error);
97+
await expect(postProfilePhoto('john', new FormData())).rejects.toMatchObject(error);
98+
});
99+
});
100+
101+
describe('deleteProfilePhoto', () => {
102+
it('should delete photo and return updated profile image', async () => {
103+
mockHttpClient.delete.mockResolvedValue({});
104+
mockHttpClient.get.mockResolvedValue({
105+
data: { profileImage: { url: 'deleted.png' } },
106+
});
107+
108+
const result = await deleteProfilePhoto('john');
109+
expect(result).toEqual({ url: 'deleted.png' });
110+
});
111+
});
112+
113+
describe('getPreferences', () => {
114+
it('should return camelCased preferences', async () => {
115+
mockHttpClient.get.mockResolvedValue({ data: { pref: 1 } });
116+
117+
const result = await getPreferences('john');
118+
expect(result).toMatchObject({ pref: 1 });
119+
expect(camelCaseObject).toHaveBeenCalledWith({ pref: 1 });
120+
});
121+
});
122+
123+
describe('patchPreferences', () => {
124+
it('should patch preferences and return params', async () => {
125+
mockHttpClient.patch.mockResolvedValue({});
126+
const params = { visibility_bio: true };
127+
128+
const result = await patchPreferences('john', params);
129+
expect(result).toBe(params);
130+
expect(snakeCaseObject).toHaveBeenCalledWith(params);
131+
expect(convertKeyNames).toHaveBeenCalled();
132+
});
133+
});
134+
135+
describe('getCourseCertificates', () => {
136+
it('should return transformed certificates', async () => {
137+
mockHttpClient.get.mockResolvedValue({
138+
data: [{ download_url: '/path', certificate_type: 'type' }],
139+
});
140+
141+
const result = await getCourseCertificates('john');
142+
expect(result[0]).toHaveProperty('downloadUrl', 'http://fake-lms/path');
143+
});
144+
145+
it('should log error and return empty array on failure', async () => {
146+
mockHttpClient.get.mockRejectedValue(new Error('fail'));
147+
const result = await getCourseCertificates('john');
148+
expect(result).toEqual([]);
149+
expect(logError).toHaveBeenCalled();
150+
});
151+
});
152+
153+
describe('getCountryList', () => {
154+
it('should extract country list', async () => {
155+
mockHttpClient.get.mockResolvedValue({
156+
data: {
157+
fields: [
158+
{ name: FIELD_LABELS.COUNTRY, options: [{ value: 'US' }, { value: 'CA' }] },
159+
],
160+
},
161+
});
162+
163+
const result = await getCountryList();
164+
expect(result).toEqual(['US', 'CA']);
165+
});
166+
167+
it('should log error and return empty array on failure', async () => {
168+
mockHttpClient.get.mockRejectedValue(new Error('fail'));
169+
const result = await getCountryList();
170+
expect(result).toEqual([]);
171+
expect(logError).toHaveBeenCalled();
172+
});
173+
});
174+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import FormControls from './FormControls';
4+
import messages from './FormControls.messages';
5+
6+
const defaultProps = {
7+
cancelHandler: jest.fn(),
8+
changeHandler: jest.fn(),
9+
visibilityId: 'visibility-id',
10+
visibility: 'private',
11+
saveState: null,
12+
};
13+
14+
jest.mock('@edx/frontend-platform/i18n', () => {
15+
const actual = jest.requireActual('@edx/frontend-platform/i18n');
16+
return {
17+
...actual,
18+
injectIntl: (Component) => (props) => (
19+
<Component
20+
{...props}
21+
intl={{
22+
formatMessage: (msg) => msg.id, // returns id so we can assert on it
23+
}}
24+
/>
25+
),
26+
intlShape: {}, // optional, prevents prop-type warnings
27+
};
28+
});
29+
30+
describe('FormControls', () => {
31+
it('renders Save button label when saveState is null', () => {
32+
render(<FormControls {...defaultProps} />);
33+
expect(
34+
screen.getByRole('button', { name: messages['profile.formcontrols.button.save'].id }),
35+
).toBeInTheDocument();
36+
});
37+
38+
it('renders Saved label when saveState is complete', () => {
39+
render(<FormControls {...defaultProps} saveState="complete" />);
40+
expect(
41+
screen.getByRole('button', { name: messages['profile.formcontrols.button.saved'].id }),
42+
).toBeInTheDocument();
43+
});
44+
45+
it('renders Saving label when saveState is pending', () => {
46+
render(<FormControls {...defaultProps} saveState="pending" />);
47+
expect(
48+
screen.getByRole('button', { name: messages['profile.formcontrols.button.saving'].id }),
49+
).toBeInTheDocument();
50+
});
51+
52+
it('calls cancelHandler when Cancel button is clicked', () => {
53+
render(<FormControls {...defaultProps} />);
54+
fireEvent.click(
55+
screen.getByRole('button', { name: messages['profile.formcontrols.button.cancel'].id }),
56+
);
57+
expect(defaultProps.cancelHandler).toHaveBeenCalled();
58+
});
59+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* eslint-disable react/prop-types */
2+
3+
import React from 'react';
4+
import { render, screen } from '@testing-library/react';
5+
import SwitchContent from './SwitchContent';
6+
7+
jest.mock('@openedx/paragon', () => ({
8+
TransitionReplace: ({ children, onChildExit, className }) => (
9+
<div data-testid="transition" data-class={className} data-onchildexit={!!onChildExit}>
10+
{children}
11+
</div>
12+
),
13+
}));
14+
15+
describe('SwitchContent', () => {
16+
const makeElement = (text) => <div>{text}</div>;
17+
18+
it('renders matching case element directly', () => {
19+
render(
20+
<SwitchContent
21+
expression="one"
22+
cases={{ one: makeElement('Case One') }}
23+
/>,
24+
);
25+
expect(screen.getByText('Case One')).toBeInTheDocument();
26+
});
27+
28+
it('renders case via string alias', () => {
29+
render(
30+
<SwitchContent
31+
expression="alias"
32+
cases={{
33+
alias: 'target',
34+
target: makeElement('Target Case'),
35+
}}
36+
/>,
37+
);
38+
expect(screen.getByText('Target Case')).toBeInTheDocument();
39+
});
40+
41+
it('renders default alias when expression not found', () => {
42+
render(
43+
<SwitchContent
44+
expression="missing"
45+
cases={{
46+
default: 'target',
47+
target: makeElement('Target via Default'),
48+
}}
49+
/>,
50+
);
51+
expect(screen.getByText('Target via Default')).toBeInTheDocument();
52+
});
53+
54+
it('renders null when no matching case and no default', () => {
55+
const { container } = render(
56+
<SwitchContent
57+
expression="missing"
58+
cases={{ something: makeElement('Something') }}
59+
/>,
60+
);
61+
expect(container.querySelector('[data-testid="transition"]').textContent).toBe('');
62+
});
63+
64+
it('calls onChildExit when child exits', () => {
65+
const onChildExit = jest.fn();
66+
render(
67+
<SwitchContent
68+
expression="one"
69+
cases={{ one: makeElement('Case One') }}
70+
className="test-class"
71+
/>,
72+
);
73+
const transition = screen.getByTestId('transition');
74+
transition.dataset.onchildexit = onChildExit;
75+
76+
// Simulate child exit
77+
onChildExit(transition);
78+
expect(onChildExit).toHaveBeenCalledWith(transition);
79+
});
80+
});

0 commit comments

Comments
 (0)