Skip to content

Commit

Permalink
Merge pull request #200 from openedx/ansab/PROD-2478
Browse files Browse the repository at this point in the history
feat: add link program enrollment to v2
  • Loading branch information
ansabgillani authored Dec 23, 2021
2 parents 3d31dd5 + d29b26d commit 1f64e41
Show file tree
Hide file tree
Showing 15 changed files with 727 additions and 7 deletions.
95 changes: 95 additions & 0 deletions src/ProgramEnrollments/LinkProgramEnrollments.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Input, Button } from '@edx/paragon';
import React, { useState, useCallback } from 'react';
import getLinkProgramEnrollmentDetails from './data/api';
import LinkProgramEnrollmentsTable from './LinkProgramEnrollmentsTable';

export default function LinkProgramEnrollments() {
const [programID, setProgramID] = useState(undefined);
const [usernamePairText, setUsernamePairText] = useState(undefined);
const [successMessage, setSuccessMessage] = useState(undefined);
const [errorMessage, setErrorMessage] = useState(undefined);
const [isFetchingData, setIsFetchingData] = useState(false);

const onProgramChange = (e) => {
if (e.currentTarget.value) {
setProgramID(e.currentTarget.value);
} else {
setProgramID(undefined);
}
};

const onUserTextChange = (e) => {
if (e.currentTarget.value) {
setUsernamePairText(e.currentTarget.value);
} else {
setUsernamePairText(undefined);
}
};

const handleSubmit = () => {
setIsFetchingData(true);
getLinkProgramEnrollmentDetails({ programID, usernamePairText }).then((response) => {
setSuccessMessage(response.successes);
setErrorMessage(response.errors);
setIsFetchingData(false);
});
};

const submit = useCallback((event) => {
event.preventDefault();
handleSubmit();
return false;
});

return (
<>
<h3>Link Program Enrollments</h3>
<section className="my-3">
<form>
<div className="my-2">
<label htmlFor="programUUID">Program UUID</label>
<Input
className="mr-1 col-sm-12"
name="programUUID"
type="text"
defaultValue={programID}
onChange={onProgramChange}
/>
</div>
<div className="my-4">
<label
className="d-flex align-items-start"
htmlFor="usernamePairText"
>
List of External key and username pairings (one per line)
</label>
<Input
className="mr-1 col-sm-12"
name="usernamePairText"
type="textarea"
rows="10"
onChange={onUserTextChange}
defaultValue={usernamePairText}
placeholder="external_user_key,lms_username"
/>
</div>
<Button
type="submit"
onClick={submit}
disabled={isFetchingData}
>
Submit
</Button>
</form>
</section>
{((errorMessage && errorMessage.length > 0)
|| (successMessage && successMessage.length > 0)) && (
<LinkProgramEnrollmentsTable
successMessage={successMessage}
errorMessage={errorMessage}
usernamePairText={usernamePairText}
/>
)}
</>
);
}
173 changes: 173 additions & 0 deletions src/ProgramEnrollments/LinkProgramEnrollments.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { mount } from 'enzyme';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { history } from '@edx/frontend-platform';
import { waitForComponentToPaint } from '../setupTest';
import LinkProgramEnrollments from './LinkProgramEnrollments';
import UserMessagesProvider from '../userMessages/UserMessagesProvider';
import {
lpeSuccessResponse,
lpeErrorResponseInvalidUUID,
lpeErrorResponseEmptyValues,
lpeErrorResponseInvalidUsername,
lpeErrorResponseInvalidExternalKey,
lpeErrorResponseAlreadyLinked,
} from './data/test/linkProgramEnrollment';

import * as api from './data/api';

const LinkProgramEnrollmentsWrapper = (props) => (
<MemoryRouter>
<UserMessagesProvider>
<LinkProgramEnrollments {...props} />
</UserMessagesProvider>
</MemoryRouter>
);

