Skip to content

Commit

Permalink
feat: Filtering by user email in learner stepper screen (#573)
Browse files Browse the repository at this point in the history
* feat: Filtering by user email in learner stepper screen

ENT-4573

* feat: use find instead of filter

* feat: Tests and constant name change

* feat: Add debounce (test is failing as of now)

* feat: Test fix

* test: Test fix by mocking out debounce (for now)

* test: update test comments

* test: Add test to fix coverage and capture error handling case
  • Loading branch information
binodpant authored Jun 14, 2021
1 parent 17b8c3c commit f5978fc
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 12 deletions.
36 changes: 29 additions & 7 deletions src/components/BulkEnrollmentPage/stepper/AddLearnersStep.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, {
useContext, useState, useMemo, useCallback, useEffect, useRef,
useContext, useState, useMemo, useEffect, useRef,
} from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';

import {
Alert, DataTable, TextFilter,
} from '@edx/paragon';
Expand All @@ -16,7 +18,9 @@ import { BaseSelectWithContext, BaseSelectWithContextHeader } from '../table/Bul
import BaseSelectionStatus from '../table/BaseSelectionStatus';
import { ROUTE_NAMES } from '../../EnterpriseApp/constants';
import LicenseManagerApiService from '../../../data/services/LicenseManagerAPIService';
import { DEBOUNCE_TIME_MILLIS } from '../../../algoliaUtils';

export const ADD_LEARNERS_ERROR_TEXT = 'There was an error retrieving email data. Please try again later.';
export const TABLE_HEADERS = {
email: 'Email',
};
Expand Down Expand Up @@ -50,7 +54,7 @@ const tableColumns = [
];

const INITIAL_PAGE_INDEX = 0;
const PAGE_SIZE = 25;
export const LEARNERS_PAGE_SIZE = 25;

const useIsMounted = () => {
const componentIsMounted = useRef(true);
Expand All @@ -69,11 +73,18 @@ const AddLearnersStep = ({
const { results, count, numPages } = data;
const isMounted = useIsMounted();

const fetchData = useCallback((tableInstance = {}) => {
const fetchData = (tableInstance = {}) => {
const pageIndex = tableInstance.pageIndex || INITIAL_PAGE_INDEX;
let options = { active_only: 1, page_size: LEARNERS_PAGE_SIZE, page: pageIndex + 1 };

const { filters } = tableInstance;
const emailFilter = filters.find(item => item.id === 'userEmail');
if (emailFilter) {
options = { ...options, search: emailFilter.value };
}
LicenseManagerApiService.fetchSubscriptionUsers(
subscriptionUUID,
{ active_only: 1, page_size: PAGE_SIZE, page: pageIndex + 1 },
options,
).then((response) => {
if (isMounted.current) {
setData(camelCaseObject(response.data));
Expand All @@ -89,7 +100,16 @@ const AddLearnersStep = ({
setLoading(false);
}
});
}, [subscriptionUUID, enterpriseSlug]);
};

const debouncedFetchData = useMemo(
() => debounce(fetchData, DEBOUNCE_TIME_MILLIS),
[subscriptionUUID, enterpriseSlug],
);
// Stop the invocation of the debounced function on unmount
useEffect(() => () => {
debouncedFetchData.cancel();
}, []);

const initialTableOptions = useMemo(() => ({
getRowId: (row, relativeIndex, parent) => row?.uuid || (parent ? [parent.id, relativeIndex].join('.') : relativeIndex),
Expand All @@ -103,15 +123,17 @@ const AddLearnersStep = ({
<Link to={`/${enterpriseSlug}/admin/${ROUTE_NAMES.subscriptionManagement}/${subscriptionUUID}`}>{LINK_TEXT}</Link>
</p>
<h2>{ADD_LEARNERS_TITLE}</h2>
{errors && <Alert variant="danger">There was an error retrieving email data. Please try again later.</Alert>}
{errors && <Alert variant="danger">{ADD_LEARNERS_ERROR_TEXT}</Alert>}
<DataTable
isFilterable
manualFilters
columns={tableColumns}
data={results}
itemCount={count}
isPaginated
pageCount={numPages}
manualPagination
fetchData={fetchData}
fetchData={debouncedFetchData}
SelectionStatusComponent={AddLearnersSelectionStatus}
initialTableOptions={initialTableOptions}
selectedFlatRows={selectedEmails}
Expand Down
50 changes: 45 additions & 5 deletions src/components/BulkEnrollmentPage/stepper/AddLearnersStep.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { screen, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { renderWithRouter } from '../../test/testUtils';
import { ROUTE_NAMES } from '../../EnterpriseApp/constants';
import LicenseManagerApiService from '../../../data/services/LicenseManagerAPIService';

import BulkEnrollContextProvider from '../BulkEnrollmentContext';
import { ADD_LEARNERS_TITLE } from './constants';
import AddLearnersStep, { LINK_TEXT, TABLE_HEADERS } from './AddLearnersStep';
import AddLearnersStep, {
ADD_LEARNERS_ERROR_TEXT, LEARNERS_PAGE_SIZE, LINK_TEXT, TABLE_HEADERS,
} from './AddLearnersStep';

jest.mock('../../../data/services/LicenseManagerAPIService', () => ({
__esModule: true,
Expand All @@ -18,12 +19,27 @@ jest.mock('../../../data/services/LicenseManagerAPIService', () => ({
},
}));

const mockResults = [{ uuid: 'foo', userEmail: '[email protected]' }, { uuid: 'bar', userEmail: '[email protected]' }];
jest.mock('lodash.debounce', () => fn => {
// eslint-disable-next-line no-param-reassign
fn.cancel = jest.fn();
return fn;
});

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

const mockResults = [
{ uuid: 'foo', userEmail: '[email protected]' },
{ uuid: 'bar', userEmail: '[email protected]' },
{ uuid: 'afam', userEmail: '[email protected]' },
{ uuid: 'bears', userEmail: '[email protected]' },
];

const mockTableData = Promise.resolve({
data: {
results: mockResults,
count: 2,
count: 4,
numPages: 6,
},
});
Expand Down Expand Up @@ -59,12 +75,36 @@ describe('AddLearnersStep', () => {
expect(await screen.findByText(mockResults[1].userEmail)).toBeInTheDocument();
await act(() => mockTableData);
});
it.skip('allows search by email', async () => {
it('allows search by email', async () => {
act(() => {
renderWithRouter(<StepperWrapper {...defaultProps} />);
});
expect(await screen.findByText('Search Email')).toBeInTheDocument();
await act(() => mockTableData);
userEvent.type(screen.getByPlaceholderText('Search Email'), 'beAR');
expect(await screen.findByText(/Filtered by userEmail/)).toBeInTheDocument();
expect(await screen.findByText('Clear Filters')).toBeInTheDocument();
const { subscriptionUUID } = defaultProps;
// multiple calls will occur to this function, we only test for the last one
// for correctness, and don't test backend filtering part here (tested in backend).
// currently debouncing is mocked out since the use of jest.useFakeTimers() did not work
// due to an issue with lodash.debounce + jest. Perhaps a newer version of jest will do better.
await screen.findByDisplayValue('beAR');
expect(LicenseManagerApiService.fetchSubscriptionUsers).toHaveBeenLastCalledWith(
subscriptionUUID,
{
active_only: 1, page_size: LEARNERS_PAGE_SIZE, page: 1, search: 'beAR',
},
);
});
it('logs Error when response fails', async () => {
LicenseManagerApiService.fetchSubscriptionUsers.mockImplementation(async () => {
throw new Error('Failed response');
});
expect(screen.queryByText(ADD_LEARNERS_ERROR_TEXT)).not.toBeInTheDocument();
act(() => { renderWithRouter(<StepperWrapper {...defaultProps} />); });
await expect(LicenseManagerApiService.fetchSubscriptionUsers).toHaveBeenCalled();
expect(await screen.findByText(ADD_LEARNERS_ERROR_TEXT)).toBeInTheDocument();
});
it('displays a table skeleton when loading', () => {
LicenseManagerApiService.fetchSubscriptionUsers.mockReturnValue(new Promise(() => {}));
Expand Down

0 comments on commit f5978fc

Please sign in to comment.