Skip to content

Commit

Permalink
✨(frontend) conditionaly show attestation tab
Browse files Browse the repository at this point in the history
On certificate page, we don't want user that only have regular
certificate to see an empty "attestation certificate" tab.
  • Loading branch information
rlecellier committed May 17, 2024
1 parent d2927af commit c84334e
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 74 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
Expand Down
137 changes: 81 additions & 56 deletions src/frontend/js/pages/DashboardCertificates/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -32,18 +35,6 @@ jest.mock('utils/indirection/window', () => ({
}));

describe('<DashboardCertificates/>', () => {
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/', []);
Expand All @@ -55,63 +46,94 @@ describe('<DashboardCertificates/>', () => {
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(<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />, {
wrapper: BaseJoanieAppWrapper,
});

render(
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<HistoryContext.Provider value={makeHistoryOf({})}>
<SessionProvider>
<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />
</SessionProvider>
</HistoryContext.Provider>
</IntlProvider>
</QueryClientProvider>,
// 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(<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />, {
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 () => {
const certificates: Certificate[] = CertificateFactory().many(30);
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(
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<HistoryContext.Provider value={makeHistoryOf({})}>
<SessionProvider>
<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />
</SessionProvider>
</HistoryContext.Provider>
</IntlProvider>
</QueryClientProvider>,
);
render(<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />, {
wrapper: BaseJoanieAppWrapper,
});

// Make sure the spinner appear during first load.
await expectSpinner('Loading certificates...');
Expand All @@ -126,7 +148,7 @@ describe('<DashboardCertificates/>', () => {
});

// 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(() =>
Expand All @@ -153,30 +175,33 @@ describe('<DashboardCertificates/>', () => {
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(
<QueryClientProvider client={createTestQueryClient({ user: true })}>
<IntlProvider locale="en">
<HistoryContext.Provider value={makeHistoryOf({})}>
<SessionProvider>
<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />
</SessionProvider>
</HistoryContext.Provider>
</IntlProvider>
</QueryClientProvider>,
);
render(<DashboardTest initialRoute={LearnerDashboardPaths.CERTIFICATES} />, {
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();
});
});
42 changes: 25 additions & 17 deletions src/frontend/js/pages/DashboardCertificates/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 (
<div className="dashboard-certificates">
<Tabs
initialActiveTabName={
certificateType === CertificateType.ORDER
? 'order-certificate-tab'
: 'enrollment-certificate-tab'
}
>
<Tabs.Tab name="order-certificate-tab" href={LearnerDashboardPaths.ORDER_CERTIFICATES}>
<FormattedMessage {...messages.orderCertificateTabLabel} />
</Tabs.Tab>
<Tabs.Tab
name="enrollment-certificate-tab"
href={LearnerDashboardPaths.ENROLLMENT_CERTIFICATES}
>
<FormattedMessage {...messages.enrollmentCertificateTabLabel} />
</Tabs.Tab>
</Tabs>
{isFetched && enrollmentCertificates.length > 0 && (
<Tabs initialActiveTabName={certificateType}>
<Tabs.Tab name={CertificateType.ORDER} href={LearnerDashboardPaths.ORDER_CERTIFICATES}>
<FormattedMessage {...messages.orderCertificateTabLabel} />
</Tabs.Tab>
<Tabs.Tab
name={CertificateType.ENROLLMENT}
href={LearnerDashboardPaths.ENROLLMENT_CERTIFICATES}
>
<FormattedMessage {...messages.enrollmentCertificateTabLabel} />
</Tabs.Tab>
</Tabs>
)}

<div className="dashboard-certificates__content">
<CertificatesList certificateType={certificateType} />
</div>
Expand Down
1 change: 1 addition & 0 deletions src/frontend/js/settings/settings.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit c84334e

Please sign in to comment.