Skip to content

Commit 065aaa6

Browse files
feat: replaced TableContainer with DataTable in enrolledLearnersTable (#1518)
1 parent bcc60f5 commit 065aaa6

File tree

9 files changed

+328
-99
lines changed

9 files changed

+328
-99
lines changed

src/components/Admin/Admin.test.jsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,6 @@ describe('<Admin />', () => {
480480
csvFetchMethod: 'fetchCourseEnrollments',
481481
csvFetchParams: [enterpriseId, {}, { csv: true }],
482482
},
483-
'enrolled-learners': {
484-
csvFetchMethod: 'fetchEnrolledLearners',
485-
csvFetchParams: [enterpriseId, {}, { csv: true }],
486-
},
487483
};
488484

489485
afterEach(() => {

src/components/Admin/DownloadButtonWrapper.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const DownloadButtonWrapper = ({
1717
'learners-inactive-month',
1818
'registered-unenrolled-learners',
1919
'enrolled-learners-inactive-courses',
20+
'enrolled-learners',
2021
'completed-learners',
2122
'completed-learners-week',
2223
].includes(actionSlug);

src/components/Admin/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class Admin extends React.Component {
125125
defaultMessage: 'Number of Courses Enrolled by Learners',
126126
description: 'Report title for number of courses enrolled by learners',
127127
}),
128-
component: <EnrolledLearnersTable />,
128+
component: <EnrolledLearnersTable id="enrolled-learners" />,
129129
csvFetchMethod: () => (
130130
EnterpriseDataApiService.fetchEnrolledLearners(enterpriseId, {}, { csv: true })
131131
),
Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,69 @@
11
import React from 'react';
22
import { MemoryRouter } from 'react-router-dom';
3-
import renderer from 'react-test-renderer';
43
import { IntlProvider } from '@edx/frontend-platform/i18n';
54
import configureMockStore from 'redux-mock-store';
65
import thunk from 'redux-thunk';
76
import { Provider } from 'react-redux';
7+
import { mount } from 'enzyme';
88

9-
import EnrolledLearnersTable from '.';
9+
import EnrolledLearnersTable from './index';
10+
import useCourseUsers from './data/hooks/useCourseUsers';
11+
import { mockEnrolledLearnersData, mockEmptyEnrolledLearnersData } from './data/tests/constants';
1012

11-
const mockStore = configureMockStore([thunk]);
1213
const enterpriseId = 'test-enterprise';
14+
const mockStore = configureMockStore([thunk]);
15+
16+
jest.mock('./data/hooks/useCourseUsers', () => jest.fn());
17+
1318
const store = mockStore({
1419
portalConfiguration: {
1520
enterpriseId,
1621
},
17-
table: {
18-
'enrolled-learners': {
19-
data: {
20-
results: [],
21-
current_page: 1,
22-
num_pages: 1,
23-
},
24-
ordering: null,
25-
loading: false,
26-
error: null,
27-
},
28-
},
2922
});
3023

3124
const EnrolledLearnersWrapper = props => (
3225
<MemoryRouter>
3326
<IntlProvider locale="en">
3427
<Provider store={store}>
35-
<EnrolledLearnersTable
36-
{...props}
37-
/>
28+
<EnrolledLearnersTable {...props} />
3829
</Provider>
3930
</IntlProvider>
4031
</MemoryRouter>
4132
);
4233

4334
describe('EnrolledLearnersTable', () => {
44-
it('renders empty state correctly', () => {
45-
const tree = renderer
46-
.create((
47-
<EnrolledLearnersWrapper />
48-
))
49-
.toJSON();
50-
expect(tree).toMatchSnapshot();
35+
beforeEach(() => {
36+
useCourseUsers.mockReturnValue(mockEnrolledLearnersData);
37+
});
38+
39+
afterEach(() => {
40+
jest.clearAllMocks();
41+
});
42+
43+
it('renders table with correct columns', () => {
44+
const wrapper = mount(<EnrolledLearnersWrapper id="enrolled-learners" />);
45+
const table = wrapper.find('[role="table"]');
46+
47+
expect(table.exists()).toBe(true);
48+
49+
const columnHeaders = table.find('thead th').map(th => th.text());
50+
expect(columnHeaders).toEqual([
51+
'Email',
52+
'Account Created',
53+
'Total Course Enrollment Count',
54+
]);
55+
});
56+
57+
it('renders correct number of rows with data', () => {
58+
const wrapper = mount(<EnrolledLearnersWrapper id="enrolled-learners" />);
59+
const rows = wrapper.find('tbody tr');
60+
expect(rows.length).toBe(mockEnrolledLearnersData.data.results.length);
61+
});
62+
63+
it('renders empty table correctly', () => {
64+
useCourseUsers.mockReturnValue(mockEmptyEnrolledLearnersData);
65+
const wrapper = mount(<EnrolledLearnersWrapper id="enrolled-learners" />);
66+
expect(wrapper.find('[role="table"]').exists()).toBe(true);
67+
expect(wrapper.find('tbody tr').length).toBe(0);
5168
});
5269
});

src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import usePaginatedTableData from '../../../../hooks/usePaginatedTableData';
2+
import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService';
3+
4+
const useCourseUsers = (enterpriseId, tableId, apiFieldsForColumnAccessor) => usePaginatedTableData({
5+
enterpriseId,
6+
tableId,
7+
apiFieldsForColumnAccessor,
8+
fetchFunction: EnterpriseDataApiService.fetchEnrolledLearners,
9+
});
10+
11+
export default useCourseUsers;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import useCourseUsers from './useCourseUsers';
3+
import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService';
4+
5+
jest.mock('../../../../data/services/EnterpriseDataApiService');
6+
7+
const mockApiFields = {
8+
userEmail: { key: 'user_email' },
9+
courseTitle: { key: 'course_title' },
10+
};
11+
12+
const enterpriseId = 'enterprise-123';
13+
const tableId = 'test-table';
14+
15+
describe('useCourseUsers', () => {
16+
const mockResponse = {
17+
data: {
18+
count: 2,
19+
results: [
20+
{
21+
user_email: '[email protected]',
22+
enrollment_count: 23,
23+
},
24+
{
25+
user_email: '[email protected]',
26+
enrollment_count: 15,
27+
},
28+
],
29+
},
30+
};
31+
32+
const emptyResponse = {
33+
data: {
34+
count: 0,
35+
results: [],
36+
},
37+
};
38+
39+
beforeEach(() => {
40+
jest.clearAllMocks();
41+
});
42+
43+
it('fetches and returns user course data successfully', async () => {
44+
EnterpriseDataApiService.fetchEnrolledLearners.mockResolvedValueOnce(mockResponse);
45+
46+
const { result } = renderHook(() => useCourseUsers(enterpriseId, tableId, mockApiFields));
47+
48+
await act(async () => {
49+
await result.current.fetchDataImmediate({
50+
pageIndex: 0,
51+
pageSize: 10,
52+
sortBy: [],
53+
});
54+
});
55+
56+
expect(EnterpriseDataApiService.fetchEnrolledLearners).toHaveBeenCalledWith(enterpriseId, {
57+
page: 1,
58+
pageSize: 10,
59+
});
60+
61+
expect(result.current.data.results).toHaveLength(2);
62+
expect(result.current.data.itemCount).toBe(2);
63+
expect(result.current.hasData).toBe(true);
64+
expect(result.current.isLoading).toBe(false);
65+
});
66+
67+
it('handles empty data response', async () => {
68+
EnterpriseDataApiService.fetchEnrolledLearners.mockResolvedValueOnce(emptyResponse);
69+
70+
const { result } = renderHook(() => useCourseUsers(enterpriseId, tableId, mockApiFields));
71+
72+
await act(async () => {
73+
await result.current.fetchDataImmediate({
74+
pageIndex: 0,
75+
pageSize: 10,
76+
sortBy: [],
77+
});
78+
});
79+
80+
expect(result.current.data.results).toHaveLength(0);
81+
expect(result.current.hasData).toBe(false);
82+
});
83+
84+
it('sets loading state correctly during fetch', async () => {
85+
let resolvePromise;
86+
EnterpriseDataApiService.fetchEnrolledLearners.mockReturnValueOnce(
87+
new Promise((resolve) => {
88+
resolvePromise = resolve;
89+
}),
90+
);
91+
92+
const { result } = renderHook(() => useCourseUsers(enterpriseId, tableId, mockApiFields));
93+
94+
act(() => {
95+
result.current.fetchDataImmediate({
96+
pageIndex: 0,
97+
pageSize: 5,
98+
sortBy: [],
99+
});
100+
});
101+
102+
expect(result.current.isLoading).toBe(true);
103+
104+
await act(async () => {
105+
resolvePromise(mockResponse);
106+
});
107+
108+
expect(result.current.isLoading).toBe(false);
109+
});
110+
111+
it('logs error when fetch fails', async () => {
112+
const error = new Error('API failure');
113+
EnterpriseDataApiService.fetchEnrolledLearners.mockRejectedValueOnce(error);
114+
115+
const { result } = renderHook(() => useCourseUsers(enterpriseId, tableId, mockApiFields));
116+
117+
await act(async () => {
118+
await result.current.fetchDataImmediate({
119+
pageIndex: 1,
120+
pageSize: 5,
121+
sortBy: [],
122+
filters: {},
123+
});
124+
});
125+
126+
expect(result.current.data.results).toHaveLength(0);
127+
expect(result.current.isLoading).toBe(false);
128+
expect(result.current.hasData).toBe(false);
129+
});
130+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const mockEnrolledLearnersData = {
2+
isLoading: false,
3+
hasData: true,
4+
fetchData: jest.fn(),
5+
fetchDataImmediate: jest.fn(),
6+
data: {
7+
results: [
8+
{
9+
userEmail: '[email protected]',
10+
lmsUserCreated: '2024-01-01T00:00:00Z',
11+
enrollmentCount: 3,
12+
},
13+
{
14+
userEmail: '[email protected]',
15+
lmsUserCreated: '2024-01-02T00:00:00Z',
16+
enrollmentCount: 5,
17+
},
18+
],
19+
itemCount: 2,
20+
pageCount: 1,
21+
},
22+
};
23+
24+
export const mockEmptyEnrolledLearnersData = {
25+
isLoading: false,
26+
hasData: false,
27+
fetchData: jest.fn(),
28+
fetchDataImmediate: jest.fn(),
29+
data: {
30+
results: [],
31+
itemCount: 0,
32+
pageCount: 0,
33+
},
34+
};

0 commit comments

Comments
 (0)