richie-education 2.25.0-b2.dev68 → 2.25.0-b2.dev71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/js/api/joanie.ts +19 -5
  2. package/js/components/DownloadContractButton/index.spec.tsx +17 -4
  3. package/js/components/DownloadContractButton/index.tsx +6 -11
  4. package/js/hooks/useContractArchive/index.download.spec.tsx +8 -1
  5. package/js/hooks/useContractArchive/index.ts +8 -27
  6. package/js/hooks/useDownloadCertificate/index.spec.tsx +17 -5
  7. package/js/hooks/useDownloadCertificate/index.tsx +2 -20
  8. package/js/utils/download.ts +43 -0
  9. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +12 -4
  10. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
  11. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.spec.tsx → CourseEnrolling/index.spec.tsx} +44 -13
  12. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +2 -2
  13. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +38 -57
  14. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +7 -19
  15. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  16. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +76 -24
  17. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +1 -1
  18. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.spec.tsx +8 -1
  19. package/package.json +1 -1
package/js/api/joanie.ts CHANGED
@@ -23,13 +23,21 @@ interface CheckStatusOptions {
23
23
  ignoredErrorStatus: (number | HttpStatusCode)[];
24
24
  }
25
25
 
26
+ export async function getFileFromResponse(response: Response): Promise<File> {
27
+ const filenameRegex = /filename="(.*)";/;
28
+ const dispositionHeader = response.headers.get('Content-Disposition');
29
+ const matches = dispositionHeader?.match(filenameRegex);
30
+
31
+ return new File([await response.blob()], matches ? matches[1] : '');
32
+ }
33
+
26
34
  export function getResponseBody(response: Response) {
27
35
  if (response.headers.get('Content-Type') === 'application/json') {
28
36
  return response.json();
29
37
  }
30
38
  const fileType = ['application/pdf', 'application/zip'];
31
39
  if (fileType.includes(response.headers.get('Content-Type') || '')) {
32
- return response.blob();
40
+ return new Promise((resolve) => resolve(response));
33
41
  }
34
42
  return response.text();
35
43
  }
@@ -321,7 +329,7 @@ const API = (): Joanie.API => {
321
329
  let url = ROUTES.user.orders.invoice.download.replace(':id', order_id);
322
330
  url += `?${queryString.stringify({ reference: invoice_reference })}`;
323
331
 
324
- return fetchWithJWT(url).then(checkStatus);
332
+ return fetchWithJWT(url).then(checkStatus).then(getFileFromResponse);
325
333
  },
326
334
  },
327
335
  submit_for_signature: async (id) =>
@@ -346,7 +354,9 @@ const API = (): Joanie.API => {
346
354
  },
347
355
  certificates: {
348
356
  download: async (id: string): Promise<File> =>
349
- fetchWithJWT(ROUTES.user.certificates.download.replace(':id', id)).then(checkStatus),
357
+ fetchWithJWT(ROUTES.user.certificates.download.replace(':id', id))
358
+ .then(checkStatus)
359
+ .then(getFileFromResponse),
350
360
  get: async (filters) => {
351
361
  return fetchWithJWT(buildApiUrl(ROUTES.user.certificates.get, filters)).then(checkStatus);
352
362
  },
@@ -382,7 +392,9 @@ const API = (): Joanie.API => {
382
392
  download(id: string): Promise<any> {
383
393
  return fetchWithJWT(ROUTES.user.contracts.download.replace(':id', id), {
384
394
  method: 'GET',
385
- }).then(checkStatus);
395
+ })
396
+ .then(checkStatus)
397
+ .then(getFileFromResponse);
386
398
  },
387
399
  zip_archive: {
388
400
  check: async (id) => {
@@ -399,7 +411,9 @@ const API = (): Joanie.API => {
399
411
  get: async (id) => {
400
412
  return fetchWithJWT(ROUTES.user.contracts.zip_archive.get.replace(':id', id), {
401
413
  method: 'GET',
402
- }).then(checkStatus);
414
+ })
415
+ .then(checkStatus)
416
+ .then(getFileFromResponse);
403
417
  },
404
418
  },
405
419
  },
@@ -10,6 +10,7 @@ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/fac
10
10
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
11
11
  import JoanieApiProvider from 'contexts/JoanieApiContext';
12
12
  import { alert } from 'utils/indirection/window';
13
+ import { HttpStatusCode } from 'utils/errors/HttpError';
13
14
  import DownloadContractButton from '.';
14
15
 
