Skip to content

Commit

Permalink
Merge pull request #233 from edx/mroytman/checklist-loading-state
Browse files Browse the repository at this point in the history
mroytman/checklist loading state
  • Loading branch information
MichaelRoytman authored Jul 24, 2018
2 parents ba83eef + 8079b9a commit 3c64712
Show file tree
Hide file tree
Showing 19 changed files with 765 additions and 114 deletions.
33 changes: 31 additions & 2 deletions src/components/CourseChecklist/CourseChecklist.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,42 @@ describe('CourseChecklist', () => {

const completionCountSection = wrapper.find('.row .col').at(1).find(WrappedMessage);

const completionCount = completionCountSection.dive({ context: { intl } })
.dive({ context: { intl } }).find(FormattedMessage)
const completionCount = completionCountSection
.dive({ context: { intl } })
.dive({ context: { intl } })
.find(FormattedMessage)
.dive({ context: { intl } });

expect(completionCount.prop('id')).toEqual(getCompletionCountID());
});

it('a loading spinner when isLoading prop is true', () => {
wrapper = shallowWithIntl(<CourseChecklist {...defaultProps} />);

wrapper.setProps({
isLoading: true,
});

const heading = wrapper.find('h3').at(0);
expect(heading).toHaveLength(1);

const completionCountSection = wrapper.find('.row .col').at(1).find(WrappedMessage);
expect(completionCountSection).toHaveLength(0);

const loadingIconSection = wrapper.find('.row .col').at(2).find(WrappedMessage);
expect(loadingIconSection).toHaveLength(1);

const loadingIcon = loadingIconSection.dive({ context: { intl } })
.dive({ context: { intl } })
.find(FormattedMessage)
.dive({ context: { intl } })
.find(Icon);

expect(loadingIcon.prop('className')[0]).toEqual(expect.stringContaining('fa-spinner'));
expect(loadingIcon.prop('className')[0]).toEqual(expect.stringContaining('fa-spin'));
expect(loadingIcon.prop('className')[0]).toEqual(expect.stringContaining('fa-5x'));
});

