diff --git a/src/courseInfo/CourseInfoPage.tsx b/src/courseInfo/CourseInfoPage.tsx index 1213dd7c..b06c0cf3 100644 --- a/src/courseInfo/CourseInfoPage.tsx +++ b/src/courseInfo/CourseInfoPage.tsx @@ -1,14 +1,13 @@ -import { Container } from '@openedx/paragon'; import { PendingTasks } from '@src/components/PendingTasks'; import { GeneralCourseInfo } from '@src/courseInfo/components/generalCourseInfo'; +import { EnrollmentSummary } from './components/EnrollmentSummary'; -const CourseInfoPage = () => { - return ( - - - - - ); -}; +const CourseInfoPage = () => ( + <> + + + + +); export default CourseInfoPage; diff --git a/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.test.tsx b/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.test.tsx new file mode 100644 index 00000000..2dddafed --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.test.tsx @@ -0,0 +1,295 @@ +import { render, screen } from '@testing-library/react'; +import { Verified, Person, Group } from '@openedx/paragon/icons'; +import { EnrollmentCounter } from './'; + +describe('EnrollmentCounter', () => { + it('displays the enrollment label and count', () => { + render( + + ); + + expect(screen.getByText('Total Students')).toBeInTheDocument(); + expect(screen.getByText('1,500')).toBeInTheDocument(); + }); + + it('formats numbers with thousands separators', () => { + render( + + ); + + expect(screen.getByText('All Enrollments')).toBeInTheDocument(); + expect(screen.getByText('3,640')).toBeInTheDocument(); + }); + + it('displays and checks an SVG icon when provided', () => { + render( + } + /> + ); + + expect(screen.getByText('Verified Students')).toBeInTheDocument(); + expect(screen.getByText('410')).toBeInTheDocument(); + + const svgElement = document.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + }); + + it('renders without an icon when not provided', () => { + render( + + ); + + expect(screen.getByText('Audit Students')).toBeInTheDocument(); + expect(screen.getByText('3,230')).toBeInTheDocument(); + + // Should not have any icon elements + const svgElement = document.querySelector('svg'); + expect(svgElement).not.toBeInTheDocument(); + }); + + it('displays different types of enrollment data correctly', () => { + const { rerender } = render( + + ); + + expect(screen.getByText('Staff and Admins')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + + rerender( + } + /> + ); + + expect(screen.getByText('Learners')).toBeInTheDocument(); + expect(screen.getByText('3,630')).toBeInTheDocument(); + }); + + it('handles large numbers correctly with comma formatting', () => { + render( + + ); + + expect(screen.getByText('Total Enrollments')).toBeInTheDocument(); + expect(screen.getByText('1,234,567')).toBeInTheDocument(); + }); + + it('handles small numbers without unnecessary formatting', () => { + render( + + ); + + expect(screen.getByText('Admin Users')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + }); + + it('displays numbers in the hundreds correctly', () => { + render( + + ); + + expect(screen.getByText('Premium Users')).toBeInTheDocument(); + expect(screen.getByText('150')).toBeInTheDocument(); + }); + + it('maintains proper visual hierarchy with label above count', () => { + render( + + ); + + const label = screen.getByText('Total Active'); + const count = screen.getByText('999'); + + // Label should appear before count in the DOM + expect(label.compareDocumentPosition(count) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + it('handles zero count correctly', () => { + render( + + ); + + expect(screen.getByText('Pending Approvals')).toBeInTheDocument(); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('works with different paragon icon types', () => { + const { rerender } = render( + } + /> + ); + + expect(screen.getByText('Verified Users')).toBeInTheDocument(); + let svgElement = document.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + + rerender( + } + /> + ); + + expect(screen.getByText('Regular Users')).toBeInTheDocument(); + svgElement = document.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + }); + + it('is accessible to screen readers', () => { + render( + } + /> + ); + + // Text should be accessible and visible + expect(screen.getByText('Verified Certificates')).toBeVisible(); + expect(screen.getByText('125')).toBeVisible(); + + // Content should be readable by screen readers + const labelElement = screen.getByText('Verified Certificates'); + const countElement = screen.getByText('125'); + + expect(labelElement).not.toHaveAttribute('aria-hidden'); + expect(countElement).not.toHaveAttribute('aria-hidden'); + }); + + it('displays complete enrollment information as a cohesive unit', () => { + render( + } + /> + ); + + // All elements should be present and visible together + expect(screen.getByText('Premium Members')).toBeVisible(); + expect(screen.getByText('1,250')).toBeVisible(); + + // The container should hold all related information + const container = screen.getByText('Premium Members').closest('div'); + expect(container).toContainElement(screen.getByText('1,250')); + }); + + it('handles various enrollment scenarios users might see', () => { + const scenarios = [ + { label: 'All Enrollments', count: '3640' }, + { label: 'Staff and Admins', count: '10' }, + { label: 'Learners', count: '3630' }, + { label: 'Verified', count: '410' }, + { label: 'Audit', count: '3230' }, + ]; + + scenarios.forEach(({ label, count }) => { + const { unmount } = render( + : undefined} + /> + ); + + expect(screen.getByText(label)).toBeInTheDocument(); + expect(screen.getByText(count === '3640' ? '3,640' + : count === '3630' ? '3,630' + : count === '3230' ? '3,230' : count)).toBeInTheDocument(); + + if (label === 'Verified') { + const svgElement = document.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + } + + unmount(); + }); + }); + + it('provides clear visual distinction between label and count', () => { + render( + + ); + + const label = screen.getByText('Course Participants'); + const count = screen.getByText('2,500'); + + // Both should be visible but with different styling + expect(label).toBeVisible(); + expect(count).toBeVisible(); + + // They should be in separate elements + expect(label.tagName).toBe('P'); + expect(count.tagName).toBe('P'); + }); + + it('handles edge case of very large numbers', () => { + render( + + ); + + expect(screen.getByText('Global Users')).toBeInTheDocument(); + expect(screen.getByText('10,000,000')).toBeInTheDocument(); + }); + + it('displays icon and text content together when both are provided', () => { + render( + } + /> + ); + + const container = screen.getByText('Certified Learners').closest('div'); + + // Should contain both text elements and icon + expect(container).toContainElement(screen.getByText('Certified Learners')); + expect(container).toContainElement(screen.getByText('850')); + + const svgElement = document.querySelector('svg'); + expect(svgElement).toBeInTheDocument(); + }); +}); diff --git a/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.tsx b/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.tsx new file mode 100644 index 00000000..747c8c1d --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/EnrollmentCounter.tsx @@ -0,0 +1,32 @@ +import React, { FC } from 'react'; +import { formatNumberWithCommas } from './utils'; + +interface EnrollmentCounterProps { + label: string, + count: string, + icon?: React.ReactNode, +} + +const EnrollmentCounter: FC = (props) => { + const renderCounter = () => { + return (

{formatNumberWithCommas(props.count)}

); + }; + + const renderCounterWithIcon = () => { + return ( +
+ {props.icon} + {renderCounter()} +
+ ); + }; + + return ( +
+

{props.label}

+ { props.icon ? renderCounterWithIcon() : renderCounter() } +
+ ); +}; + +export { EnrollmentCounter }; diff --git a/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.test.tsx b/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.test.tsx new file mode 100644 index 00000000..9cdd3fc2 --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.test.tsx @@ -0,0 +1,328 @@ +import { screen } from '@testing-library/react'; +import { EnrollmentSummary } from './EnrollmentSummary'; +import { renderWithIntl } from '../../../testUtils'; +import { useCourseInfo } from '@src/data/apiHook'; +import messages from './messages'; + +jest.mock('react-router-dom', () => ({ + useParams: () => ({ + courseId: 'course-v1:edX+DemoX+Demo_Course', + }), +})); + +jest.mock('@src/data/apiHook', () => ({ + useCourseInfo: jest.fn(), +})); + +const mockCounter = { + enrollmentCounts: { + total: 5000, + verified: 3500, + audit: 1500, + }, + staffCount: 25, + learnerCount: 4975, +}; + +describe('EnrollmentSummary', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays the enrollment summary title', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + expect(screen.getByRole('heading', { name: /course enrollment/i })).toBeInTheDocument(); + }); + + it('displays total enrollment count with proper formatting', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + screen.debug(); + + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.total.toLocaleString())).toBeInTheDocument(); + }); + + it('displays Staff / Admin count when provided', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + expect(screen.getByText(messages.staffAndAdminsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.staffCount.toLocaleString())).toBeInTheDocument(); + }); + + it('displays learners count when provided', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + expect(screen.getByText(messages.learnersLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.learnerCount.toLocaleString())).toBeInTheDocument(); + }); + + it('displays verified count with svg icon when provided', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + expect(screen.getByText(messages.verified.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.verified.toLocaleString())).toBeInTheDocument(); + + const svgElements = document.querySelectorAll('svg'); + expect(svgElements.length).toBeGreaterThan(0); + }); + + it('displays audit count when provided', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + expect(screen.getByText(messages.audit.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.audit.toLocaleString())).toBeInTheDocument(); + }); + + it('does not display verified section when not provided', () => { + const countsWithoutVerified = { + total: 2000, + audit: 2000, + }; + + (useCourseInfo as jest.Mock).mockReturnValue({ + data: { enrollmentCounts: countsWithoutVerified, staffCount: 20, learnerCount: 1980 }, + isLoading: false, + }); + + renderWithIntl(); + + expect(screen.queryByText(messages.verified.defaultMessage)).not.toBeInTheDocument(); + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.learnersLabel.defaultMessage)).toBeInTheDocument(); + }); + + it('does not display audit section when not provided', () => { + const countsWithoutAudit = { + total: 1500, + verified: 1500, + }; + + (useCourseInfo as jest.Mock).mockReturnValue({ + data: { enrollmentCounts: countsWithoutAudit, staffCount: 15, learnerCount: 1485 }, + isLoading: false, + }); + + renderWithIntl(); + + expect(screen.queryByText(messages.audit.defaultMessage)).not.toBeInTheDocument(); + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.verified.defaultMessage)).toBeInTheDocument(); + }); + + it('displays only total when other counts are not provided', () => { + const minimalCounts = { total: 100 }; + (useCourseInfo as jest.Mock).mockReturnValue({ + data: { enrollmentCounts: minimalCounts, staffCount: 0, learnerCount: 0 }, + isLoading: false, + }); + + renderWithIntl(); + + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(minimalCounts.total.toLocaleString())).toBeInTheDocument(); + expect(screen.queryByText(messages.verified.defaultMessage)).not.toBeInTheDocument(); + expect(screen.queryByText(messages.audit.defaultMessage)).not.toBeInTheDocument(); + }); + + it('handles zero values correctly', () => { + const countsWithZeros = { + total: 0, + verified: 0, + audit: 0, + }; + + (useCourseInfo as jest.Mock).mockReturnValue({ + data: { enrollmentCounts: countsWithZeros, staffCount: 0, learnerCount: 0 }, + isLoading: false, + }); + + renderWithIntl(); + + // Should still display sections with zero values when provided + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getAllByText('0')).toHaveLength(5); + }); + + it('displays enrollment data in proper visual order', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + + renderWithIntl(); + + const allEnrollments = screen.getByText(messages.allEnrollmentsLabel.defaultMessage); + const staffAndAdmins = screen.getByText(messages.staffAndAdminsLabel.defaultMessage); + const learners = screen.getByText(messages.learnersLabel.defaultMessage); + const verified = screen.getByText(messages.verified.defaultMessage); + const audit = screen.getByText(messages.audit.defaultMessage); + + // Check DOM order + expect(allEnrollments.compareDocumentPosition(staffAndAdmins) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(staffAndAdmins.compareDocumentPosition(learners) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(learners.compareDocumentPosition(verified) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(verified.compareDocumentPosition(audit) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + it('displays large numbers with proper comma formatting', () => { + const largeCounts = { + enrollmentCounts: { + total: 1234567, + verified: 987654, + audit: 245913, + }, + staffCount: 1234, + learnerCount: 1233333, + }; + (useCourseInfo as jest.Mock).mockReturnValue({ + data: largeCounts, + isLoading: false, + }); + + renderWithIntl(); + + expect(screen.getByText(largeCounts.enrollmentCounts.total.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(largeCounts.enrollmentCounts.verified.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(largeCounts.enrollmentCounts.audit.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(largeCounts.staffCount.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(largeCounts.learnerCount.toLocaleString())).toBeInTheDocument(); + }); + + it('is accessible to screen readers', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + + renderWithIntl(); + + // Main heading should be accessible + const heading = screen.getByRole('heading', { name: /course enrollment/i }); + expect(heading).toBeInTheDocument(); + expect(heading).toBeVisible(); + + // All enrollment information should be visible and accessible + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeVisible(); + expect(screen.getByText(messages.staffAndAdminsLabel.defaultMessage)).toBeVisible(); + expect(screen.getByText(messages.learnersLabel.defaultMessage)).toBeVisible(); + expect(screen.getByText(messages.verified.defaultMessage)).toBeVisible(); + expect(screen.getByText(messages.audit.defaultMessage)).toBeVisible(); + + // All counts should be visible + expect(screen.getByText(mockCounter.enrollmentCounts.total.toLocaleString())).toBeVisible(); + expect(screen.getByText(mockCounter.enrollmentCounts.verified.toLocaleString())).toBeVisible(); + expect(screen.getByText(mockCounter.enrollmentCounts.audit.toLocaleString())).toBeVisible(); + expect(screen.getByText(mockCounter.staffCount.toLocaleString())).toBeVisible(); + expect(screen.getByText(mockCounter.learnerCount.toLocaleString())).toBeVisible(); + }); + + it('maintains proper heading hierarchy', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + const heading = screen.getByRole('heading', { level: 3 }); + expect(heading).toHaveTextContent('Course Enrollment'); + }); + + it('displays enrollment counters in horizontal layout', () => { + const { container } = renderWithIntl(); + + // Check for horizontal stack layout + const stackElement = container.querySelector('.d-flex'); + expect(stackElement).toBeInTheDocument(); + }); + + it('shows verified enrollment with distinctive icon presentation', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + // Verified section should have both text and icon + expect(screen.getByText(messages.verified.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.verified.toLocaleString())).toBeInTheDocument(); + + // Should have an SVG icon for verified + const svgElements = document.querySelectorAll('svg'); + expect(svgElements.length).toBeGreaterThan(0); + }); + + it('handles edge case with only verified enrollments', () => { + const verifiedOnlyCounts = { + enrollmentCounts: { + total: 500, + verified: 500, + }, + staffCount: 0, + learnerCount: 0, + }; + + (useCourseInfo as jest.Mock).mockReturnValue({ + data: verifiedOnlyCounts, + isLoading: false, + }); + + renderWithIntl(); + + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.verified.defaultMessage)).toBeInTheDocument(); + expect(screen.getAllByText(verifiedOnlyCounts.enrollmentCounts.verified.toLocaleString())).toHaveLength(2); + + // Should not show other sections + expect(screen.queryByText(messages.audit.defaultMessage)).not.toBeInTheDocument(); + }); + + it('provides complete enrollment overview for course administrators', () => { + (useCourseInfo as jest.Mock).mockReturnValue({ + data: mockCounter, + isLoading: false, + }); + renderWithIntl(); + + // Should show comprehensive enrollment data + expect(screen.getByText(messages.enrollmentSummaryTitle.defaultMessage)).toBeInTheDocument(); + + // All major enrollment categories should be visible + expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.staffAndAdminsLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.learnersLabel.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.verified.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.audit.defaultMessage)).toBeInTheDocument(); + + // All counts should be properly formatted and visible + expect(screen.getByText(mockCounter.enrollmentCounts.total.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(mockCounter.staffCount.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(mockCounter.learnerCount.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.verified.toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(mockCounter.enrollmentCounts.audit.toLocaleString())).toBeInTheDocument(); + }); +}); diff --git a/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.tsx b/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.tsx new file mode 100644 index 00000000..150891eb --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/EnrollmentSummary.tsx @@ -0,0 +1,66 @@ +import { useIntl } from '@openedx/frontend-base'; +import messages from './messages'; +import { Col, Row, Skeleton, Stack } from '@openedx/paragon'; +import { Verified } from '@openedx/paragon/icons'; +import { EnrollmentCounter } from './'; +import { useParams } from 'react-router-dom'; +import { useCourseInfo } from '@src/data/apiHook'; + +const EnrollmentSummary = () => { + const intl = useIntl(); + const { courseId = '' } = useParams(); + const { data: courseInfo, isLoading } = useCourseInfo(courseId); + const { enrollmentCounts, staffCount = 0, learnerCount = 0 } = courseInfo ?? {}; + + return ( + <> + + +

{intl.formatMessage(messages.enrollmentSummaryTitle)}

+ +
+ { + isLoading ? ( + <> + + + + ) : ( + + +
+ + +
+ { + Object.entries(enrollmentCounts).map(([type, count]) => { + if (type === 'total' || count === undefined) { + return null; + } + return ( + : undefined} + /> + ); + }) + } + + ) + } + + ); +}; + +export { EnrollmentSummary }; diff --git a/src/courseInfo/components/EnrollmentSummary/index.ts b/src/courseInfo/components/EnrollmentSummary/index.ts new file mode 100644 index 00000000..f56ed0f1 --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/index.ts @@ -0,0 +1,2 @@ +export { EnrollmentSummary } from './EnrollmentSummary'; +export { EnrollmentCounter } from './EnrollmentCounter'; diff --git a/src/courseInfo/components/EnrollmentSummary/messages.ts b/src/courseInfo/components/EnrollmentSummary/messages.ts new file mode 100644 index 00000000..aaf14e05 --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/messages.ts @@ -0,0 +1,87 @@ +import { defineMessages } from '@openedx/frontend-base'; + +const messages = defineMessages({ + + enrollmentSummaryTitle: { + id: 'instruct.courseInfo.enrollmentSummary.title', + defaultMessage: 'Course Enrollment', + description: 'Title for the enrollment summary section', + }, + allEnrollmentsLabel: { + id: 'instruct.courseInfo.enrollmentSummary.allEnrollments', + defaultMessage: 'All Enrollments', + description: 'Label for all enrollments count', + }, + staffAndAdminsLabel: { + id: 'instruct.courseInfo.enrollmentSummary.staffAndAdmins', + defaultMessage: 'Staff / Admin', + description: 'Label for staff and admins count', + }, + learnersLabel: { + id: 'instruct.courseInfo.enrollmentSummary.learners', + defaultMessage: 'Learners', + description: 'Label for learners count', + }, + verified: { + id: 'instruct.courseInfo.enrollmentSummary.verified', + defaultMessage: 'Verified', + description: 'Label for verified enrollments count', + }, + audit: { + id: 'instruct.courseInfo.enrollmentSummary.audit', + defaultMessage: 'Audit', + description: 'Label for audit enrollments count', + }, + masters: { + id: 'instruct.courseInfo.enrollmentSummary.masters', + defaultMessage: 'Master\'s', + description: 'Label for master\'s enrollments count', + }, + honor: { + id: 'instruct.courseInfo.enrollmentSummary.honor', + defaultMessage: 'Honor', + description: 'Label for honor enrollments count', + }, + professional: { + id: 'instruct.courseInfo.enrollmentSummary.professional', + defaultMessage: 'Professional', + description: 'Label for professional enrollments count', + }, + noIdProfessional: { + id: 'instruct.courseInfo.enrollmentSummary.noIdProfessional', + defaultMessage: 'No ID Professional', + description: 'Label for no ID professional enrollments count', + }, + credit: { + id: 'instruct.courseInfo.enrollmentSummary.credit', + defaultMessage: 'Credit', + description: 'Label for credit enrollments count', + }, + paidBootcamp: { + id: 'instruct.courseInfo.enrollmentSummary.paidBootcamp', + defaultMessage: 'Paid Bootcamp', + description: 'Label for paid bootcamp enrollments count', + }, + unpaidBootcamp: { + id: 'instruct.courseInfo.enrollmentSummary.unpaidBootcamp', + defaultMessage: 'Unpaid Bootcamp', + description: 'Label for unpaid bootcamp enrollments count', + }, + executiveEducation: { + id: 'instruct.courseInfo.enrollmentSummary.executiveEducation', + defaultMessage: 'Executive Education', + description: 'Label for executive education enrollments count', + }, + paidExecutiveEducation: { + id: 'instruct.courseInfo.enrollmentSummary.paidExecutiveEducation', + defaultMessage: 'Paid Executive Education', + description: 'Label for paid executive education enrollments count', + }, + unpaidExecutiveEducation: { + id: 'instruct.courseInfo.enrollmentSummary.unpaidExecutiveEducation', + defaultMessage: 'Unpaid Executive Education', + description: 'Label for unpaid executive education enrollments count', + }, +}); + +export default messages; diff --git a/src/courseInfo/components/EnrollmentSummary/utils.test.ts b/src/courseInfo/components/EnrollmentSummary/utils.test.ts new file mode 100644 index 00000000..b23eebe1 --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/utils.test.ts @@ -0,0 +1,24 @@ +import { formatNumberWithCommas } from './utils'; + +describe('formatNumberWithCommas', () => { + it('formats valid numbers with commas', () => { + expect(formatNumberWithCommas('1000')).toBe('1,000'); + expect(formatNumberWithCommas('1234567')).toBe('1,234,567'); + }); + + it('removes existing commas and spaces before formatting', () => { + expect(formatNumberWithCommas('1,000')).toBe('1,000'); + expect(formatNumberWithCommas('1 000')).toBe('1,000'); + expect(formatNumberWithCommas('1, 000')).toBe('1,000'); + }); + + it('returns original string for invalid numbers', () => { + expect(formatNumberWithCommas('abc')).toBe('abc'); + expect(formatNumberWithCommas('12abc')).toBe('12abc'); + }); + + it('handles small numbers without commas', () => { + expect(formatNumberWithCommas('123')).toBe('123'); + expect(formatNumberWithCommas('0')).toBe('0'); + }); +}); diff --git a/src/courseInfo/components/EnrollmentSummary/utils.ts b/src/courseInfo/components/EnrollmentSummary/utils.ts new file mode 100644 index 00000000..7cfd0fe3 --- /dev/null +++ b/src/courseInfo/components/EnrollmentSummary/utils.ts @@ -0,0 +1,7 @@ +export const formatNumberWithCommas = (numberString: string): string => { + const cleanNumber = numberString.replace(/[,\s]/g, ''); + if (isNaN(Number(cleanNumber))) { + return numberString; + } + return Number(cleanNumber).toLocaleString('en-US'); +};