diff --git a/CHANGELOG.md b/CHANGELOG.md index e64d35dde0..52ab59114e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## Added +- The enrollment's certificate tab in learner dashbaord is no longer + displayed when the list is empty. - User profile in the learner dashboard is now always synchronized with openEdx profile informations. - Add a tab in learner dashboard certificate page to render both order diff --git a/src/frontend/js/pages/DashboardCertificates/components/CertificateList/index.tsx b/src/frontend/js/pages/DashboardCertificates/components/CertificateList/index.tsx index 2b5d772687..55008777ef 100644 --- a/src/frontend/js/pages/DashboardCertificates/components/CertificateList/index.tsx +++ b/src/frontend/js/pages/DashboardCertificates/components/CertificateList/index.tsx @@ -8,6 +8,7 @@ import { Spinner } from 'components/Spinner'; import Banner, { BannerType } from 'components/Banner'; import { DashboardItemCertificate } from 'widgets/Dashboard/components/DashboardItem/Certificate'; import { CertificateType } from 'types/Joanie'; +import { PER_PAGE } from 'settings'; const messages = defineMessages({ loading: { @@ -27,7 +28,7 @@ interface CertificatesListProps { } const CertificatesList = ({ certificateType }: CertificatesListProps) => { const intl = useIntl(); - const pagination = usePagination({}); + const pagination = usePagination({ itemsPerPage: PER_PAGE.certificateList }); const certificates = useCertificates( { type: certificateType, diff --git a/src/frontend/js/pages/DashboardCertificates/index.spec.tsx b/src/frontend/js/pages/DashboardCertificates/index.spec.tsx index 6deda585bc..02f21a3dc3 100644 --- a/src/frontend/js/pages/DashboardCertificates/index.spec.tsx +++ b/src/frontend/js/pages/DashboardCertificates/index.spec.tsx @@ -1,22 +1,25 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { IntlProvider } from 'react-intl'; import fetchMock from 'fetch-mock'; import userEvent from '@testing-library/user-event'; import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie'; -import { createTestQueryClient } from 'utils/test/createTestQueryClient'; import { Certificate } from 'types/Joanie'; import { resolveAll } from 'utils/resolveAll'; import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner'; import { expectBannerError, expectBannerInfo } from 'utils/test/expectBanner'; import { Deferred } from 'utils/test/deferred'; -import { History, HistoryContext } from 'hooks/useHistory'; -import { SessionProvider } from 'contexts/SessionContext'; +import * as mockUseHistory from 'hooks/useHistory'; import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest'; import { CertificateFactory } from 'utils/test/factories/joanie'; import { HttpStatusCode } from 'utils/errors/HttpError'; import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths'; +import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper'; + +jest.mock('hooks/useHistory', () => ({ + __esModule: true, + ...mockUseHistory, + useHistory: () => [jest.fn(), jest.fn(), jest.fn()], +})); jest.mock('utils/context', () => ({ __esModule: true, @@ -32,18 +35,6 @@ jest.mock('utils/indirection/window', () => ({ })); describe('', () => { - const historyPushState = jest.fn(); - const historyReplaceState = jest.fn(); - const makeHistoryOf: (params: any) => History = () => [ - { - state: { name: '', data: {} }, - title: '', - url: `/`, - }, - historyPushState, - historyReplaceState, - ]; - beforeEach(() => { fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []); fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []); @@ -55,32 +46,62 @@ describe('', () => { fetchMock.restore(); }); - it('renders an empty list of certificates', async () => { - fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', { + it('should render both certificate tabs', async () => { + fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=25', { results: [], next: null, previous: null, count: 30, }); + fetchMock.get( + 'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=25', + { + results: [CertificateFactory().one()], + next: null, + previous: null, + count: 1, + }, + ); + + render(, { + wrapper: BaseJoanieAppWrapper, + }); - render( - - - - - - - - - , + // Make sure the spinner appear during first load. + await expectSpinner('Loading certificates...'); + await expectNoSpinner('Loading certificates...'); + expect(screen.queryByTestId('tabs-header')).toBeInTheDocument(); + }); + + it('renders an empty list of certificates', async () => { + fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=25', { + results: [], + next: null, + previous: null, + count: 30, + }); + fetchMock.get( + 'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=25', + { + results: [], + next: null, + previous: null, + count: 0, + }, ); + render(, { + wrapper: BaseJoanieAppWrapper, + }); + // Make sure the spinner appear during first load. await expectSpinner('Loading certificates...'); await expectNoSpinner('Loading certificates...'); await expectBannerInfo('You have no certificates yet.'); + + expect(screen.queryByTestId('tabs-header')).not.toBeInTheDocument(); }); it('renders 3 pages of certificates', async () => { @@ -88,30 +109,31 @@ describe('', () => { const certificatesPage1 = certificates.slice(0, 10); const certificatesPage2 = certificates.slice(10, 20); - fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', { + fetchMock.get( + 'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=25', + { + results: [], + next: null, + previous: null, + count: 0, + }, + ); + fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=25', { results: certificatesPage1, next: null, previous: null, - count: 30, + count: 60, }); const page2Deferred = new Deferred(); fetchMock.get( - 'https://joanie.endpoint/api/v1.0/certificates/?type=order&page=2&page_size=10', + 'https://joanie.endpoint/api/v1.0/certificates/?type=order&page=2&page_size=25', page2Deferred.promise, ); - render( - - - - - - - - - , - ); + render(, { + wrapper: BaseJoanieAppWrapper, + }); // Make sure the spinner appear during first load. await expectSpinner('Loading certificates...'); @@ -126,7 +148,7 @@ describe('', () => { }); // Go to page 2. - await userEvent.click(screen.getByText('Next page 2')); + await userEvent.click(await screen.findByText('Next page 2')); // Make sure the loading class is set. await waitFor(() => @@ -153,30 +175,33 @@ describe('', () => { await resolveAll(certificatesPage1, async (certificate) => { await screen.findByText(certificate.certificate_definition.title); }); + expect(screen.queryByTestId('tabs-header')).not.toBeInTheDocument(); }); it('shows an error when request to retrieve certificates fails', async () => { - fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', { + fetchMock.get( + 'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=25', + { + results: [], + next: null, + previous: null, + count: 0, + }, + ); + fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=25', { status: HttpStatusCode.INTERNAL_SERVER_ERROR, body: 'Internal Server Error', }); - render( - - - - - - - - - , - ); + render(, { + wrapper: BaseJoanieAppWrapper, + }); // Make sure error is shown. await expectBannerError('An error occurred while fetching certificates. Please retry later.'); // ... and the spinner hidden. await expectNoSpinner('Loading ...'); + expect(screen.queryByTestId('tabs-header')).not.toBeInTheDocument(); }); }); diff --git a/src/frontend/js/pages/DashboardCertificates/index.tsx b/src/frontend/js/pages/DashboardCertificates/index.tsx index 0fd3e9a2dd..6dd17953fe 100644 --- a/src/frontend/js/pages/DashboardCertificates/index.tsx +++ b/src/frontend/js/pages/DashboardCertificates/index.tsx @@ -1,6 +1,8 @@ import { FormattedMessage, defineMessages } from 'react-intl'; import { CertificateType } from 'types/Joanie'; import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths'; +import { useCertificates } from 'hooks/useCertificates'; +import { PER_PAGE } from 'settings'; import Tabs from '../../components/Tabs'; import CertificatesList from './components/CertificateList'; @@ -22,25 +24,31 @@ interface DashboardCertificatesProps { } export const DashboardCertificates = ({ certificateType }: DashboardCertificatesProps) => { + const { + items: enrollmentCertificates, + states: { isFetched }, + } = useCertificates({ + type: CertificateType.ENROLLMENT, + page: 1, + page_size: PER_PAGE.certificateList, + }); + return (
- - - - - - - - + {isFetched && enrollmentCertificates.length > 0 && ( + + + + + + + + + )} +
diff --git a/src/frontend/js/settings/settings.prod.ts b/src/frontend/js/settings/settings.prod.ts index b5688dd2e7..c8ab7e7ab7 100644 --- a/src/frontend/js/settings/settings.prod.ts +++ b/src/frontend/js/settings/settings.prod.ts @@ -54,6 +54,7 @@ export const CONTRACT_DOWNLOAD_SETTINGS = { const DEFAULT_PER_PAGE = 50; export const PER_PAGE = { teacherContractList: 25, + certificateList: 25, courseLearnerList: DEFAULT_PER_PAGE, useUnionResources: DEFAULT_PER_PAGE, useCourseProductUnion: DEFAULT_PER_PAGE,