describe('checks with', () => {
it('the correct number of checks', () => {
wrapper = shallowWithIntl(<CourseChecklist {...defaultProps} />);
Expand Down
5 changes: 5 additions & 0 deletions src/components/CourseChecklist/displayMessages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ const messages = defineMessages({
defaultMessage: 'uncompleted',
description: 'Label that describes an uncompleted task',
},
loadingChecklistLabel: {
id: 'loadingChecklistLabel',
defaultMessage: 'Loading',
description: 'Label telling the user that a checklist is loading',
},
});

export default messages;
58 changes: 42 additions & 16 deletions src/components/CourseChecklist/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class CourseChecklist extends React.Component {
values: {},
};

this.spinnerClasses = [FontAwesomeStyles.fa, FontAwesomeStyles['fa-spinner'], FontAwesomeStyles['fa-spin'], FontAwesomeStyles['fa-5x']];

this.onAssignmentHyperlinkClick = this.onAssignmentHyperlinkClick.bind(this);
this.onCheckUpdateHyperlinkClick = this.onCheckUpdateHyperlinkClick.bind(this);
}
Expand Down Expand Up @@ -66,21 +68,23 @@ class CourseChecklist extends React.Component {
getCompletionCount = () => {
const totalCompletedChecks = Object.values(this.state.checks).length;

return (
<WrappedMessage
message={messages.completionCountLabel}
values={{ completed: this.state.totalCompletedChecks, total: totalCompletedChecks }}
>
{displayText =>
(<div
className="font-large"
id={this.getCompletionCountID()}
>
{displayText}
</div>)
}
</WrappedMessage>
);
return this.props.isLoading ?
null :
(
<WrappedMessage
message={messages.completionCountLabel}
values={{ completed: this.state.totalCompletedChecks, total: totalCompletedChecks }}
>
{displayText =>
(<div
className="font-large"
id={this.getCompletionCountID()}
>
{displayText}
</div>)
}
</WrappedMessage>
);
}

getCompletionIcon = (checkID) => {
Expand Down Expand Up @@ -148,6 +152,23 @@ class CourseChecklist extends React.Component {
</div>
);

getLoadingIcon = () => (
<WrappedMessage message={messages.loadingChecklistLabel}>
{displayText =>
(<div className="text-center">
<Icon
className={[classNames(...this.spinnerClasses)]}
screenReaderText={displayText}
/>
</div>)
}
</WrappedMessage>
)

getBody = () => (
this.props.isLoading ? this.getLoadingIcon() : this.getListItems()
)

getListItems = () => (
this.state.checks.map((check) => {
const isCompleted = this.isCheckCompleted(check.id);
Expand Down Expand Up @@ -331,7 +352,7 @@ class CourseChecklist extends React.Component {
</div>
<div className="row no-gutters">
<div className="col">
{this.getListItems()}
{this.getBody()}
</div>
</div>
</div>
Expand Down Expand Up @@ -416,4 +437,9 @@ CourseChecklist.propTypes = {
lang: PropTypes.string,
links: PropTypes.objectOf(PropTypes.string),
}).isRequired,
isLoading: PropTypes.bool,
};

CourseChecklist.defaultProps = {
isLoading: false,
};
152 changes: 151 additions & 1 deletion src/components/CourseChecklistPage/CourseChecklistPage.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';

import { checklistLoading } from '../../data/constants/loadingTypes';
import { courseDetails } from '../../utils/testConstants';
import CourseChecklistPage from '.';
import { launchChecklist, bestPracticesChecklist } from '../../utils/CourseChecklist/courseChecklistData';
Expand Down Expand Up @@ -40,6 +41,64 @@ describe('CourseChecklistPage', () => {
expect(wrapper.find(WrappedCourseChecklist)).toHaveLength(2);
});

describe('an aria-live region with', () => {
it('an aria-live region', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion).toHaveLength(1);
expect(ariaLiveRegion.prop('className')).toEqual(expect.stringContaining('sr-only'));
expect(ariaLiveRegion.prop('role')).toEqual(expect.stringContaining('status'));
});

it('correct content when the launch checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

wrapper.setProps({
loadingChecklists: [
checklistLoading.COURSE_LAUNCH,
],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(2);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistLoadingLabel);
expect(ariaLiveRegion.childAt(1).find(WrappedMessage).prop('message')).toEqual(messages.bestPracticesChecklistDoneLoadingLabel);
});

it('correct content when the best practices checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

wrapper.setProps({
loadingChecklists: [
checklistLoading.COURSE_BEST_PRACTICES,
],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(2);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistDoneLoadingLabel);
expect(ariaLiveRegion.childAt(1).find(WrappedMessage).prop('message')).toEqual(messages.bestPracticesChecklistLoadingLabel);
});

it('correct content when both checklists are done loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

wrapper.setProps({
loadingChecklists: [],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(2);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistDoneLoadingLabel);
expect(ariaLiveRegion.childAt(1).find(WrappedMessage).prop('message')).toEqual(messages.bestPracticesChecklistDoneLoadingLabel);
});
});

describe('a WrappedCourseChecklist component', () => {
describe('for the launch checklist with', () => {
it('correct props', () => {
Expand All @@ -51,6 +110,18 @@ describe('CourseChecklistPage', () => {
expect(checklist.prop('dataList')).toEqual(launchChecklist.data);
expect(checklist.prop('data')).toEqual(testCourseLaunchData);
expect(checklist.prop('idPrefix')).toEqual('launchChecklist');
expect(checklist.prop('isLoading')).toEqual(false);
});

it('isLoading prop set to true if launch checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

wrapper.setProps({
loadingChecklists: [checklistLoading.COURSE_LAUNCH],
});

const checklist = wrapper.find(WrappedCourseChecklist).at(0);
expect(checklist.prop('isLoading')).toEqual(true);
});
});

Expand All @@ -64,6 +135,18 @@ describe('CourseChecklistPage', () => {
expect(checklist.prop('dataList')).toEqual(bestPracticesChecklist.data);
expect(checklist.prop('data')).toEqual(testCourseBestPracticesData);
expect(checklist.prop('idPrefix')).toEqual('bestPracticesChecklist');
expect(checklist.prop('isLoading')).toEqual(false);
});

it('isLoading prop set to true if best practices checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);

wrapper.setProps({
loadingChecklists: [checklistLoading.COURSE_BEST_PRACTICES],
});

const checklist = wrapper.find(WrappedCourseChecklist).at(1);
expect(checklist.prop('isLoading')).toEqual(true);
});
});
});
Expand All @@ -86,17 +169,84 @@ describe('CourseChecklistPage', () => {
expect(wrapper.find(WrappedCourseChecklist)).toHaveLength(1);
});

describe('an aria-live region with', () => {
it('an aria-live region', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion).toHaveLength(1);
expect(ariaLiveRegion.prop('className')).toEqual(expect.stringContaining('sr-only'));
expect(ariaLiveRegion.prop('role')).toEqual(expect.stringContaining('status'));
});

it('correct content when the launch checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

wrapper.setProps({
loadingChecklists: [
checklistLoading.COURSE_LAUNCH,
],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(1);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistLoadingLabel);
});

it('correct content when the best practices checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

wrapper.setProps({
loadingChecklists: [
checklistLoading.COURSE_BEST_PRACTICES,
],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(1);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistDoneLoadingLabel);
});

it('correct content when both checklists are done loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

wrapper.setProps({
loadingChecklists: [],
});

const ariaLiveRegion = wrapper.find({ 'aria-live': 'polite' });

expect(ariaLiveRegion.children()).toHaveLength(1);
expect(ariaLiveRegion.childAt(0).find(WrappedMessage).prop('message')).toEqual(messages.launchChecklistDoneLoadingLabel);
});
});