15
16
  jest.mock('utils/context', () => ({
@@ -65,7 +66,16 @@ describe('<DownloadContractButton/>', () => {
65
66
  const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
66
67
  order.contract!.id
67
68
  }/download/`;
68
- fetchMock.get(DOWNLOAD_URL, 'contract content');
69
+
70
+ fetchMock.get(DOWNLOAD_URL, () => ({
71
+ status: HttpStatusCode.OK,
72
+ body: new Blob(['contract content']),
73
+ headers: {
74
+ 'Content-Disposition': 'attachment; filename="test.pdf";',
75
+ 'Content-Type': 'application/pdf',
76
+ },
77
+ }));
78
+ const expectedFile = new File(['contract content'], 'test.pdf');
69
79
 
70
80
  render(
71
81
  <Wrapper>
@@ -88,9 +98,9 @@ describe('<DownloadContractButton/>', () => {
88
98
  // eslint-disable-next-line compat/compat
89
99
  expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
90
100
  // eslint-disable-next-line compat/compat
91
- expect(URL.createObjectURL).toHaveBeenCalledWith('contract content');
101
+ expect(URL.createObjectURL).toHaveBeenCalledWith(expectedFile);
92
102
  expect(window.open).toHaveBeenCalledTimes(1);
93
- expect(window.open).toHaveBeenCalledWith('contract content');
103
+ expect(window.open).toHaveBeenCalledWith(expectedFile);
94
104
  });
95
105
 
96
106
  it('fails downloading the contract and shows an error', async () => {
@@ -109,7 +119,10 @@ describe('<DownloadContractButton/>', () => {
109
119
  const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
110
120
  order.contract!.id
111
121
  }/download/`;
112
- fetchMock.get(DOWNLOAD_URL, 500);
122
+ fetchMock.get(DOWNLOAD_URL, {
123
+ status: HttpStatusCode.INTERNAL_SERVER_ERROR,
124
+ body: 'Bad request',
125
+ });
113
126
 
114
127
  render(
115
128
  <Wrapper>
@@ -1,9 +1,9 @@
1
1
  import { Button } from '@openfun/cunningham-react';
2
2
  import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
3
3
  import { useJoanieApi } from 'contexts/JoanieApiContext';
4
- import { handle } from 'utils/errors/handle';
5
4
  import { Contract } from 'types/Joanie';
6
5
  import { alert } from 'utils/indirection/window';
6
+ import { browserDownloadFromBlob } from 'utils/download';
7
7
 
8
8
  const messages = defineMessages({
9
9
  contractDownloadActionLabel: {
@@ -29,16 +29,11 @@ const DownloadContractButton = ({ contract, className }: DownloadContractButtonP
29
29
  const intl = useIntl();
30
30
 
31
31
  const downloadContract = async () => {
32
- try {
33
- const blob = await api.user.contracts.download(contract!.id);
34
- // eslint-disable-next-line compat/compat
35
- const file = window.URL.createObjectURL(blob);
36
- window.open(file);
37
-
38
- // eslint-disable-next-line compat/compat
39
- URL.revokeObjectURL(file);
40
- } catch (e) {
41
- handle(e);
32
+ const success = await browserDownloadFromBlob(
33
+ () => api.user.contracts.download(contract!.id),
34
+ true,
35
+ );
36
+ if (!success) {
42
37
  alert(intl.formatMessage(messages.contractDownloadError));
43
38
  }
44
39
  };
@@ -71,7 +71,14 @@ describe('useContractArchive', () => {
71
71
  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
72
72
 
73
73
  result.current.methods.get(archiveId);
74
- deferred.resolve(HttpStatusCode.OK);
74
+ deferred.resolve({
75
+ status: HttpStatusCode.OK,
76
+ body: new Blob(['test']),
77
+ headers: {
78
+ 'Content-Disposition': 'attachment; filename="test.pdf";',
79
+ 'Content-Type': 'application/pdf',
80
+ },
81
+ });
75
82
 
76
83
  await waitFor(() => {
77
84
  expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
@@ -1,4 +1,5 @@
1
1
  import { useJoanieApi } from 'contexts/JoanieApiContext';
2
+ import { browserDownloadFromBlob } from 'utils/download';
2
3
  import { HttpStatusCode } from 'utils/errors/HttpError';
3
4
  import { handle } from 'utils/errors/handle';
4
5
 
@@ -17,25 +18,6 @@ const extractArchiveId = (url: string): string => {
17
18
  return match[0];
18
19
  };
19
20
 
20
- // TODO: should be factorized with useDownloadCertificate
21
- // and maybe DownloadContractButton
22
- const buildArchiveFromBlob = (fileName: string, blob: Blob) => {
23
- // eslint-disable-next-line compat/compat
24
- const url = URL.createObjectURL(blob);
25
- const $link = document.createElement('a');
26
- $link.href = url;
27
- $link.download = fileName;
28
-
29
- const revokeObject = () => {
30
- // eslint-disable-next-line compat/compat
31
- URL.revokeObjectURL(url);
32
- window.removeEventListener('blur', revokeObject);
33
- };
34
-
35
- window.addEventListener('blur', revokeObject);
36
- $link.click();
37
- };
38
-
39
21
  const useContractArchive = () => {
40
22
  const api = useJoanieApi();
41
23
  return {
@@ -57,17 +39,16 @@ const useContractArchive = () => {
57
39
  return false;
58
40
  },
59
41
  get: async (archiveId: string): Promise<true | void> => {
60
- try {
61
- const response = api.user.contracts.zip_archive.get(archiveId);
62
- buildArchiveFromBlob('contracts.zip', await response);
63
- return true;
64
- } catch (error) {
65
- handle(error);
66
- }
67
-
68
42
  // FIXME: two thing could happen here with HttpStatusCode.NOT_FOUND response:
69
43
  // * nothing found because the zip is generating.
70
44
  // * nothing found because the zip doesn't and will never exist.
45
+ const success = await browserDownloadFromBlob(() =>
46
+ api.user.contracts.zip_archive.get(archiveId),
47
+ );
48
+
49
+ if (success) {
50
+ return true;
51
+ }
71
52
  },
72
53
  create: async (organizationId: string): Promise<string> => {
73
54
  const response = await api.user.contracts.zip_archive.create({
@@ -2,7 +2,7 @@ import fetchMock from 'fetch-mock';
2
2
  import { PropsWithChildren } from 'react';
3
3
  import { QueryClientProvider } from '@tanstack/react-query';
4
4
  import { IntlProvider } from 'react-intl';
5
- import { fireEvent, renderHook, waitFor } from '@testing-library/react';
5
+ import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
6
6
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
7
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
8
8
  import { useDownloadCertificate } from 'hooks/useDownloadCertificate/index';
@@ -72,9 +72,19 @@ describe('useDownloadCertificate', () => {
72
72
  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
73
73
  expect(result.current.loading).toBe(false);
74
74
 
75
- result.current.download(certificate.id);
76
- await waitFor(() => expect(result.current.loading).toBe(true));
77
- deferred.resolve(HttpStatusCode.OK);
75
+ act(() => {
76
+ result.current.download(certificate.id);
77
+ });
78
+ expect(result.current.loading).toBe(true);
79
+
80
+ deferred.resolve({
81
+ status: HttpStatusCode.OK,
82
+ body: new Blob(['test']),
83
+ headers: {
84
+ 'Content-Disposition': 'attachment; filename="test.pdf";',
85
+ 'Content-Type': 'application/pdf',
86
+ },
87
+ });
78
88
 
79
89
  await waitFor(() => {
80
90
  expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
@@ -109,7 +119,9 @@ describe('useDownloadCertificate', () => {
109
119
  // eslint-disable-next-line compat/compat
110
120
  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
111
121
 
112
- await result.current.download(certificate.id);
122
+ act(() => {
123
+ result.current.download(certificate.id);
124
+ });
113
125
 
114
126
  await waitFor(() => {
115
127
  expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react';
2
- import { handle } from 'utils/errors/handle';
3
2
  import { useJoanieApi } from 'contexts/JoanieApiContext';
3
+ import { browserDownloadFromBlob } from 'utils/download';
4
4
 
5
5
  export const useDownloadCertificate = () => {
6
6
  const [loading, setLoading] = useState(false);
@@ -9,25 +9,7 @@ export const useDownloadCertificate = () => {
9
9
  return {
10
10
  download: async (certificateId: string) => {
11
11
  setLoading(true);
12
- try {
13
- const $link = document.createElement('a');
14
- const file = await API.user.certificates.download(certificateId);
15
- // eslint-disable-next-line compat/compat
16
- const url = URL.createObjectURL(file);
17
- $link.href = url;
18
- $link.download = '';
19
-
20
- const revokeObject = () => {
21
- // eslint-disable-next-line compat/compat
22
- URL.revokeObjectURL(url);
23
- window.removeEventListener('blur', revokeObject);
24
- };
25
-
26
- window.addEventListener('blur', revokeObject);
27
- $link.click();
28
- } catch (error) {
29
- handle(error);
30
- }
12
+ await browserDownloadFromBlob(() => API.user.certificates.download(certificateId));
31
13
  setLoading(false);
32
14
  },
33
15
  loading,
@@ -0,0 +1,43 @@
1
+ import { handle } from './errors/handle';
2
+
3
+ /**
4
+ * browserDownloadFromBlob handle direct download of a file api response.
5
+ *
6
+ * @param downloadFunction, an api promise that return a File
7
+ * @param newWindow, does it open in a new window or not
8
+ * @returns boolean, true for success
9
+ */
10
+ export const browserDownloadFromBlob = async (
11
+ downloadFunction: () => Promise<File>,
12
+ newWindow: boolean = false,
13
+ ) => {
14
+ try {
15
+ const file = await downloadFunction();
16
+
17
+ // eslint-disable-next-line compat/compat
18
+ const url = URL.createObjectURL(file);
19
+
20
+ if (newWindow) {
21
+ window.open(url);
22
+ return;
23
+ }
24
+
25
+ const $link = document.createElement('a');
26
+ $link.href = url;
27
+ $link.download = file.name;
28
+
29
+ const revokeObject = () => {
30
+ // eslint-disable-next-line compat/compat
31
+ URL.revokeObjectURL(url);
32
+ window.removeEventListener('blur', revokeObject);
33
+ };
34
+
35
+ window.addEventListener('blur', revokeObject);
36
+ $link.click();
37
+ return true;
38
+ } catch (error) {
39
+ handle(error);
40
+ }
41
+
42
+ return false;
43
+ };
@@ -48,9 +48,13 @@ describe.each([
48
48
  beforeAll(() => {
49
49
  // eslint-disable-next-line compat/compat
50
50
  URL.createObjectURL = jest.fn();
51
+ // eslint-disable-next-line compat/compat
52
+ URL.revokeObjectURL = jest.fn();
53
+ HTMLAnchorElement.prototype.click = jest.fn();
51
54
  });
52
55
 
53
56
  beforeEach(() => {
57
+ // SessionProvider queries
54
58
  fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
55
59
  fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
56
60
  fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
@@ -88,10 +92,14 @@ describe.each([
88
92
  order: OrderFactory().one(),
89
93
  }).one();
90
94
 
91
- fetchMock.get(
92
- `https://joanie.test/api/v1.0/certificates/${certificate.id}/download/`,
93
- HttpStatusCode.OK,
94
- );
95
+ fetchMock.get(`https://joanie.test/api/v1.0/certificates/${certificate.id}/download/`, () => ({
96
+ status: HttpStatusCode.OK,
97
+ body: new Blob(['test']),
98
+ headers: {
99
+ 'Content-Disposition': 'attachment; filename="test.pdf";',
100
+ 'Content-Type': 'application/pdf',
101
+ },
102
+ }));
95
103
 
96
104
  render(
97
105
  <DashboardItemCertificate certificate={certificate} productType={ProductType.CREDENTIAL} />,
@@ -0,0 +1,76 @@
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+ import useDateRelative from 'hooks/useDateRelative';
3
+ import { Priority } from 'types';
4
+ import { CourseRun } from 'types/Joanie';
5
+ import useDateFormat, { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
6
+
7
+ const messages = defineMessages({
8
+ onGoingRunPeriod: {
9
+ id: 'components.useCourseRunPeriodMessage.onGoingRunPeriod',
10
+ description: 'Text to display when a course run is on going.',
11
+ defaultMessage: 'This session started on {startDate} and will end on {endDate}',
12
+ },
13
+ futureRunPeriod: {
14
+ id: 'components.useCourseRunPeriodMessage.futureRunPeriod',
15
+ description: 'Text to display the period of a future course run.',
16
+ defaultMessage: 'This session starts {relativeStartDate}, the {startDate}',
17
+ },
18
+ onGoingEnrolledRunPeriod: {
19
+ id: 'components.useCourseRunPeriodMessage.onGoingEnrolledRunPeriod',
20
+ description: 'Text to display when a course run is ongoing and the user is enrolled to.',
21
+ defaultMessage: "You are enrolled for this session. It's open from {startDate} to {endDate}",
22
+ },
23
+ futureEnrolledRunPeriod: {
24
+ id: 'components.useCourseRunPeriodMessage.futureEnrolledRunPeriod',
25
+ description: 'Text to display when a course run is not yet opened and the user is enrolled to.',
26
+ defaultMessage:
27
+ 'You are enrolled for this session. It starts {relativeStartDate}, the {startDate}.',
28
+ },
29
+ archivedEnrolledRunPeriod: {
30
+ id: 'components.useCourseRunPeriodMessage.archivedEnrolledRunPeriod',
31
+ description: 'Text to display when a course run is archived and the user is enrolled to.',
32
+ defaultMessage: 'You are enrolled for this session.',
33
+ },
34
+ });
35
+
36
+ const useCourseRunPeriodMessage = (courseRun: CourseRun, enrolled: boolean = false) => {
37
+ const intl = useIntl();
38
+ const formatDate = useDateFormat();
39
+
40
+ const relativeStartDate = useDateRelative(new Date(courseRun.start));
41
+ const startDate = formatDate(courseRun.start, DEFAULT_DATE_FORMAT);
42
+ const endDate = formatDate(courseRun.end, DEFAULT_DATE_FORMAT);
43
+ const isArchived = [Priority.ARCHIVED_CLOSED, Priority.ARCHIVED_OPEN].includes(
44
+ courseRun.state.priority,
45
+ );
46
+ const isOnGoing = [Priority.ONGOING_OPEN, Priority.ONGOING_CLOSED].includes(
47
+ courseRun.state.priority,
48
+ );
49
+ if (enrolled) {
50
+ if (isArchived) {
51
+ return intl.formatMessage(messages.archivedEnrolledRunPeriod);
52
+ }
53
+ if (isOnGoing) {
54
+ return intl.formatMessage(messages.onGoingEnrolledRunPeriod, {
55
+ startDate,
56
+ endDate,
57
+ });
58
+ }
59
+ return intl.formatMessage(messages.futureEnrolledRunPeriod, {
60
+ relativeStartDate,
61
+ startDate,
62
+ });
63
+ }
64
+ if (isOnGoing) {
65
+ return intl.formatMessage(messages.onGoingRunPeriod, {
66
+ startDate,
67
+ endDate,
68
+ });
69
+ }
70
+ return intl.formatMessage(messages.futureRunPeriod, {
71
+ relativeStartDate,
72
+ startDate,
73
+ });
74
+ };
75
+
76
+ export default useCourseRunPeriodMessage;
@@ -1,5 +1,5 @@
1
1
  import { render, screen } from '@testing-library/react';
2
- import { IntlProvider } from 'react-intl';
2
+ import { IntlProvider, createIntl } from 'react-intl';
3
3
  import { PropsWithChildren } from 'react';
4
4
  import { CredentialOrderFactory, EnrollmentFactory } from 'utils/test/factories/joanie';
5
5
  import { Priority } from 'types';
@@ -8,7 +8,8 @@ import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
8
8
  import { CourseRunFactoryFromPriority } from 'utils/test/factories/richie';
9
9
  import { noop } from 'utils';
10
10
  import { computeState } from 'utils/CourseRuns';
11
- import { DashboardItemCourseEnrollingRun, Enrolled } from './DashboardItemCourseEnrolling';
11
+ import { formatRelativeDate } from 'utils/relativeDate';
12
+ import { DashboardItemCourseEnrollingRun, Enrolled } from '.';
12
13
 
13
14
  /**
14
15
  * Most of the component of this file are tested from DashboardItemEnrollment.spec.tsx and
@@ -23,59 +24,89 @@ describe('<Enrolled/>', () => {
23
24
  {
24
25
  buttonTestLabel: 'and access course button',
25
26
  priority: Priority.ONGOING_OPEN,
27
+ priorityLabel: 'ONGOING_OPEN',
26
28
  expectButton: true,
29
+ expectLabelTemplate:
30
+ "You are enrolled for this session. It's open from %fromDate% to %toDate%",
27
31
  },
28
32
  {
29
33
  buttonTestLabel: 'and no access course button',
30
34
  priority: Priority.FUTURE_OPEN,
35
+ priorityLabel: 'FUTURE_OPEN',
31
36
  expectButton: false,
37
+ expectLabelTemplate:
38
+ 'You are enrolled for this session. It starts %fromRelativeDate%, the %fromDate%.',
32
39
  },
33
40
  {
34
41
  buttonTestLabel: 'and access course button',
35
42
  priority: Priority.ARCHIVED_OPEN,
43
+ priorityLabel: 'ARCHIVED_OPEN',
36
44
  expectButton: true,
45
+ expectLabelTemplate: `You are enrolled for this session.`,
37
46
  },
38
47
  {
39
48
  buttonTestLabel: 'and no access course button',
40
49
  priority: Priority.FUTURE_NOT_YET_OPEN,
50
+ priorityLabel: 'FUTURE_NOT_YET_OPEN',
41
51
  expectButton: false,
52
+ expectLabelTemplate:
53
+ 'You are enrolled for this session. It starts %fromRelativeDate%, the %fromDate%.',
42
54
  },
43
55
  {
44
56
  buttonTestLabel: 'and no access course button',
45
57
  priority: Priority.FUTURE_CLOSED,
58
+ priorityLabel: 'FUTURE_CLOSED',
46
59
  expectButton: false,
60
+ expectLabelTemplate:
61
+ 'You are enrolled for this session. It starts %fromRelativeDate%, the %fromDate%.',
47
62
  },
48
63
  {
49
64
  buttonTestLabel: 'and access course button',
50
65
  priority: Priority.ONGOING_CLOSED,
66
+ priorityLabel: 'ONGOING_CLOSED',
51
67
  expectButton: true,
68
+ expectLabelTemplate: `You are enrolled for this session. It's open from %fromDate% to %toDate%`,
52
69
  },
53
70
  {
54
71
  buttonTestLabel: 'and access course button',
55
72
  priority: Priority.ARCHIVED_CLOSED,
73
+ priorityLabel: 'ARCHIVED_CLOSED',
56
74
  expectButton: true,
75
+ expectLabelTemplate: `You are enrolled for this session.`,
57
76
  },
58
77
  {
59
78
  buttonTestLabel: 'and no access course button',
60
79
  priority: Priority.TO_BE_SCHEDULED,
80
+ priorityLabel: 'TO_BE_SCHEDULED',
61
81
  expectButton: false,
82
+ expectLabelTemplate:
83
+ 'You are enrolled for this session. It starts %fromRelativeDate%, the %fromDate%.',
62
84
  },
63
85
  ])(
64
- 'handles enrollments with priority=$priority $buttonTestLabel',
65
- async ({ priority, expectButton }) => {
86
+ 'handles enrollments with priority=$priorityLabel $buttonTestLabel',
87
+ async ({ priority, expectButton, expectLabelTemplate }) => {
66
88
  const enrollment: Enrollment = EnrollmentFactory().one();
67
89
  enrollment.course_run.state.priority = priority;
68
90
  render(<Enrolled enrollment={enrollment} />, { wrapper });
69
- await screen.findByText(
70
- 'You are enrolled for the session from ' +
71
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
72
- new Date(enrollment.course_run.start),
73
- ) +
74
- ' to ' +
75
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
76
- new Date(enrollment.course_run.end),
77
- ),
91
+ const intl = createIntl({ locale: 'en' });
92
+
93
+ const fromDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
94
+ new Date(enrollment.course_run.start),
95
+ );
96
+ const fromRelativeDate = formatRelativeDate(
97
+ new Date(enrollment.course_run.start),
98
+ new Date(),
99
+ intl.locale,
78
100
  );
101
+ const toDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
102
+ new Date(enrollment.course_run.end),
103
+ );
104
+
105
+ const expectLabel = expectLabelTemplate
106
+ .replace('%fromRelativeDate%', fromRelativeDate)
107
+ .replace('%fromDate%', fromDate)
108
+ .replace('%toDate%', toDate);
109
+ expect(await screen.findByText(expectLabel)).toBeInTheDocument();
79
110
  if (expectButton) {
80
111
  const link = screen.getByRole('link', { name: 'Access to course' });
81
112
  expect(link).toBeEnabled();
@@ -4,8 +4,8 @@ import { faker } from '@faker-js/faker';
4
4
  import { TargetCourseFactory } from 'utils/test/factories/joanie';
5
5
  import { StorybookHelper } from 'utils/StorybookHelper';
6
6
  import { Priority } from 'types';
7
- import { enrollment } from './stories.mock';
8
- import { DashboardItemCourseEnrolling } from './DashboardItemCourseEnrolling';
7
+ import { enrollment } from '../stories.mock';
8
+ import { DashboardItemCourseEnrolling } from '.';
9
9
 
10
10
  export default {
11
11
  component: DashboardItemCourseEnrolling,
@@ -13,11 +13,12 @@ import {
13
13
  } from 'types/Joanie';
14
14
  import { Spinner } from 'components/Spinner';
15
15
  import Banner, { BannerType } from 'components/Banner';
16
- import useDateFormat, { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
17
16
  import { Icon, IconTypeEnum } from 'components/Icon';
17
+ import useDateFormat from 'hooks/useDateFormat';
18
18
  import { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
19
- import { RouterButton } from '../RouterButton';
20
- import { useEnroll } from '../../hooks/useEnroll';
19
+ import { RouterButton } from 'widgets/Dashboard/components/RouterButton';
20
+ import { useEnroll } from 'widgets/Dashboard/hooks/useEnroll';
21
+ import useCourseRunPeriodMessage from './hooks/useCourseRunPeriodMessage';
21
22
 
22
23
  const messages = defineMessages({
23
24
  notEnrolled: {
@@ -41,16 +42,6 @@ const messages = defineMessages({
41
42
  description: 'Button to access course when the user is enrolled',
42
43
  defaultMessage: 'Access to course',
43
44
  },
44
- runPeriod: {
45
- id: 'components.DashboardItemEnrollment.runPeriod',
46
- description: 'Text to display the period of a course run',
47
- defaultMessage: 'From {startDate} to {endDate}',
48
- },
49
- enrolledRunPeriod: {
50
- id: 'components.DashboardItemEnrollment.enrolledRunPeriod',
51
- description: 'Text to display the period of a course run',
52
- defaultMessage: 'You are enrolled for the session from {startDate} to {endDate}',
53
- },
54
45
  enrolled: {
55
46
  id: 'components.DashboardItemEnrollment.enrolled',
56
47
  description: 'Text shown when user is enrolled in a course run',
@@ -152,7 +143,7 @@ const DashboardItemCourseEnrollingRuns = ({
152
143
  const { enroll, isLoading, error } = useEnroll(enrollments, order);
153
144
 
154
145
  // Hide runs with finished enrollment.
155
- const datas = useMemo(() => {
146
+ const courseRunOpenForEnrollmentList = useMemo(() => {
156
147
  const activeEnrollment = CoursesHelper.findActiveEnrollment(course, enrollments);
157
148
  return course.course_runs
158
149
  .map((courseRun) => ({
@@ -160,6 +151,9 @@ const DashboardItemCourseEnrollingRuns = ({
160
151
  selected: activeEnrollment?.course_run.id === courseRun.id,
161
152
  }))
162
153
  .filter(
154
+ // FIXME(rlecellier): question!
155
+ // does that mean the we hide the enrollment when user cannot enroll?
156
+ // even if he's already enrolled ?
163
157
  (data) => data.selected || data.courseRun.state.priority <= Priority.FUTURE_NOT_YET_OPEN,
164
158
  );
165
159
  }, [course, enrollments]);
@@ -168,13 +162,13 @@ const DashboardItemCourseEnrollingRuns = ({
168
162
  return (
169
163
  <div className="dashboard-item__course-enrolling__runs">
170
164
  {error && <Banner message={error} type={BannerType.ERROR} />}
171
- {datas.length === 0 && (
165
+ {courseRunOpenForEnrollmentList.length === 0 && (
172
166
  <div className="dashboard-item__course-enrolling__no-runs">
173
167
  <Icon name={IconTypeEnum.WARNING} size="small" />
174
168
  <FormattedMessage {...messages.noCourseRunAvailable} />
175
169
  </div>
176
170
  )}
177
- {datas.map((data) => (
171
+ {courseRunOpenForEnrollmentList.map((data) => (
178
172
  <DashboardItemCourseEnrollingRun
179
173
  key={data.courseRun.id}
180
174
  courseRun={data.courseRun}
@@ -217,6 +211,7 @@ export const DashboardItemCourseEnrollingRun = ({
217
211
  }: DashboardItemCourseEnrollingRunProps) => {
218
212
  const intl = useIntl();
219
213
  const formatDate = useDateFormat();
214
+ const courseRunPeriodMessage = useCourseRunPeriodMessage(courseRun, selected);
220
215
  const haveToSignContract = order ? orderNeedsSignature(order, product) : false;
221
216
  const isOpenedForEnrollment = useMemo(
222
217
  () => courseRun.state.priority < Priority.FUTURE_NOT_YET_OPEN,
@@ -239,16 +234,11 @@ export const DashboardItemCourseEnrollingRun = ({
239
234
  )}
240
235
  <strong>{courseRun.title}</strong>
241
236
  </p>
242
- <FormattedMessage
243
- {...(selected ? messages.enrolledRunPeriod : messages.runPeriod)}
244
- values={{
245
- startDate: formatDate(courseRun.start, DEFAULT_DATE_FORMAT),
246
- endDate: formatDate(courseRun.end, DEFAULT_DATE_FORMAT),
247
- }}
248
- />
237
+ {courseRunPeriodMessage}
249
238
  </div>
250
239
  {courseRun.state.priority === Priority.FUTURE_NOT_YET_OPEN && (
251
240
  <div className="dashboard-item__course-enrolling__run__not-opened">
241
+ -{' '}
252
242
  <FormattedMessage
253
243
  {...messages.enrollmentNotYetOpened}
254
244
  values={{ enrollment_start: formatDate(courseRun.enrollment_start) }}
@@ -256,18 +246,22 @@ export const DashboardItemCourseEnrollingRun = ({
256
246
  </div>
257
247
  )}
258
248
  </div>
259
- <div>
260
- {selected ? (
261
- <Button
262
- color="secondary"
263
- size="small"
264
- href={courseRun.resource_link}
265
- data-testid="dashboard-item-enrollment__button"
266
- className="dashboard-item__button"
267
- >
268
- <FormattedMessage {...messages.accessCourse} />
269
- </Button>
270
- ) : (
249
+ {selected ? (
250
+ SHOW_ACCESS_COURSE_PRIORITIES.includes(courseRun.state.priority) && (
251
+ <div>
252
+ <Button
253
+ color="secondary"
254
+ size="small"
255
+ href={courseRun.resource_link}
256
+ data-testid="dashboard-item-enrollment__button"
257
+ className="dashboard-item__button"
258
+ >
259
+ <FormattedMessage {...messages.accessCourse} />
260
+ </Button>
261
+ </div>
262
+ )
263
+ ) : (
264
+ <div>
271
265
  <Button
272
266
  disabled={!isOpenedForEnrollment || haveToSignContract}
273
267
  color="tertiary"
@@ -277,8 +271,8 @@ export const DashboardItemCourseEnrollingRun = ({
277
271
  >
278
272
  <FormattedMessage {...messages.enrollRun} />
279
273
  </Button>
280
- )}
281
- </div>
274
+ </div>
275
+ )}
282
276
  </div>
283
277
  );
284
278
  };
@@ -313,7 +307,7 @@ const NotEnrolled = ({
313
307
  );
314
308
  };
315
309
 
316
- const SHOW_ACCESS_COURSE_PRIORITIES = [
310
+ export const SHOW_ACCESS_COURSE_PRIORITIES = [
317
311
  Priority.ONGOING_OPEN,
318
312
  Priority.ARCHIVED_OPEN,
319
313
  Priority.ONGOING_CLOSED,
@@ -327,11 +321,16 @@ export const Enrolled = ({
327
321
  icon?: boolean;
328
322
  enrollment: Enrollment;
329
323
  }) => {
324
+ const courseRunPeriodMessage = useCourseRunPeriodMessage(enrollment.course_run, true);
330
325
  return (
331
326
  <>
332
327
  <div className="dashboard-item__block__status">
333
328
  {icon && <Icon name={IconTypeEnum.SCHOOL} />}
334
- <EnrolledStatus enrollment={enrollment} />
329
+ {enrollment.is_active ? (
330
+ courseRunPeriodMessage
331
+ ) : (
332
+ <FormattedMessage {...messages.statusNotActive} />
333
+ )}
335
334
  </div>
336
335
  {SHOW_ACCESS_COURSE_PRIORITIES.includes(enrollment.course_run.state.priority) && (
337
336
  <Button
@@ -347,21 +346,3 @@ export const Enrolled = ({
347
346
  </>
348
347
  );
349
348
  };
350
-
351
- const EnrolledStatus = ({ enrollment }: { enrollment: Enrollment }) => {
352
- const formatDate = useDateFormat();
353
-
354
- if (!enrollment.is_active) {
355
- return <FormattedMessage {...messages.statusNotActive} />;
356
- }
357
-
358
- return (
359
- <FormattedMessage
360
- {...messages.enrolledRunPeriod}
361
- values={{
362
- startDate: formatDate(enrollment.course_run.start, DEFAULT_DATE_FORMAT),
363
- endDate: formatDate(enrollment.course_run.end, DEFAULT_DATE_FORMAT),
364
- }}
365
- />
366
- );
367
- };
@@ -51,16 +51,13 @@ describe('<DashboardItemEnrollment/>', () => {
51
51
  expect(link).toBeEnabled();
52
52
  expect(link).toHaveAttribute('href', enrollment.course_run.resource_link);
53
53
 
54
- screen.getByText(
55
- 'You are enrolled for the session from ' +
56
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
57
- new Date(enrollment.course_run.start),
58
- ) +
59
- ' to ' +
60
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
61
- new Date(enrollment.course_run.end),
62
- ),
54
+ const fromDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
55
+ new Date(enrollment.course_run.start),
63
56
  );
57
+ const toDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
58
+ new Date(enrollment.course_run.end),
59
+ );
60
+ screen.getByText(`You are enrolled for this session. It's open from ${fromDate} to ${toDate}`);
64
61
  });
65
62
 
66
63
  it('renders a closed enrollment', () => {
@@ -80,16 +77,7 @@ describe('<DashboardItemEnrollment/>', () => {
80
77
  const link = screen.getByRole('link', { name: 'Access to course' });
81
78
  expect(link).toBeEnabled();
82
79
  expect(link).toHaveAttribute('href', enrollment.course_run.resource_link);
83
- screen.getByText(
84
- 'You are enrolled for the session from ' +
85
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
86
- new Date(enrollment.course_run.start),
87
- ) +
88
- ' to ' +
89
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
90
- new Date(enrollment.course_run.end),
91
- ),
92
- );
80
+ expect(screen.getByText(/You are enrolled for this session./)).toBeInTheDocument();
93
81
  });
94
82
 
95
83
  it('renders an inactive enrollment', () => {
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import { Enrollment, isCertificateProduct } from 'types/Joanie';
3
- import { Enrolled } from '../DashboardItemCourseEnrolling';
3
+ import { Enrolled } from '../CourseEnrolling';
4
4
  import { DashboardItem } from '..';
5
5
  import ProductCertificateFooter from './ProductCertificateFooter';
6
6
 
@@ -16,7 +16,10 @@ import { PropsWithChildren } from 'react';
16
16
  import fetchMock from 'fetch-mock';
17
17
  import { createMemoryRouter, RouterProvider } from 'react-router-dom';
18
18
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
19
- import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
19
+ import {
20
+ CourseStateFactory,
21
+ RichieContextFactory as mockRichieContextFactory,
22
+ } from 'utils/test/factories/richie';
20
23
  import {
21
24
  CertificateFactory,
22
25
  CourseLightFactory,
@@ -213,18 +216,19 @@ describe('<DashboardItemOrder/>', () => {
213
216
  await screen.findByRole('heading', { level: 5, name: product.title });
214
217
  await screen.findByText('Ref. ' + (order.course as CourseLight).code);
215
218
  await screen.findByText('On going');
219
+ const fromDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
220
+ new Date(order.target_enrollments[0].course_run.start),
221
+ );
222
+ const toDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
223
+ new Date(order.target_enrollments[0].course_run.end),
224
+ );
216
225
  await resolveAll(order.target_courses, async (course) => {
217
226
  await screen.findByRole('heading', { level: 6, name: course.title });
218
- screen.getByText(
219
- 'You are enrolled for the session from ' +
220
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
221
- new Date(order.target_enrollments[0].course_run.start),
222
- ) +
223
- ' to ' +
224
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
225
- new Date(order.target_enrollments[0].course_run.end),
226
- ),
227
- );
227
+ expect(
228
+ screen.getByText(
229
+ `You are enrolled for this session. It's open from ${fromDate} to ${toDate}`,
230
+ ),
231
+ ).toBeInTheDocument();
228
232
  screen.getByRole('link', { name: 'Access to course' });
229
233
  });
230
234
  });
@@ -269,7 +273,15 @@ describe('<DashboardItemOrder/>', () => {
269
273
 
270
274
  it('renders a writable order with enrolled target course', async () => {
271
275
  const order: CredentialOrder = CredentialOrderFactory({
272
- target_courses: TargetCourseFactory().many(1),
276
+ target_courses: [
277
+ TargetCourseFactory({
278
+ course_runs: [
279
+ CourseRunFactory({
280
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
281
+ }).one(),
282
+ ],
283
+ }).one(),
284
+ ],
273
285
  }).one();
274
286
  // Make target course enrolled.
275
287
  order.target_enrollments = [
@@ -297,13 +309,17 @@ describe('<DashboardItemOrder/>', () => {
297
309
  // Expect the first courseRun to be enrolled but not the others.
298
310
  if (i === 0) {
299
311
  expect(queryByRole(runElement, 'button', { name: 'Enroll' })).toBeNull();
300
- getByText(
301
- runElement,
302
- 'You are enrolled for the session from ' +
303
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(new Date(courseRun.start)) +
304
- ' to ' +
305
- new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(new Date(courseRun.end)),
312
+ const fromDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
313
+ new Date(courseRun.start),
306
314
  );
315
+ const toDate = new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
316
+ new Date(courseRun.end),
317
+ );
318
+ expect(
319
+ screen.getByText(
320
+ `You are enrolled for this session. It's open from ${fromDate} to ${toDate}`,
321
+ ),
322
+ ).toBeInTheDocument();
307
323
  const button = getByRole(runElement, 'link', { name: 'Access to course' });
308
324
  expect(button).toHaveAttribute('href', courseRun.resource_link);
309
325
  } else {
@@ -323,7 +339,15 @@ describe('<DashboardItemOrder/>', () => {
323
339
  it('renders a writable order with not enrolled target course and enrolls it', async () => {
324
340
  // Initial order without enrollment.
325
341
  const order: CredentialOrder = CredentialOrderFactory({
326
- target_courses: TargetCourseFactory().many(1),
342
+ target_courses: [
343
+ TargetCourseFactory({
344
+ course_runs: [
345
+ CourseRunFactory({
346
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
347
+ }).one(),
348
+ ],
349
+ }).one(),
350
+ ],
327
351
  target_enrollments: [],
328
352
  }).one();
329
353
  const { product } = mockCourseProductWithOrder(order);
@@ -451,7 +475,16 @@ describe('<DashboardItemOrder/>', () => {
451
475
  it('renders a writable order with enrolled target course and changes the enrollment', async () => {
452
476
  // Initial order with first course run enrolled.
453
477
  const order: CredentialOrder = CredentialOrderFactory({
454
- target_courses: TargetCourseFactory().many(1),
478
+ target_courses: [
479
+ TargetCourseFactory({
480
+ course_runs: [
481
+ CourseRunFactory().one(),
482
+ CourseRunFactory({
483
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
484
+ }).one(),
485
+ ],
486
+ }).one(),
487
+ ],
455
488
  }).one();
456
489
  const initialEnrolledCourseRun = order.target_courses[0].course_runs[0];
457
490
  order.target_enrollments = EnrollmentFactory({ course_run: initialEnrolledCourseRun }).many(1);
@@ -550,7 +583,16 @@ describe('<DashboardItemOrder/>', () => {
550
583
  it('renders a writable order with enrolled target course and refuse the confirm message when enrolling', async () => {
551
584
  // Initial order without enrollment.
552
585
  const order: CredentialOrder = CredentialOrderFactory({
553
- target_courses: TargetCourseFactory().many(1),
586
+ target_courses: [
587
+ TargetCourseFactory({
588
+ course_runs: [
589
+ CourseRunFactory({
590
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
591
+ }).one(),
592
+ CourseRunFactory().one(),
593
+ ],
594
+ }).one(),
595
+ ],
554
596
  }).one();
555
597
 
556
598
  const initialEnrolledCourseRun = order.target_courses[0].course_runs[0];
@@ -620,11 +662,20 @@ describe('<DashboardItemOrder/>', () => {
620
662
  it('renders a writable order with non-enrolled (is_active=false) target course and changes the enrollment', async () => {
621
663
  // Initial order with first course run enrolled.
622
664
  const order: CredentialOrder = CredentialOrderFactory({
623
- target_courses: TargetCourseFactory().many(1),
665
+ target_courses: TargetCourseFactory({
666
+ course_runs: [
667
+ CourseRunFactory({
668
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
669
+ }).one(),
670
+ ],
671
+ }).many(1),
624
672
  }).one();
625
673
 
626
674
  const courseRun = order.target_courses[0].course_runs[0];
627
- const enrollment = EnrollmentFactory({ course_run: courseRun, is_active: false }).one();
675
+ const enrollment = EnrollmentFactory({
676
+ course_run: courseRun,
677
+ is_active: false,
678
+ }).one();
628
679
  order.target_enrollments = [enrollment];
629
680
 
630
681
  // When the existing enrollment will be set as is_active: true.
@@ -725,6 +776,7 @@ describe('<DashboardItemOrder/>', () => {
725
776
  new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
726
777
  new Date(order.target_courses[0].course_runs[0].enrollment_start),
727
778
  ),
779
+ { exact: false },
728
780
  );
729
781
 
730
782
  // Enroll button should be disabled.
@@ -759,7 +811,7 @@ describe('<DashboardItemOrder/>', () => {
759
811
 
760
812
  // The course run should be shown as enrolled even if is it past.
761
813
  const runElement = screen.getByTestId('dashboard-item__course-enrolling__run__' + courseRun.id);
762
- getByRole(runElement, 'link', { name: 'Access to course' });
814
+ expect(screen.queryByRole('link', { name: 'Access to course' })).not.toBeInTheDocument();
763
815
  expect(queryByRole(runElement, 'button', { name: 'Enroll' })).toBeNull();
764
816
  });
765
817
 
@@ -13,7 +13,7 @@ import { useCourseProduct } from 'hooks/useCourseProducts';
13
13
  import { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
14
14
 
15
15
  import { DashboardSubItemsList } from '../DashboardSubItemsList';
16
- import { DashboardItemCourseEnrolling } from '../DashboardItemCourseEnrolling';
16
+ import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
17
17
  import { DashboardItem } from '../index';
18
18
  import { DashboardItemContract } from '../Contract';
19
19
  import OrderStateMessage from './OrderStateMessage';
@@ -91,7 +91,14 @@ describe('CourseProductCertificateItem', () => {
91
91
  // When user clicks on "Download" button, the certificate should be downloaded
92
92
  fetchMock.get(
93
93
  `https://joanie.test/api/v1.0/certificates/${order.certificate_id}/download/`,
94
- HttpStatusCode.OK,
94
+ () => ({
95
+ status: HttpStatusCode.OK,
96
+ body: new Blob(['test']),
97
+ headers: {
98
+ 'Content-Disposition': 'attachment; filename="test.pdf";',
99
+ 'Content-Type': 'application/pdf',
100
+ },
101
+ }),
95
102
  );
96
103
 
97
104
  fireEvent.click($button);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev68",
3
+ "version": "2.25.0-b2.dev71",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {