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 13, 2024
1 parent b8cb763 commit db66396
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 68 deletions.
6 changes: 5 additions & 1 deletion src/frontend/js/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const Tabs = ({ children }: PropsWithChildren) => {
};

Tabs.Header = ({ children }: PropsWithChildren) => {
return <div className="tabs__header">{children}</div>;
return (
<div className="tabs__header" data-testid="tabs-header">
{children}
</div>
);
};

Tabs.Content = ({ children }: PropsWithChildren) => {
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
125 changes: 75 additions & 50 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,39 +46,78 @@ describe('<DashboardCertificates/>', () => {
fetchMock.restore();
});

it('renders an empty list of certificates', async () => {
it('should render both certificate tabs', async () => {
fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', {
results: [],
next: null,
previous: null,
count: 30,
});
fetchMock.get(
'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=10',
{
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=10', {
results: [],
next: null,
previous: null,
count: 30,
});
fetchMock.get(
'https://joanie.endpoint/api/v1.0/certificates/?type=enrollment&page=1&page_size=10',
{
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=enrollment&page=1&page_size=10',
{
results: [],
next: null,
previous: null,
count: 0,
},
);
fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', {
results: certificatesPage1,
next: null,
Expand All @@ -101,17 +131,9 @@ describe('<DashboardCertificates/>', () => {
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 Down Expand Up @@ -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=enrollment&page=1&page_size=10',
{
results: [],
next: null,
previous: null,
count: 0,
},
);
fetchMock.get('https://joanie.endpoint/api/v1.0/certificates/?type=order&page=1&page_size=10', {
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();
});
});
45 changes: 29 additions & 16 deletions src/frontend/js/pages/DashboardCertificates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { FormattedMessage, defineMessages } from 'react-intl';
import { generatePath } from 'react-router-dom';
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 @@ -23,24 +25,35 @@ 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.Header>
<Tabs.Tab
name="order-certificate-tab"
initialIsActive={certificateType === CertificateType.ORDER}
href={LearnerDashboardPaths.ORDER_CERTIFICATES}
>
<FormattedMessage {...messages.orderCertificateTabLabel} />
</Tabs.Tab>
<Tabs.Tab
name="enrollment-certificate-tab"
initialIsActive={certificateType === CertificateType.ENROLLMENT}
href={generatePath(LearnerDashboardPaths.ENROLLMENT_CERTIFICATES)}
>
<FormattedMessage {...messages.enrollmentCertificateTabLabel} />
</Tabs.Tab>
</Tabs.Header>
{isFetched && enrollmentCertificates.length > 0 && (
<Tabs.Header>
<Tabs.Tab
name="order-certificate-tab"
initialIsActive={certificateType === CertificateType.ORDER}
href={LearnerDashboardPaths.ORDER_CERTIFICATES}
>
<FormattedMessage {...messages.orderCertificateTabLabel} />
</Tabs.Tab>
<Tabs.Tab
name="enrollment-certificate-tab"
initialIsActive={certificateType === CertificateType.ENROLLMENT}
href={generatePath(LearnerDashboardPaths.ENROLLMENT_CERTIFICATES)}
>
<FormattedMessage {...messages.enrollmentCertificateTabLabel} />
</Tabs.Tab>
</Tabs.Header>
)}

<div className="dashboard-certificates__content">
<CertificatesList certificateType={certificateType} />
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: 10,
courseLearnerList: DEFAULT_PER_PAGE,
useUnionResources: DEFAULT_PER_PAGE,
useCourseProductUnion: DEFAULT_PER_PAGE,
Expand Down

0 comments on commit db66396

Please sign in to comment.