describe('a WrappedCourseChecklist component', () => {
describe('for the launch checklist with', () => {
it('correct props', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...defaultProps} />);
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

const checklist = wrapper.find(WrappedCourseChecklist).at(0);

expect(checklist.prop('dataHeading')).toEqual(<WrappedMessage message={messages.launchChecklistLabel} />);
expect(checklist.prop('dataList')).toEqual(launchChecklist.data);
expect(checklist.prop('data')).toEqual(testCourseLaunchData);
expect(checklist.prop('idPrefix')).toEqual('launchChecklist');
expect(checklist.prop('isLoading')).toEqual(false);
});

it('isLoading prop set to true if launch checklist is loading', () => {
wrapper = shallowWithIntl(<CourseChecklistPage {...newProps} />);

wrapper.setProps({
loadingChecklists: [checklistLoading.COURSE_LAUNCH],
});

const checklist = wrapper.find(WrappedCourseChecklist).at(0);
expect(checklist.prop('isLoading')).toEqual(true);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions src/components/CourseChecklistPage/container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mapStateToProps = state => ({
studioDetails: state.studioDetails,
courseBestPracticesData: state.courseChecklistData.courseBestPractices,
courseLaunchData: state.courseChecklistData.courseLaunch,
loadingChecklists: state.courseChecklistData.loadingChecklists,
});

const mapDispatchToProps = dispatch => ({
Expand Down
20 changes: 20 additions & 0 deletions src/components/CourseChecklistPage/displayMessages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ const messages = defineMessages({
defaultMessage: 'Best Practices Checklist',
description: 'Header text for a checklist that describes best practices for a course',
},
launchChecklistLoadingLabel: {
id: 'doneLoadingChecklistStatusLabel',
defaultMessage: 'Launch Checklist data is loading',
description: 'Label telling the user that the Launch Checklist is loading',
},
launchChecklistDoneLoadingLabel: {
id: 'launchChecklistDoneLoadingLabel',
defaultMessage: 'Launch Checklist data is done loading',
description: 'Label telling the user that the Launch Checklist is done loading',
},
bestPracticesChecklistLoadingLabel: {
id: 'bestPracticesChecklistLoadingLabel',
defaultMessage: 'Best Practices Checklist data is loading',
description: 'Label telling the user that the Best Practices Checklist is loading',
},
bestPracticesChecklistDoneLoadingLabel: {
id: 'bestPracticesChecklistDoneLoadingLabel',
defaultMessage: 'Best Practices Checklist data is done loading',
description: 'Label telling the user that the Best Practices Checklist is done loading',
},
});

export default messages;
Loading

0 comments on commit 3c64712

Please sign in to comment.