richie-education 3.1.3-dev8 → 3.2.1-dev1
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/.storybook/__mocks__/utils/context.ts +4 -0
- package/i18n/locales/ar-SA.json +30 -10
- package/i18n/locales/es-ES.json +30 -10
- package/i18n/locales/fa-IR.json +30 -10
- package/i18n/locales/fr-CA.json +31 -11
- package/i18n/locales/fr-FR.json +32 -12
- package/i18n/locales/ko-KR.json +30 -10
- package/i18n/locales/pt-PT.json +30 -10
- package/i18n/locales/ru-RU.json +30 -10
- package/i18n/locales/vi-VN.json +30 -10
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +18 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +4 -0
- package/js/components/CourseGlimpse/utils.ts +35 -30
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -3
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +131 -64
- package/js/components/SaleTunnel/index.stories.tsx +17 -2
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/settings/index.ts +1 -0
- package/js/settings/settings.prod.ts +1 -0
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Course.ts +4 -0
- package/js/types/Joanie.ts +31 -22
- package/js/types/index.ts +6 -2
- package/js/utils/test/factories/joanie.ts +18 -11
- package/js/utils/test/factories/richie.ts +10 -2
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/Slider/components/SlidePanel.tsx +9 -0
- package/js/widgets/Slider/index.stories.tsx +53 -0
- package/js/widgets/Slider/index.tsx +21 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +8 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +116 -75
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +29 -30
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +36 -2
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +36 -2
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +58 -8
- package/package.json +2 -1
- package/scss/colors/_theme.scss +3 -0
- package/scss/components/templates/richie/slider/_slider.scss +19 -0
- package/scss/objects/_blogpost_glimpses.scss +5 -0
- package/scss/objects/_course_glimpses.scss +16 -0
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
|
2
2
|
import PurchaseButton from 'components/PurchaseButton';
|
|
3
|
-
import {
|
|
3
|
+
import { Offering, CredentialProduct } from 'types/Joanie';
|
|
4
4
|
import { PacedCourse } from 'types';
|
|
5
5
|
|
|
6
6
|
const messages = defineMessages({
|
|
@@ -24,20 +24,16 @@ other {# remaining seats}
|
|
|
24
24
|
|
|
25
25
|
interface CourseProductItemFooterProps {
|
|
26
26
|
course: PacedCourse;
|
|
27
|
-
|
|
27
|
+
offering: Offering;
|
|
28
28
|
canPurchase: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const CourseProductItemFooter = ({
|
|
32
32
|
course,
|
|
33
|
-
|
|
33
|
+
offering,
|
|
34
34
|
canPurchase,
|
|
35
35
|
}: CourseProductItemFooterProps) => {
|
|
36
|
-
|
|
37
|
-
const { seats, nb_seats_available } = courseProductRelation;
|
|
38
|
-
const hasSeatsLimit = seats && nb_seats_available !== undefined;
|
|
39
|
-
const hasNoSeatsAvailable = hasSeatsLimit && nb_seats_available === 0;
|
|
40
|
-
if (hasNoSeatsAvailable)
|
|
36
|
+
if (!offering?.rules?.has_seats_left)
|
|
41
37
|
return (
|
|
42
38
|
<p className="product-widget__footer__message">
|
|
43
39
|
<FormattedMessage {...messages.noSeatsAvailable} />
|
|
@@ -47,18 +43,18 @@ const CourseProductItemFooter = ({
|
|
|
47
43
|
<div className="product-widget__footer__order-group">
|
|
48
44
|
<PurchaseButton
|
|
49
45
|
course={course}
|
|
50
|
-
product={
|
|
51
|
-
|
|
52
|
-
organizations={
|
|
53
|
-
isWithdrawable={
|
|
46
|
+
product={offering.product as CredentialProduct}
|
|
47
|
+
offering={offering}
|
|
48
|
+
organizations={offering.organizations}
|
|
49
|
+
isWithdrawable={offering.is_withdrawable}
|
|
54
50
|
disabled={!canPurchase}
|
|
55
51
|
buttonProps={{ fullWidth: true }}
|
|
56
52
|
/>
|
|
57
|
-
{
|
|
53
|
+
{offering?.rules?.has_seat_limit && (
|
|
58
54
|
<p className="product-widget__footer__message">
|
|
59
55
|
<FormattedMessage
|
|
60
56
|
{...messages.nbSeatsAvailable}
|
|
61
|
-
values={{ nb:
|
|
57
|
+
values={{ nb: offering.rules.nb_available_seats }}
|
|
62
58
|
/>
|
|
63
59
|
</p>
|
|
64
60
|
)}
|
|
@@ -67,6 +67,12 @@
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
& .product-widget__header-main {
|
|
71
|
+
display: flex;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
text-align: center;
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
& .product-widget__title {
|
|
71
77
|
color: r-theme-val(product-item, light-color);
|
|
72
78
|
font-size: 1.5rem;
|
|
@@ -76,7 +82,7 @@
|
|
|
76
82
|
background-color: r-theme-val(product-item, light-color);
|
|
77
83
|
border-radius: 100vw;
|
|
78
84
|
color: r-theme-val(product-item, base-border);
|
|
79
|
-
font-size:
|
|
85
|
+
font-size: 1rem;
|
|
80
86
|
margin-bottom: 0.3rem;
|
|
81
87
|
padding: 0.375rem 0.81rem;
|
|
82
88
|
white-space: nowrap;
|
|
@@ -90,6 +96,7 @@
|
|
|
90
96
|
|
|
91
97
|
&-discount {
|
|
92
98
|
color: r-theme-val(product-item, feedback-color);
|
|
99
|
+
text-decoration: none;
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
|
|
@@ -68,7 +68,7 @@ const CourseRunList = ({ courseRuns }: Props) => {
|
|
|
68
68
|
</strong>
|
|
69
69
|
<span
|
|
70
70
|
data-testid={`course-run-${courseRun.id}-enrollment-dates`}
|
|
71
|
-
className="course-runs-item__metadata"
|
|
71
|
+
className="course-runs-item__metadata course-runs-item__enrollment-date"
|
|
72
72
|
>
|
|
73
73
|
<EnrollmentDate
|
|
74
74
|
enrollment_start={courseRun.enrollment_start}
|
|
@@ -77,7 +77,7 @@ const CourseRunList = ({ courseRuns }: Props) => {
|
|
|
77
77
|
</span>
|
|
78
78
|
<span
|
|
79
79
|
data-testid={`course-run-${courseRun.id}-languages`}
|
|
80
|
-
className="course-runs-item__metadata"
|
|
80
|
+
className="course-runs-item__metadata course-runs-item__languages"
|
|
81
81
|
>
|
|
82
82
|
<FormattedMessage
|
|
83
83
|
{...sharedMessages.language}
|
|
@@ -113,6 +113,15 @@
|
|
|
113
113
|
line-height: 1.4em;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
&__enrollment-date {
|
|
117
|
+
font-weight: bold;
|
|
118
|
+
color: #03317f;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&__languages {
|
|
122
|
+
font-weight: bold;
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
&__feedback {
|
|
117
126
|
color: r-theme-val(product-item, feedback-color);
|
|
118
127
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
PacedCourseFactory,
|
|
7
7
|
} from 'utils/test/factories/richie';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
OfferingFactory,
|
|
10
10
|
EnrollmentFactory,
|
|
11
11
|
CredentialOrderFactory,
|
|
12
12
|
ProductFactory,
|
|
@@ -74,8 +74,8 @@ describe('CourseProductItem', () => {
|
|
|
74
74
|
}).format(price);
|
|
75
75
|
|
|
76
76
|
it('should display a loader until product is loaded', async () => {
|
|
77
|
-
const
|
|
78
|
-
const { product } =
|
|
77
|
+
const offering = OfferingFactory().one();
|
|
78
|
+
const { product } = offering;
|
|
79
79
|
const productDeferred = new Deferred();
|
|
80
80
|
fetchMock.get(
|
|
81
81
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -92,16 +92,16 @@ describe('CourseProductItem', () => {
|
|
|
92
92
|
|
|
93
93
|
// - A loader should be displayed while product information are fetching
|
|
94
94
|
await expectSpinner('Loading product information...');
|
|
95
|
-
productDeferred.resolve(
|
|
95
|
+
productDeferred.resolve(offering);
|
|
96
96
|
await expectNoSpinner('Loading product information...');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it('renders product information for anonymous user', async () => {
|
|
100
|
-
const
|
|
101
|
-
const { product } =
|
|
100
|
+
const offering = OfferingFactory().one();
|
|
101
|
+
const { product } = offering;
|
|
102
102
|
fetchMock.get(
|
|
103
103
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
104
|
-
|
|
104
|
+
offering,
|
|
105
105
|
);
|
|
106
106
|
|
|
107
107
|
render(
|
|
@@ -131,7 +131,7 @@ describe('CourseProductItem', () => {
|
|
|
131
131
|
).not.toBeInTheDocument();
|
|
132
132
|
|
|
133
133
|
// - Render all target courses information
|
|
134
|
-
|
|
134
|
+
offering.product.target_courses.forEach((course) => {
|
|
135
135
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
136
136
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
137
137
|
// but we want to it to visually look like a h5
|
|
@@ -151,21 +151,23 @@ describe('CourseProductItem', () => {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
it('renders discount rate for anonymous user', async () => {
|
|
154
|
-
const
|
|
154
|
+
const offering = OfferingFactory({
|
|
155
155
|
product: CredentialProductFactory({
|
|
156
156
|
price: 840,
|
|
157
157
|
price_currency: 'EUR',
|
|
158
158
|
}).one(),
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
rules: {
|
|
160
|
+
discounted_price: 800,
|
|
161
|
+
discount_rate: 0.3,
|
|
162
|
+
description: 'Year 2023 discount',
|
|
163
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
164
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
165
|
+
},
|
|
164
166
|
}).one();
|
|
165
|
-
const { product } =
|
|
167
|
+
const { product } = offering;
|
|
166
168
|
fetchMock.get(
|
|
167
169
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
168
|
-
|
|
170
|
+
offering,
|
|
169
171
|
);
|
|
170
172
|
|
|
171
173
|
render(
|
|
@@ -192,7 +194,7 @@ describe('CourseProductItem', () => {
|
|
|
192
194
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
193
195
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
194
196
|
const discountedPrice = screen.getByText(
|
|
195
|
-
priceFormatter(product.price_currency,
|
|
197
|
+
priceFormatter(product.price_currency, offering.rules!.discounted_price!).replace(
|
|
196
198
|
/(\u202F|\u00a0)/g,
|
|
197
199
|
' ',
|
|
198
200
|
),
|
|
@@ -212,21 +214,23 @@ describe('CourseProductItem', () => {
|
|
|
212
214
|
});
|
|
213
215
|
|
|
214
216
|
it('renders discount amount for anonymous user', async () => {
|
|
215
|
-
const
|
|
217
|
+
const offering = OfferingFactory({
|
|
216
218
|
product: CredentialProductFactory({
|
|
217
219
|
price: 840,
|
|
218
220
|
price_currency: 'EUR',
|
|
219
221
|
}).one(),
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
rules: {
|
|
223
|
+
discounted_price: 800,
|
|
224
|
+
discount_amount: 40,
|
|
225
|
+
description: 'Year 2023 discount',
|
|
226
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
227
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
228
|
+
},
|
|
225
229
|
}).one();
|
|
226
|
-
const { product } =
|
|
230
|
+
const { product } = offering;
|
|
227
231
|
fetchMock.get(
|
|
228
232
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
229
|
-
|
|
233
|
+
offering,
|
|
230
234
|
);
|
|
231
235
|
|
|
232
236
|
render(
|
|
@@ -253,7 +257,7 @@ describe('CourseProductItem', () => {
|
|
|
253
257
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
254
258
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
255
259
|
const discountedPrice = screen.getByText(
|
|
256
|
-
priceFormatter(product.price_currency,
|
|
260
|
+
priceFormatter(product.price_currency, offering.rules!.discounted_price!).replace(
|
|
257
261
|
/(\u202F|\u00a0)/g,
|
|
258
262
|
' ',
|
|
259
263
|
),
|
|
@@ -273,15 +277,15 @@ describe('CourseProductItem', () => {
|
|
|
273
277
|
});
|
|
274
278
|
|
|
275
279
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
276
|
-
const
|
|
280
|
+
const offering = OfferingFactory({
|
|
277
281
|
product: ProductFactory({
|
|
278
282
|
certificate_definition: undefined,
|
|
279
283
|
}).one(),
|
|
280
284
|
}).one();
|
|
281
|
-
const { product } =
|
|
285
|
+
const { product } = offering;
|
|
282
286
|
fetchMock.get(
|
|
283
287
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
284
|
-
|
|
288
|
+
offering,
|
|
285
289
|
);
|
|
286
290
|
|
|
287
291
|
render(
|
|
@@ -300,16 +304,16 @@ describe('CourseProductItem', () => {
|
|
|
300
304
|
});
|
|
301
305
|
|
|
302
306
|
it('renders product information in compact mode', async () => {
|
|
303
|
-
const
|
|
307
|
+
const offering = OfferingFactory().one();
|
|
304
308
|
fetchMock.get(
|
|
305
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
306
|
-
|
|
309
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
310
|
+
offering,
|
|
307
311
|
);
|
|
308
312
|
|
|
309
313
|
const { container } = render(
|
|
310
314
|
<CourseProductItem
|
|
311
315
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
312
|
-
productId={
|
|
316
|
+
productId={offering.product.id}
|
|
313
317
|
compact
|
|
314
318
|
/>,
|
|
315
319
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -317,14 +321,14 @@ describe('CourseProductItem', () => {
|
|
|
317
321
|
|
|
318
322
|
// In the header, we should display the product title, the product price
|
|
319
323
|
// and product date range and languages
|
|
320
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
324
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
321
325
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
322
326
|
// but we want to it to visually look like a h6
|
|
323
327
|
|
|
324
328
|
const $price = screen.getByText(
|
|
325
329
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
326
330
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
327
|
-
priceFormatter(
|
|
331
|
+
priceFormatter(offering.product.price_currency, offering.product.price).replace(
|
|
328
332
|
/(\u202F|\u00a0)/g,
|
|
329
333
|
' ',
|
|
330
334
|
),
|
|
@@ -340,7 +344,7 @@ describe('CourseProductItem', () => {
|
|
|
340
344
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
341
345
|
|
|
342
346
|
// - Any target courses information should be displayed
|
|
343
|
-
|
|
347
|
+
offering.product.target_courses.forEach((course) => {
|
|
344
348
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
345
349
|
expect($item).not.toBeInTheDocument();
|
|
346
350
|
});
|
|
@@ -349,7 +353,7 @@ describe('CourseProductItem', () => {
|
|
|
349
353
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
350
354
|
|
|
351
355
|
// - Render a login button
|
|
352
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
356
|
+
screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
|
|
353
357
|
// - Does not render PurchaseButton cta
|
|
354
358
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
355
359
|
});
|
|
@@ -357,8 +361,8 @@ describe('CourseProductItem', () => {
|
|
|
357
361
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
358
362
|
'renders product informations for %s order',
|
|
359
363
|
async (state) => {
|
|
360
|
-
const
|
|
361
|
-
const { product } =
|
|
364
|
+
const offering = OfferingFactory().one();
|
|
365
|
+
const { product } = offering;
|
|
362
366
|
const order = CredentialOrderFactory({
|
|
363
367
|
product_id: product.id,
|
|
364
368
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -368,7 +372,7 @@ describe('CourseProductItem', () => {
|
|
|
368
372
|
|
|
369
373
|
fetchMock.get(
|
|
370
374
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
371
|
-
|
|
375
|
+
offering,
|
|
372
376
|
);
|
|
373
377
|
const orderQueryParameters = {
|
|
374
378
|
course_code: order.course.code,
|
|
@@ -382,13 +386,13 @@ describe('CourseProductItem', () => {
|
|
|
382
386
|
render(
|
|
383
387
|
<CourseProductItem
|
|
384
388
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
385
|
-
productId={
|
|
389
|
+
productId={offering.product.id}
|
|
386
390
|
/>,
|
|
387
391
|
);
|
|
388
392
|
|
|
389
393
|
// In the header, we should display the product title, the product price
|
|
390
394
|
// and product date range and languages
|
|
391
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
395
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
392
396
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
393
397
|
// but we want to it to visually look like a h6
|
|
394
398
|
|
|
@@ -418,8 +422,8 @@ describe('CourseProductItem', () => {
|
|
|
418
422
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
419
423
|
'renders product informations for %s order in compact mode',
|
|
420
424
|
async (state) => {
|
|
421
|
-
const
|
|
422
|
-
const { product } =
|
|
425
|
+
const offering = OfferingFactory().one();
|
|
426
|
+
const { product } = offering;
|
|
423
427
|
const order = CredentialOrderFactory({
|
|
424
428
|
product_id: product.id,
|
|
425
429
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -429,7 +433,7 @@ describe('CourseProductItem', () => {
|
|
|
429
433
|
|
|
430
434
|
fetchMock.get(
|
|
431
435
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
432
|
-
|
|
436
|
+
offering,
|
|
433
437
|
);
|
|
434
438
|
const orderQueryParameters = {
|
|
435
439
|
course_code: order.course.code,
|
|
@@ -443,14 +447,14 @@ describe('CourseProductItem', () => {
|
|
|
443
447
|
render(
|
|
444
448
|
<CourseProductItem
|
|
445
449
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
446
|
-
productId={
|
|
450
|
+
productId={offering.product.id}
|
|
447
451
|
compact
|
|
448
452
|
/>,
|
|
449
453
|
);
|
|
450
454
|
|
|
451
455
|
// In the header, we should display the product title, the product price
|
|
452
456
|
// and product date range and languages
|
|
453
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
457
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
454
458
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
455
459
|
// but we want to it to visually look like a h6
|
|
456
460
|
|
|
@@ -460,7 +464,7 @@ describe('CourseProductItem', () => {
|
|
|
460
464
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
461
465
|
|
|
462
466
|
// - Any target courses information should be displayed
|
|
463
|
-
|
|
467
|
+
offering.product.target_courses.forEach((course) => {
|
|
464
468
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
465
469
|
expect($item).not.toBeInTheDocument();
|
|
466
470
|
});
|
|
@@ -471,8 +475,8 @@ describe('CourseProductItem', () => {
|
|
|
471
475
|
);
|
|
472
476
|
|
|
473
477
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
474
|
-
const
|
|
475
|
-
const { product } =
|
|
478
|
+
const offering = OfferingFactory().one();
|
|
479
|
+
const { product } = offering;
|
|
476
480
|
const order = CredentialOrderFactory({
|
|
477
481
|
product_id: product.id,
|
|
478
482
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -482,7 +486,7 @@ describe('CourseProductItem', () => {
|
|
|
482
486
|
|
|
483
487
|
fetchMock.get(
|
|
484
488
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
485
|
-
|
|
489
|
+
offering,
|
|
486
490
|
);
|
|
487
491
|
const orderQueryParameters = {
|
|
488
492
|
course_code: order.course.code,
|
|
@@ -533,17 +537,17 @@ describe('CourseProductItem', () => {
|
|
|
533
537
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
534
538
|
'renders product informations for a %s order in compact mode',
|
|
535
539
|
async (state) => {
|
|
536
|
-
const
|
|
540
|
+
const offering = OfferingFactory().one();
|
|
537
541
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
538
|
-
product_id:
|
|
542
|
+
product_id: offering.product.id,
|
|
539
543
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
540
|
-
target_courses:
|
|
544
|
+
target_courses: offering.product.target_courses,
|
|
541
545
|
state,
|
|
542
546
|
}).one();
|
|
543
547
|
|
|
544
548
|
fetchMock.get(
|
|
545
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
546
|
-
|
|
549
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
550
|
+
offering,
|
|
547
551
|
);
|
|
548
552
|
const orderQueryParameters = {
|
|
549
553
|
product_id: order.product_id,
|
|
@@ -557,14 +561,14 @@ describe('CourseProductItem', () => {
|
|
|
557
561
|
|
|
558
562
|
render(
|
|
559
563
|
<CourseProductItem
|
|
560
|
-
productId={
|
|
564
|
+
productId={offering.product.id}
|
|
561
565
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
562
566
|
compact
|
|
563
567
|
/>,
|
|
564
568
|
);
|
|
565
569
|
|
|
566
570
|
// Wait for product information to be fetched
|
|
567
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
571
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
568
572
|
|
|
569
573
|
// - In place of product price, a label should be displayed
|
|
570
574
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -601,8 +605,8 @@ describe('CourseProductItem', () => {
|
|
|
601
605
|
);
|
|
602
606
|
|
|
603
607
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
604
|
-
const
|
|
605
|
-
const { product } =
|
|
608
|
+
const offering = OfferingFactory().one();
|
|
609
|
+
const { product } = offering;
|
|
606
610
|
// - Create an order with an active enrollment
|
|
607
611
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
608
612
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -616,7 +620,7 @@ describe('CourseProductItem', () => {
|
|
|
616
620
|
|
|
617
621
|
fetchMock.get(
|
|
618
622
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
619
|
-
|
|
623
|
+
offering,
|
|
620
624
|
);
|
|
621
625
|
const orderQueryParameters = {
|
|
622
626
|
product_id: order.product_id,
|
|
@@ -668,8 +672,8 @@ describe('CourseProductItem', () => {
|
|
|
668
672
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
669
673
|
'renders sale tunnel button if user already has a %s order',
|
|
670
674
|
async (state) => {
|
|
671
|
-
const
|
|
672
|
-
const { product } =
|
|
675
|
+
const offering = OfferingFactory().one();
|
|
676
|
+
const { product } = offering;
|
|
673
677
|
const order = CredentialOrderFactory({
|
|
674
678
|
product_id: product.id,
|
|
675
679
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -678,7 +682,7 @@ describe('CourseProductItem', () => {
|
|
|
678
682
|
}).one();
|
|
679
683
|
fetchMock.get(
|
|
680
684
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
681
|
-
|
|
685
|
+
offering,
|
|
682
686
|
);
|
|
683
687
|
const orderQueryParameters = {
|
|
684
688
|
product_id: order.product_id,
|
|
@@ -709,7 +713,7 @@ describe('CourseProductItem', () => {
|
|
|
709
713
|
expect($price.classList.contains('h6')).toBe(true);
|
|
710
714
|
|
|
711
715
|
// - Render all target courses information
|
|
712
|
-
|
|
716
|
+
offering.product.target_courses.forEach((course) => {
|
|
713
717
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
714
718
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
715
719
|
// but we want to it to visually look like a h5
|
|
@@ -724,11 +728,11 @@ describe('CourseProductItem', () => {
|
|
|
724
728
|
);
|
|
725
729
|
|
|
726
730
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
727
|
-
const
|
|
728
|
-
const { product } =
|
|
731
|
+
const offering = OfferingFactory().one();
|
|
732
|
+
const { product } = offering;
|
|
729
733
|
fetchMock.get(
|
|
730
734
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
731
|
-
|
|
735
|
+
offering,
|
|
732
736
|
);
|
|
733
737
|
const orderQueryParameters = {
|
|
734
738
|
product_id: product.id,
|
|
@@ -759,7 +763,7 @@ describe('CourseProductItem', () => {
|
|
|
759
763
|
expect($price.classList.contains('h6')).toBe(true);
|
|
760
764
|
|
|
761
765
|
// - Render all target courses information
|
|
762
|
-
|
|
766
|
+
offering.product.target_courses.forEach((course) => {
|
|
763
767
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
764
768
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
765
769
|
// but we want to it to visually look like a h5
|
|
@@ -773,7 +777,7 @@ describe('CourseProductItem', () => {
|
|
|
773
777
|
});
|
|
774
778
|
|
|
775
779
|
it('renders error message when product fetching has failed', async () => {
|
|
776
|
-
const { product } =
|
|
780
|
+
const { product } = OfferingFactory().one();
|
|
777
781
|
|
|
778
782
|
fetchMock.get(
|
|
779
783
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -794,11 +798,13 @@ describe('CourseProductItem', () => {
|
|
|
794
798
|
});
|
|
795
799
|
|
|
796
800
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
801
|
+
const offering = OfferingFactory({
|
|
802
|
+
rules: {
|
|
803
|
+
nb_available_seats: 0,
|
|
804
|
+
has_seats_left: false,
|
|
805
|
+
},
|
|
800
806
|
}).one();
|
|
801
|
-
const { product } =
|
|
807
|
+
const { product } = offering;
|
|
802
808
|
const order = CredentialOrderFactory({
|
|
803
809
|
product_id: product.id,
|
|
804
810
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -807,7 +813,7 @@ describe('CourseProductItem', () => {
|
|
|
807
813
|
}).one();
|
|
808
814
|
fetchMock.get(
|
|
809
815
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
810
|
-
|
|
816
|
+
offering,
|
|
811
817
|
);
|
|
812
818
|
const orderQueryParameters = {
|
|
813
819
|
product_id: order.product_id,
|
|
@@ -832,4 +838,39 @@ describe('CourseProductItem', () => {
|
|
|
832
838
|
expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
|
|
833
839
|
screen.getByText('Sorry, no seats available for now');
|
|
834
840
|
});
|
|
841
|
+
|
|
842
|
+
it('renders product information without rules in offering', async () => {
|
|
843
|
+
const offering = OfferingFactory({
|
|
844
|
+
product: CredentialProductFactory({
|
|
845
|
+
price: 840,
|
|
846
|
+
price_currency: 'EUR',
|
|
847
|
+
}).one(),
|
|
848
|
+
rules: undefined,
|
|
849
|
+
}).one();
|
|
850
|
+
const { product } = offering;
|
|
851
|
+
fetchMock.get(
|
|
852
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
853
|
+
offering,
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
render(
|
|
857
|
+
<CourseProductItem
|
|
858
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
859
|
+
productId={product.id}
|
|
860
|
+
/>,
|
|
861
|
+
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
// Wait for product information to be fetched
|
|
865
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
866
|
+
|
|
867
|
+
// Expect to render the component without rules information
|
|
868
|
+
expect(
|
|
869
|
+
screen.getByText(
|
|
870
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
871
|
+
),
|
|
872
|
+
).toBeInTheDocument();
|
|
873
|
+
expect(document.querySelector('.product-widget__price-discounted')).not.toBeInTheDocument();
|
|
874
|
+
expect(screen.getByText('Sorry, no seats available for now')).toBeInTheDocument();
|
|
875
|
+
});
|
|
835
876
|
});
|
|
@@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { StorybookHelper } from 'utils/StorybookHelper';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
OfferingFactory,
|
|
7
7
|
CourseRunFactory,
|
|
8
8
|
CredentialOrderFactory,
|
|
9
9
|
CredentialProductFactory,
|
|
@@ -22,13 +22,15 @@ const render = (args: CourseProductItemProps, options?: Maybe<{ order: Credentia
|
|
|
22
22
|
fetchMock.get(`http://localhost:8071/api/v1.0/addresses/`, [], { overwriteRoutes: true });
|
|
23
23
|
fetchMock.get(
|
|
24
24
|
`http://localhost:8071/api/v1.0/courses/${args.course.code}/products/${args.productId}/`,
|
|
25
|
-
|
|
25
|
+
OfferingFactory({
|
|
26
26
|
product: CredentialProductFactory({
|
|
27
27
|
price: 840,
|
|
28
28
|
price_currency: 'EUR',
|
|
29
29
|
}).one(),
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
rules: {
|
|
31
|
+
discounted_price: 800,
|
|
32
|
+
discount_rate: 0.3,
|
|
33
|
+
},
|
|
32
34
|
}).one(),
|
|
33
35
|
{ overwriteRoutes: true },
|
|
34
36
|
);
|