|
1 | | -import React from 'react'; |
2 | | -import { MemoryRouter } from 'react-router-dom'; |
3 | | -import renderer from 'react-test-renderer'; |
| 1 | +import { IntlProvider } from '@edx/frontend-platform/i18n'; |
4 | 2 | import configureMockStore from 'redux-mock-store'; |
5 | 3 | import thunk from 'redux-thunk'; |
6 | 4 | import { Provider } from 'react-redux'; |
7 | | -import { IntlProvider } from '@edx/frontend-platform/i18n'; |
| 5 | +import { createMemoryHistory } from 'history'; |
| 6 | +import { Router } from 'react-router'; |
| 7 | +import { act } from '@testing-library/react'; |
8 | 8 | import { mount } from 'enzyme'; |
9 | | - |
10 | 9 | import PastWeekPassedLearnersTable from '.'; |
| 10 | +import usePastWeekPassedLearners from './data/hooks/usePastWeekPassedLearners'; |
| 11 | +import { PAGE_SIZE } from '../../data/constants/table'; |
| 12 | + |
| 13 | +// Mock the hooks |
| 14 | +jest.mock('./data/hooks/usePastWeekPassedLearners', () => jest.fn()); |
| 15 | +jest.mock('../Admin/TableDataContext', () => ({ |
| 16 | + useTableData: () => ({ |
| 17 | + setTableHasData: jest.fn(), |
| 18 | + }), |
| 19 | +})); |
11 | 20 |
|
12 | | -const enterpriseId = 'test-enterprise'; |
13 | 21 | const mockStore = configureMockStore([thunk]); |
14 | | -const store = mockStore({ |
15 | | - portalConfiguration: { |
16 | | - enterpriseId, |
17 | | - }, |
18 | | - table: { |
19 | | - 'completed-learners-week': { |
| 22 | + |
| 23 | +// Mock implementations |
| 24 | +const mockFetchData = jest.fn().mockResolvedValue({}); |
| 25 | +const mockFetchDataImmediate = jest.fn(); |
| 26 | + |
| 27 | +describe('PastWeekPassedLearnersTable', () => { |
| 28 | + const enterpriseId = 'test-enterprise-id'; |
| 29 | + const tableId = 'completed-learners-week'; |
| 30 | + |
| 31 | + const store = mockStore({ |
| 32 | + portalConfiguration: { |
| 33 | + enterpriseId, |
| 34 | + }, |
| 35 | + }); |
| 36 | + |
| 37 | + const defaultProps = { |
| 38 | + id: tableId, |
| 39 | + }; |
| 40 | + |
| 41 | + beforeEach(() => { |
| 42 | + // Setup default mock implementation |
| 43 | + usePastWeekPassedLearners.mockReturnValue({ |
| 44 | + isLoading: false, |
20 | 45 | data: { |
21 | | - count: 2, |
22 | | - num_pages: 1, |
23 | | - current_page: 1, |
24 | 46 | results: [ |
25 | 47 | { |
26 | | - id: 1, |
27 | | - passed_date: '2018-09-23T16:27:34.690065Z', |
28 | | - course_title: 'Dive into ReactJS', |
29 | | - course_key: 'edX/ReactJS', |
30 | | - |
| 48 | + |
| 49 | + courseTitle: 'React Basics', |
| 50 | + passedDate: '2025-04-20T12:00:00Z', |
31 | 51 | }, |
32 | 52 | { |
33 | | - id: 5, |
34 | | - passed_date: '2018-09-22T16:27:34.690065Z', |
35 | | - course_title: 'Redux with ReactJS', |
36 | | - course_key: 'edX/Redux_ReactJS', |
37 | | - |
38 | | - |
| 53 | + |
| 54 | + courseTitle: 'Advanced React', |
| 55 | + passedDate: '2025-04-19T12:00:00Z', |
39 | 56 | }, |
40 | 57 | ], |
41 | | - next: null, |
42 | | - start: 0, |
43 | | - previous: null, |
| 58 | + itemCount: 2, |
| 59 | + pageCount: 1, |
44 | 60 | }, |
45 | | - ordering: null, |
46 | | - loading: false, |
47 | | - error: null, |
48 | | - }, |
49 | | - }, |
50 | | -}); |
| 61 | + fetchData: mockFetchData, |
| 62 | + fetchDataImmediate: mockFetchDataImmediate, |
| 63 | + hasData: true, |
| 64 | + }); |
| 65 | + }); |
51 | 66 |
|
52 | | -const PastWeekPassedLearnersWrapper = props => ( |
53 | | - <MemoryRouter> |
54 | | - <IntlProvider locale="en"> |
55 | | - <Provider store={store}> |
56 | | - <PastWeekPassedLearnersTable |
57 | | - {...props} |
58 | | - /> |
59 | | - </Provider> |
60 | | - </IntlProvider> |
61 | | - </MemoryRouter> |
62 | | -); |
| 67 | + afterEach(() => { |
| 68 | + jest.clearAllMocks(); |
| 69 | + }); |
63 | 70 |
|
64 | | -describe('PastWeekPassedLearnersTable', () => { |
65 | | - let wrapper; |
66 | | - |
67 | | - it('renders table correctly', () => { |
68 | | - const tree = renderer |
69 | | - .create(( |
70 | | - <PastWeekPassedLearnersWrapper /> |
71 | | - )) |
72 | | - .toJSON(); |
73 | | - expect(tree).toMatchSnapshot(); |
| 71 | + const PastWeekPassedLearnersTableWrapper = (props = {}) => { |
| 72 | + const history = createMemoryHistory(); |
| 73 | + return ( |
| 74 | + <Router location={history.location} navigator={history}> |
| 75 | + <IntlProvider locale="en"> |
| 76 | + <Provider store={store}> |
| 77 | + <PastWeekPassedLearnersTable {...defaultProps} {...props} /> |
| 78 | + </Provider> |
| 79 | + </IntlProvider> |
| 80 | + </Router> |
| 81 | + ); |
| 82 | + }; |
| 83 | + |
| 84 | + it('renders the table with learner data', () => { |
| 85 | + const wrapper = mount(<PastWeekPassedLearnersTableWrapper />); |
| 86 | + |
| 87 | + // Check if table exists |
| 88 | + const table = wrapper.find('DataTable'); |
| 89 | + expect(table.exists()).toBe(true); |
| 90 | + |
| 91 | + // Verify DataTable props |
| 92 | + expect(table.prop('id')).toBe(tableId); |
| 93 | + expect(table.prop('data')).toEqual([ |
| 94 | + { |
| 95 | + |
| 96 | + courseTitle: 'React Basics', |
| 97 | + passedDate: '2025-04-20T12:00:00Z', |
| 98 | + }, |
| 99 | + { |
| 100 | + |
| 101 | + courseTitle: 'Advanced React', |
| 102 | + passedDate: '2025-04-19T12:00:00Z', |
| 103 | + }, |
| 104 | + ]); |
| 105 | + expect(table.prop('itemCount')).toBe(2); |
| 106 | + expect(table.prop('pageCount')).toBe(1); |
| 107 | + expect(table.prop('isLoading')).toBe(false); |
| 108 | + |
| 109 | + // Verify columns are correctly configured |
| 110 | + expect(table.prop('columns').length).toBe(3); |
| 111 | + expect(table.prop('columns')[0].accessor).toBe('userEmail'); |
| 112 | + expect(table.prop('columns')[1].accessor).toBe('courseTitle'); |
| 113 | + expect(table.prop('columns')[2].accessor).toBe('passedDate'); |
| 114 | + }); |
| 115 | + |
| 116 | + it('renders empty table when no data is available', () => { |
| 117 | + usePastWeekPassedLearners.mockReturnValue({ |
| 118 | + isLoading: false, |
| 119 | + data: { |
| 120 | + results: [], |
| 121 | + itemCount: 0, |
| 122 | + pageCount: 0, |
| 123 | + }, |
| 124 | + fetchData: mockFetchData, |
| 125 | + fetchDataImmediate: mockFetchDataImmediate, |
| 126 | + hasData: false, |
| 127 | + }); |
| 128 | + |
| 129 | + const wrapper = mount(<PastWeekPassedLearnersTableWrapper />); |
| 130 | + |
| 131 | + const table = wrapper.find('DataTable'); |
| 132 | + expect(table.exists()).toBe(true); |
| 133 | + expect(table.prop('data')).toEqual([]); |
| 134 | + expect(table.prop('itemCount')).toBe(0); |
| 135 | + expect(table.prop('pageCount')).toBe(0); |
74 | 136 | }); |
75 | 137 |
|
76 | | - it('renders table with correct data', () => { |
77 | | - const tableId = 'completed-learners-week'; |
78 | | - const columnTitles = ['Email', 'Course Title', 'Passed Date']; |
79 | | - const rowsData = [ |
80 | | - [ |
81 | | - |
82 | | - 'Dive into ReactJS', |
83 | | - 'September 23, 2018', |
84 | | - ], |
85 | | - [ |
86 | | - |
87 | | - 'Redux with ReactJS', |
88 | | - 'September 22, 2018', |
89 | | - ], |
90 | | - ]; |
91 | | - |
92 | | - wrapper = mount(( |
93 | | - <PastWeekPassedLearnersWrapper /> |
94 | | - )); |
95 | | - |
96 | | - // Verify that table has correct number of columns |
97 | | - expect(wrapper.find(`.${tableId} thead th`).length).toEqual(3); |
98 | | - |
99 | | - // Verify only expected columns are shown |
100 | | - wrapper.find(`.${tableId} thead th`).forEach((column, index) => { |
101 | | - expect(column.text()).toContain(columnTitles[index]); |
| 138 | + it('shows loading state when data is being fetched', () => { |
| 139 | + usePastWeekPassedLearners.mockReturnValue({ |
| 140 | + isLoading: true, |
| 141 | + data: { |
| 142 | + results: [], |
| 143 | + itemCount: 0, |
| 144 | + pageCount: 0, |
| 145 | + }, |
| 146 | + fetchData: mockFetchData, |
| 147 | + fetchDataImmediate: mockFetchDataImmediate, |
| 148 | + hasData: false, |
102 | 149 | }); |
103 | 150 |
|
104 | | - // Verify that table has correct number of rows |
105 | | - expect(wrapper.find(`.${tableId} tbody tr`).length).toEqual(2); |
| 151 | + const wrapper = mount(<PastWeekPassedLearnersTableWrapper />); |
| 152 | + |
| 153 | + const table = wrapper.find('DataTable'); |
| 154 | + expect(table.prop('isLoading')).toBe(true); |
| 155 | + }); |
| 156 | + |
| 157 | + it('fetches data immediately on mount', () => { |
| 158 | + mount(<PastWeekPassedLearnersTableWrapper />); |
| 159 | + |
| 160 | + expect(mockFetchDataImmediate).toHaveBeenCalledTimes(1); |
| 161 | + expect(mockFetchDataImmediate).toHaveBeenCalledWith( |
| 162 | + { |
| 163 | + pageIndex: 0, |
| 164 | + pageSize: PAGE_SIZE, // PAGE_SIZE constant value |
| 165 | + sortBy: [ |
| 166 | + { id: 'passedDate', desc: true }, |
| 167 | + ], |
| 168 | + }, |
| 169 | + true, |
| 170 | + ); |
| 171 | + }); |
| 172 | + |
| 173 | + it('uses URL query parameters for initial page', () => { |
| 174 | + const history = createMemoryHistory(); |
| 175 | + history.push(`?${tableId}-page=2`); // Set page 2 in URL |
| 176 | + |
| 177 | + const wrapper = mount( |
| 178 | + <Router location={history.location} navigator={history}> |
| 179 | + <IntlProvider locale="en"> |
| 180 | + <Provider store={store}> |
| 181 | + <PastWeekPassedLearnersTable {...defaultProps} /> |
| 182 | + </Provider> |
| 183 | + </IntlProvider> |
| 184 | + </Router>, |
| 185 | + ); |
| 186 | + |
| 187 | + const table = wrapper.find('DataTable'); |
| 188 | + // Check that initialState has pageIndex set to 1 (0-based index for page 2) |
| 189 | + expect(table.prop('initialState').pageIndex).toBe(1); |
| 190 | + |
| 191 | + // Check that fetchDataImmediate was called with pageIndex 1 |
| 192 | + expect(mockFetchDataImmediate).toHaveBeenCalledWith( |
| 193 | + expect.objectContaining({ |
| 194 | + pageIndex: 1, |
| 195 | + }), |
| 196 | + true, |
| 197 | + ); |
| 198 | + }); |
| 199 | + |
| 200 | + it('updates URL when page changes', async () => { |
| 201 | + const history = createMemoryHistory(); |
| 202 | + jest.spyOn(history, 'push'); |
106 | 203 |
|
107 | | - // Verify each row in table has correct data |
108 | | - wrapper.find(`.${tableId} tbody tr`).forEach((row, rowIndex) => { |
109 | | - row.find('td').forEach((cell, colIndex) => { |
110 | | - expect(cell.text()).toEqual(rowsData[rowIndex][colIndex]); |
| 204 | + const wrapper = mount( |
| 205 | + <Router location={history.location} navigator={history}> |
| 206 | + <IntlProvider locale="en"> |
| 207 | + <Provider store={store}> |
| 208 | + <PastWeekPassedLearnersTable {...defaultProps} /> |
| 209 | + </Provider> |
| 210 | + </IntlProvider> |
| 211 | + </Router>, |
| 212 | + ); |
| 213 | + |
| 214 | + // Simulate page change by calling fetchData prop with new table state |
| 215 | + const table = wrapper.find('DataTable'); |
| 216 | + await act(async () => { |
| 217 | + await table.prop('fetchData')({ |
| 218 | + pageIndex: 1, // Navigate to page 2 (0-indexed) |
| 219 | + pageSize: 50, |
| 220 | + sortBy: [], |
111 | 221 | }); |
112 | 222 | }); |
| 223 | + |
| 224 | + // Check that fetchData was called |
| 225 | + expect(mockFetchData).toHaveBeenCalledWith({ |
| 226 | + pageIndex: 1, |
| 227 | + pageSize: 50, |
| 228 | + sortBy: [], |
| 229 | + }); |
| 230 | + }); |
| 231 | + |
| 232 | + it('renders UserEmail component correctly', () => { |
| 233 | + const wrapper = mount(<PastWeekPassedLearnersTableWrapper />); |
| 234 | + |
| 235 | + // Find columns in the DataTable props |
| 236 | + const columns = wrapper.find('DataTable').prop('columns'); |
| 237 | + const emailColumn = columns.find(col => col.accessor === 'userEmail'); |
| 238 | + |
| 239 | + // Test the Cell renderer with a sample row |
| 240 | + const testRow = { |
| 241 | + original: { |
| 242 | + |
| 243 | + }, |
| 244 | + }; |
| 245 | + |
| 246 | + const emailCell = mount( |
| 247 | + <IntlProvider locale="en"> |
| 248 | + {emailColumn.Cell({ row: testRow })} |
| 249 | + </IntlProvider>, |
| 250 | + ); |
| 251 | + |
| 252 | + expect(emailCell.find('[data-hj-suppress]').text()).toBe('[email protected]'); |
| 253 | + }); |
| 254 | + |
| 255 | + it('formats timestamps correctly in the passedDate column', () => { |
| 256 | + const wrapper = mount(<PastWeekPassedLearnersTableWrapper />); |
| 257 | + |
| 258 | + // Since the actual formatting depends on a utility function i18nFormatTimestamp, |
| 259 | + // we can verify the Cell prop is properly configured |
| 260 | + const columns = wrapper.find('DataTable').prop('columns'); |
| 261 | + const dateColumn = columns.find(col => col.accessor === 'passedDate'); |
| 262 | + expect(dateColumn).toBeDefined(); |
| 263 | + expect(typeof dateColumn.Cell).toBe('function'); |
113 | 264 | }); |
114 | 265 | }); |
0 commit comments