describe('Link Program Enrollments', () => {
let wrapper;
let apiMock;
const data = {
programID: '8bee627e-d85e-4a76-be41-d58921da666e',
usernamePairText: 'testuser,verified',
};

beforeEach(() => {
if (apiMock) {
apiMock.mockReset();
}
});

it('default page render', async () => {
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

const programIdInput = wrapper.find("input[name='programUUID']");
const usernamePairInput = wrapper.find("textarea[name='usernamePairText']");
const submitButton = wrapper.find('button.btn-primary');

expect(programIdInput.prop('defaultValue')).toEqual(undefined);
expect(usernamePairInput.prop('defaultValue')).toEqual(undefined);
expect(submitButton.text()).toEqual('Submit');
});

it('valid search value', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeSuccessResponse));
history.push = jest.fn();

wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
});

it('api call made on each click', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementation(() => Promise.resolve(lpeSuccessResponse));
history.push = jest.fn();

wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);

wrapper.find('button.btn-primary').simulate('click');
await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(2);
});

it('empty search value yields error response', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseEmptyValues));
history.replace = jest.fn();
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = '';
wrapper.find('textarea[name="usernamePairText"]').instance().value = '';
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
expect(wrapper.find('.error-message')).toHaveLength(1);
expect(wrapper.find('.success-message')).toHaveLength(0);
});

it('Invalid Program UUID value', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidUUID));
history.replace = jest.fn();
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
expect(wrapper.find('.error-message')).toHaveLength(1);
expect(wrapper.find('.success-message')).toHaveLength(0);
});

it('Invalid Username value', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidUsername));
history.replace = jest.fn();
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
expect(wrapper.find('.error-message')).toHaveLength(1);
expect(wrapper.find('.success-message')).toHaveLength(0);
});

it('Invalid External User Key value', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseInvalidExternalKey));
history.replace = jest.fn();
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
expect(wrapper.find('.error-message')).toHaveLength(1);
expect(wrapper.find('.success-message')).toHaveLength(0);
});

it('Program Already Linked', async () => {
apiMock = jest
.spyOn(api, 'default')
.mockImplementationOnce(() => Promise.resolve(lpeErrorResponseAlreadyLinked));
history.replace = jest.fn();
wrapper = mount(<LinkProgramEnrollmentsWrapper />);

wrapper.find('input[name="programUUID"]').instance().value = data.programID;
wrapper.find('textarea[name="usernamePairText"]').instance().value = data.usernamePairText;
wrapper.find('button.btn-primary').simulate('click');

await waitForComponentToPaint(wrapper);
expect(apiMock).toHaveBeenCalledTimes(1);
expect(wrapper.find('.error-message')).toHaveLength(1);
expect(wrapper.find('.success-message')).toHaveLength(0);
});
});
69 changes: 69 additions & 0 deletions src/ProgramEnrollments/LinkProgramEnrollmentsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import TableV2 from '../components/Table';
import { extractMessageTuple } from '../utils/index';

export default function LinkProgramEnrollmentsTable({
successMessage,
errorMessage,
}) {
return (
<>
{successMessage && successMessage.length > 0 && (
<div className="my-2 success-message">
<h4>Successes</h4>
<TableV2
columns={[
{
Header: 'External User Key',
accessor: 'external_user_key',
},
{
Header: 'LMS Username',
accessor: 'lms_username',
},
{
Header: 'Message',
accessor: 'message',
},
]}
data={successMessage.map((text) => {
const pair = extractMessageTuple(text);
return {
external_user_key: pair[0],
lms_username: pair[1],
message: 'Linkage Successfully Created',
};
})}
styleName="custom-table success-table"
/>
</div>
)}
{errorMessage && errorMessage.length > 0 && (
<div className="my-2 error-message">
<h4>Errors</h4>
<TableV2
columns={[
{
Header: 'Error Messages',
accessor: 'message',
},
]}
data={errorMessage.map((text) => ({ message: text }))}
styleName="custom-table error-table"
/>
</div>
)}
</>
);
}

LinkProgramEnrollmentsTable.propTypes = {
successMessage: PropTypes.arrayOf(PropTypes.string),
errorMessage: PropTypes.arrayOf(PropTypes.string),
};

LinkProgramEnrollmentsTable.defaultProps = {
successMessage: [],
errorMessage: [],
};
Loading

0 comments on commit 1f64e41

Please sign in to comment.