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.
- package/js/api/joanie.ts +19 -5
- package/js/components/DownloadContractButton/index.spec.tsx +17 -4
- package/js/components/DownloadContractButton/index.tsx +6 -11
- package/js/hooks/useContractArchive/index.download.spec.tsx +8 -1
- package/js/hooks/useContractArchive/index.ts +8 -27
- package/js/hooks/useDownloadCertificate/index.spec.tsx +17 -5
- package/js/hooks/useDownloadCertificate/index.tsx +2 -20
- package/js/utils/download.ts +43 -0
- package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +12 -4
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.spec.tsx → CourseEnrolling/index.spec.tsx} +44 -13
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +38 -57
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +7 -19
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +76 -24
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.spec.tsx +8 -1
- 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
|
|
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))
|
|
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
|
-
})
|
|
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
|
-
})
|
|
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
|
-
|
|
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(
|
|
101
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(expectedFile);
|
|
92
102
|
expect(window.open).toHaveBeenCalledTimes(1);
|
|
93
|
-
expect(window.open).toHaveBeenCalledWith(
|
|
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,
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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 {
|
|
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=$
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 '
|
|
8
|
-
import { DashboardItemCourseEnrolling } from '
|
|
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 '
|
|
20
|
-
import { useEnroll } from '
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
};
|
package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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', () => {
|
package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { Enrollment, isCertificateProduct } from 'types/Joanie';
|
|
3
|
-
import { Enrolled } from '../
|
|
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 {
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
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
|
-
|
|
301
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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 '../
|
|
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
|
-
|
|
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);
|