Skip to content

Commit efb6399

Browse files
authored
Merge pull request #1486 from openedx/eahmadjaved/ENT-10168
chore: Migrate deprecated Table to DataTable for CompletedLearnersTable
2 parents 50ca6ba + 23cbd1c commit efb6399

File tree

9 files changed

+522
-103
lines changed

9 files changed

+522
-103
lines changed

src/components/Admin/Admin.test.jsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,10 +484,6 @@ describe('<Admin />', () => {
484484
csvFetchMethod: 'fetchEnrolledLearners',
485485
csvFetchParams: [enterpriseId, {}, { csv: true }],
486486
},
487-
'completed-learners': {
488-
csvFetchMethod: 'fetchCompletedLearners',
489-
csvFetchParams: [enterpriseId, {}, { csv: true }],
490-
},
491487
'completed-learners-week': {
492488
csvFetchMethod: 'fetchCourseEnrollments',
493489
csvFetchParams: [enterpriseId, { passedDate: 'last_week' }, { csv: true }],

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+
'completed-learners',
2021
].includes(actionSlug);
2122

2223
return (

src/components/Admin/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ class Admin extends React.Component {
221221
defaultMessage: 'Number of Courses Completed by Learner',
222222
description: 'Report title for number of courses completed by learners',
223223
}),
224-
component: <CompletedLearnersTable />,
224+
component: <CompletedLearnersTable id="completed-learners" />,
225225
csvFetchMethod: () => (
226226
EnterpriseDataApiService.fetchCompletedLearners(enterpriseId, {}, { csv: true })
227227
),
Lines changed: 218 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,232 @@
11
import React from 'react';
2-
import { MemoryRouter } from 'react-router-dom';
3-
import renderer from 'react-test-renderer';
2+
import { createMemoryHistory } from 'history';
3+
import { Router } from 'react-router-dom';
44
import { IntlProvider } from '@edx/frontend-platform/i18n';
55
import configureMockStore from 'redux-mock-store';
66
import thunk from 'redux-thunk';
77
import { Provider } from 'react-redux';
8+
import { act } from '@testing-library/react';
9+
import { mount } from 'enzyme';
810

911
import CompletedLearnersTable from '.';
12+
import useCompletedLearners from './data/hooks/useCompletedLearners';
13+
import { PAGE_SIZE } from '../../data/constants/table';
14+
import { mockCompletedLearners, mockEmptyLearners } from './data/tests/constants';
15+
16+
// Mock the hooks
17+
jest.mock('./data/hooks/useCompletedLearners', () => jest.fn());
18+
jest.mock('../Admin/TableDataContext', () => ({
19+
useTableData: () => ({
20+
setTableHasData: jest.fn(),
21+
}),
22+
}));
1023

1124
const mockStore = configureMockStore([thunk]);
12-
const enterpriseId = 'test-enterprise';
13-
const store = mockStore({
14-
portalConfiguration: {
15-
enterpriseId,
16-
},
17-
table: {
18-
'completed-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-
},
29-
});
3025

31-
const CompletedLearnersWrapper = props => (
32-
<MemoryRouter>
33-
<IntlProvider locale="en">
34-
<Provider store={store}>
35-
<CompletedLearnersTable
36-
{...props}
37-
/>
38-
</Provider>
39-
</IntlProvider>
40-
</MemoryRouter>
41-
);
26+
// Mock implementations
27+
const mockFetchData = jest.fn().mockResolvedValue({});
28+
const mockFetchDataImmediate = jest.fn();
4229

4330
describe('CompletedLearnersTable', () => {
44-
it('renders empty state correctly', () => {
45-
const tree = renderer
46-
.create((
47-
<CompletedLearnersWrapper />
48-
))
49-
.toJSON();
50-
expect(tree).toMatchSnapshot();
31+
const enterpriseId = 'test-enterprise-id';
32+
const tableId = 'completed-learners';
33+
34+
const store = mockStore({
35+
portalConfiguration: {
36+
enterpriseId,
37+
},
38+
});
39+
40+
const defaultProps = {
41+
id: tableId,
42+
};
43+
44+
beforeEach(() => {
45+
// Setup default mock implementation
46+
useCompletedLearners.mockReturnValue({
47+
isLoading: false,
48+
data: mockCompletedLearners,
49+
fetchData: mockFetchData,
50+
fetchDataImmediate: mockFetchDataImmediate,
51+
hasData: true,
52+
});
53+
});
54+
55+
afterEach(() => {
56+
jest.clearAllMocks();
57+
});
58+
59+
const CompletedLearnersTableWrapper = (props = {}) => {
60+
const history = createMemoryHistory();
61+
return (
62+
<Router location={history.location} navigator={history}>
63+
<IntlProvider locale="en">
64+
<Provider store={store}>
65+
<CompletedLearnersTable {...defaultProps} {...props} />
66+
</Provider>
67+
</IntlProvider>
68+
</Router>
69+
);
70+
};
71+
72+
it('renders the table with learner data', () => {
73+
const wrapper = mount(<CompletedLearnersTableWrapper />);
74+
75+
// Check if table exists
76+
const table = wrapper.find('DataTable');
77+
expect(table.exists()).toBe(true);
78+
79+
// Verify DataTable props
80+
expect(table.prop('id')).toBe(tableId);
81+
expect(table.prop('data')).toEqual(mockCompletedLearners.results);
82+
expect(table.prop('itemCount')).toBe(mockCompletedLearners.itemCount);
83+
expect(table.prop('pageCount')).toBe(mockCompletedLearners.pageCount);
84+
expect(table.prop('isLoading')).toBe(false);
85+
86+
// Verify columns are correctly configured
87+
expect(table.prop('columns').length).toBe(2);
88+
expect(table.prop('columns')[0].accessor).toBe('userEmail');
89+
expect(table.prop('columns')[1].accessor).toBe('completedCourses');
90+
});
91+
92+
it('renders empty table when no data is available', () => {
93+
useCompletedLearners.mockReturnValue({
94+
isLoading: false,
95+
data: mockEmptyLearners,
96+
fetchData: mockFetchData,
97+
fetchDataImmediate: mockFetchDataImmediate,
98+
hasData: false,
99+
});
100+
101+
const wrapper = mount(<CompletedLearnersTableWrapper />);
102+
103+
const table = wrapper.find('DataTable');
104+
expect(table.exists()).toBe(true);
105+
expect(table.prop('data')).toEqual([]);
106+
expect(table.prop('itemCount')).toBe(0);
107+
expect(table.prop('pageCount')).toBe(0);
108+
});
109+
110+
it('shows loading state when data is being fetched', () => {
111+
useCompletedLearners.mockReturnValue({
112+
isLoading: true,
113+
data: mockEmptyLearners,
114+
fetchData: mockFetchData,
115+
fetchDataImmediate: mockFetchDataImmediate,
116+
hasData: false,
117+
});
118+
119+
const wrapper = mount(<CompletedLearnersTableWrapper />);
120+
121+
const table = wrapper.find('DataTable');
122+
expect(table.prop('isLoading')).toBe(true);
123+
});
124+
125+
it('fetches data immediately on mount', () => {
126+
mount(<CompletedLearnersTableWrapper />);
127+
128+
expect(mockFetchDataImmediate).toHaveBeenCalledTimes(1);
129+
expect(mockFetchDataImmediate).toHaveBeenCalledWith(
130+
{
131+
pageIndex: 0,
132+
pageSize: PAGE_SIZE,
133+
sortBy: [],
134+
},
135+
true,
136+
);
137+
});
138+
139+
it('uses URL query parameters for initial page', () => {
140+
const history = createMemoryHistory();
141+
history.push(`?${tableId}-page=3`); // Set page 3 in URL
142+
143+
const wrapper = mount(
144+
<Router location={history.location} navigator={history}>
145+
<IntlProvider locale="en">
146+
<Provider store={store}>
147+
<CompletedLearnersTable {...defaultProps} />
148+
</Provider>
149+
</IntlProvider>
150+
</Router>,
151+
);
152+
153+
const table = wrapper.find('DataTable');
154+
// Check that initialState has pageIndex set to 2 (0-based index for page 3)
155+
expect(table.prop('initialState').pageIndex).toBe(2);
156+
157+
// Check that fetchDataImmediate was called with pageIndex 2
158+
expect(mockFetchDataImmediate).toHaveBeenCalledWith(
159+
expect.objectContaining({
160+
pageIndex: 2,
161+
}),
162+
true,
163+
);
164+
});
165+
166+
it('updates URL when page changes', async () => {
167+
const history = createMemoryHistory();
168+
jest.spyOn(history, 'push');
169+
170+
const wrapper = mount(
171+
<Router location={history.location} navigator={history}>
172+
<IntlProvider locale="en">
173+
<Provider store={store}>
174+
<CompletedLearnersTable {...defaultProps} />
175+
</Provider>
176+
</IntlProvider>
177+
</Router>,
178+
);
179+
180+
// Simulate page change by calling fetchData prop with new table state
181+
const table = wrapper.find('DataTable');
182+
await act(async () => {
183+
await table.prop('fetchData')({
184+
pageIndex: 2, // Navigate to page 3 (0-indexed)
185+
pageSize: PAGE_SIZE,
186+
sortBy: [],
187+
});
188+
});
189+
190+
// Check that fetchData was called
191+
expect(mockFetchData).toHaveBeenCalledWith({
192+
pageIndex: 2,
193+
pageSize: PAGE_SIZE,
194+
sortBy: [],
195+
});
196+
});
197+
198+
it('renders UserEmail component correctly', () => {
199+
const wrapper = mount(<CompletedLearnersTableWrapper />);
200+
201+
// Find columns in the DataTable props
202+
const columns = wrapper.find('DataTable').prop('columns');
203+
const emailColumn = columns.find(col => col.accessor === 'userEmail');
204+
205+
// Test the Cell renderer with a sample row
206+
const testRow = {
207+
original: {
208+
userEmail: '[email protected]',
209+
},
210+
};
211+
212+
const emailCell = mount(
213+
<IntlProvider locale="en">
214+
{emailColumn.Cell({ row: testRow })}
215+
</IntlProvider>,
216+
);
217+
218+
expect(emailCell.find('[data-hj-suppress]').text()).toBe('[email protected]');
219+
});
220+
221+
it('renders completedCourses column correctly', () => {
222+
const wrapper = mount(<CompletedLearnersTableWrapper />);
223+
224+
// Find columns in the DataTable props
225+
const columns = wrapper.find('DataTable').prop('columns');
226+
const completedCoursesColumn = columns.find(col => col.accessor === 'completedCourses');
227+
expect(completedCoursesColumn).toBeDefined();
228+
229+
// Verify column header text
230+
expect(completedCoursesColumn.Header).toBe('Total Course Completed Count');
51231
});
52232
});

src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.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 useCompletedLearners = (enterpriseId, tableId, apiFieldsForColumnAccessor) => usePaginatedTableData({
5+
enterpriseId,
6+
tableId,
7+
apiFieldsForColumnAccessor,
8+
fetchFunction: EnterpriseDataApiService.fetchCompletedLearners,
9+
});
10+
11+
export default useCompletedLearners;

0 commit comments

Comments
 (0)