Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=''
APP_ID='learning'
BASE_URL=''
COMPLETION_PERCENTAGE_PRECISION=''
CONTACT_URL=''
CREDENTIALS_BASE_URL=''
CREDIT_HELP_LINK_URL=''
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000'
COMPLETION_PERCENTAGE_PRECISION=''
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ NODE_ENV='test'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000'
COMPLETION_PERCENTAGE_PRECISION=''
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
Expand Down
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ as documented in the Open edX Developer Guide under

The learning micro-frontend also supports the following additional variables:

COMPLETION_PERCENTAGE_PRECISION
The number of decimal places used when rounding the percentages shown in the
completion donut chart on the progress tab. Defaults to ``0``.

CREDIT_HELP_LINK_URL
A link to resources to help explain what course credit is and how to earn it.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
import { useProgressData } from '../hooks';

Expand All @@ -17,10 +18,14 @@ const CompletionDonutChart = () => {
},
} = useProgressData();

const precision = Number(getConfig().COMPLETION_PERCENTAGE_PRECISION) || 0;
const toPercentage = (count, total) => Number(((count / total) * 100).toFixed(precision));
const formatPercentage = (percentage) => intl.formatNumber(percentage, { maximumFractionDigits: precision });

const numTotalUnits = completeCount + incompleteCount + lockedCount;
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
const incompletePercentage = 100 - completePercentage - lockedPercentage;
const completePercentage = completeCount ? toPercentage(completeCount, numTotalUnits) : 0;
const lockedPercentage = lockedCount ? toPercentage(lockedCount, numTotalUnits) : 0;
const incompletePercentage = Number((100 - completePercentage - lockedPercentage).toFixed(precision));

const isLocaleRtl = isRtl(getLocale());

Expand All @@ -33,7 +38,7 @@ const CompletionDonutChart = () => {
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
<g className="donut-chart-text">
<text x="50%" y="50%" className="donut-chart-number">
{completePercentage}{isLocaleRtl && '\u200f'}%
{formatPercentage(completePercentage)}{isLocaleRtl && '\u200f'}%
</text>
<text x="50%" y="50%" className="donut-chart-label">
{intl.formatMessage(messages.donutLabel)}
Expand All @@ -44,11 +49,11 @@ const CompletionDonutChart = () => {
<CompleteDonutSegment completePercentage={completePercentage} lockedPercentage={lockedPercentage} />
</svg>
<div className="sr-only">
{intl.formatMessage(messages.percentComplete, { percent: completePercentage })}
{intl.formatMessage(messages.percentIncomplete, { percent: incompletePercentage })}
{intl.formatMessage(messages.percentComplete, { percent: formatPercentage(completePercentage) })}
{intl.formatMessage(messages.percentIncomplete, { percent: formatPercentage(incompletePercentage) })}
{lockedPercentage > 0 && (
<>
{intl.formatMessage(messages.percentLocked, { percent: lockedPercentage })}
{intl.formatMessage(messages.percentLocked, { percent: formatPercentage(lockedPercentage) })}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { mergeConfig } from '@edx/frontend-platform';

import { initializeMockApp, render } from '../../../setupTest';
import { useProgressData } from '../hooks';
import CompletionDonutChart from './CompletionDonutChart';

jest.mock('../hooks', () => ({
useProgressData: jest.fn(),
}));

describe('CompletionDonutChart', () => {
const setPrecision = (COMPLETION_PERCENTAGE_PRECISION) => {
mergeConfig({ COMPLETION_PERCENTAGE_PRECISION });
};

beforeAll(async () => {
initializeMockApp();
});

beforeEach(() => {
// 4 complete, 7 incomplete and 1 locked unit out of 12, so that none of the
// percentages can be represented exactly.
useProgressData.mockReturnValue({
completionSummary: {
completeCount: 4,
incompleteCount: 7,
lockedCount: 1,
},
});
});

afterEach(() => {
setPrecision(0);
});

// The length of a donut segment is the first value of its `stroke-dasharray`.
const getSegmentLength = (container, segmentClass) => (
container.querySelector(`.${segmentClass}`)?.getAttribute('stroke-dasharray').split(' ')[0]
);

const renderChart = () => {
const { container } = render(<CompletionDonutChart />);
return {
donutPercentage: container.querySelector('.donut-chart-number').textContent,
screenReaderText: container.querySelector('.sr-only').textContent,
completeSegment: getSegmentLength(container, 'complete-stroke'),
incompleteSegment: getSegmentLength(container, 'incomplete-stroke'),
lockedSegment: getSegmentLength(container, 'locked-stroke'),
};
};

it('rounds the percentages to whole numbers by default', () => {
const { donutPercentage, screenReaderText } = renderChart();

expect(donutPercentage).toEqual('33%');
expect(screenReaderText).toContain('You have completed 33% of content in this course.');
expect(screenReaderText).toContain('You have not completed 59% of content in this course');
expect(screenReaderText).toContain('8% of content in this course is locked');
});

it('rounds the percentages to the configured precision', () => {
setPrecision(2);
const { donutPercentage, screenReaderText } = renderChart();

expect(donutPercentage).toEqual('33.33%');
expect(screenReaderText).toContain('You have completed 33.33% of content in this course.');
expect(screenReaderText).toContain('You have not completed 58.34% of content in this course');
expect(screenReaderText).toContain('8.33% of content in this course is locked');
});

it('draws segments that add up to 100% without floating point residue', () => {
setPrecision(2);
const { completeSegment, incompleteSegment, lockedSegment } = renderChart();

// 100 - 33.33 - 8.33 evaluates to 58.339999999999996 before rounding.
expect([completeSegment, incompleteSegment, lockedSegment]).toEqual(['33.33', '58.34', '8.33']);
});

it('falls back to whole numbers for a non-numeric precision', () => {
setPrecision('two');
const { donutPercentage } = renderChart();

expect(donutPercentage).toEqual('33%');
});

it('omits the segments for units that are not present', () => {
useProgressData.mockReturnValue({
completionSummary: {
completeCount: 0,
incompleteCount: 3,
lockedCount: 0,
},
});
setPrecision(2);
const {
donutPercentage, screenReaderText, completeSegment, incompleteSegment, lockedSegment,
} = renderChart();

expect(donutPercentage).toEqual('0%');
expect(screenReaderText).toContain('You have not completed 100% of content in this course');
expect(screenReaderText).not.toContain('is locked');
expect(completeSegment).toBeUndefined();
expect(incompleteSegment).toEqual('100');
expect(lockedSegment).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ initialize({
config: () => {
/* istanbul ignore next */
mergeConfig({
COMPLETION_PERCENTAGE_PRECISION: process.env.COMPLETION_PERCENTAGE_PRECISION || 0,
CONTACT_URL: process.env.CONTACT_URL || null,
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
Expand Down