richie-education 3.1.3-dev11 → 3.1.3-dev12
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 +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -20
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +22 -35
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -10
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +5 -5
- 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/useOffer/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -17
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
- 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 -23
- 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 -6
- 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 -7
- 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 +25 -33
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
- package/js/types/Joanie.ts +17 -19
- package/js/utils/test/factories/joanie.ts +3 -3
- 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 -27
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
- 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/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +9 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +63 -87
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -34
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
PacedCourseFactory,
|
|
7
7
|
} from 'utils/test/factories/richie';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
OfferFactory,
|
|
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 offer = OfferFactory().one();
|
|
78
|
+
const { product } = offer;
|
|
79
79
|
const productDeferred = new Deferred();
|
|
80
80
|
fetchMock.get(
|
|
81
81
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -92,17 +92,14 @@ 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(offer);
|
|
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 } =
|
|
102
|
-
fetchMock.get(
|
|
103
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
104
|
-
relation,
|
|
105
|
-
);
|
|
100
|
+
const offer = OfferFactory().one();
|
|
101
|
+
const { product } = offer;
|
|
102
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
106
103
|
|
|
107
104
|
render(
|
|
108
105
|
<CourseProductItem
|
|
@@ -131,7 +128,7 @@ describe('CourseProductItem', () => {
|
|
|
131
128
|
).not.toBeInTheDocument();
|
|
132
129
|
|
|
133
130
|
// - Render all target courses information
|
|
134
|
-
|
|
131
|
+
offer.product.target_courses.forEach((course) => {
|
|
135
132
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
136
133
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
137
134
|
// but we want to it to visually look like a h5
|
|
@@ -151,7 +148,7 @@ describe('CourseProductItem', () => {
|
|
|
151
148
|
});
|
|
152
149
|
|
|
153
150
|
it('renders discount rate for anonymous user', async () => {
|
|
154
|
-
const
|
|
151
|
+
const offer = OfferFactory({
|
|
155
152
|
product: CredentialProductFactory({
|
|
156
153
|
price: 840,
|
|
157
154
|
price_currency: 'EUR',
|
|
@@ -162,11 +159,8 @@ describe('CourseProductItem', () => {
|
|
|
162
159
|
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
163
160
|
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
164
161
|
}).one();
|
|
165
|
-
const { product } =
|
|
166
|
-
fetchMock.get(
|
|
167
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
168
|
-
relation,
|
|
169
|
-
);
|
|
162
|
+
const { product } = offer;
|
|
163
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
170
164
|
|
|
171
165
|
render(
|
|
172
166
|
<CourseProductItem
|
|
@@ -192,7 +186,7 @@ describe('CourseProductItem', () => {
|
|
|
192
186
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
193
187
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
194
188
|
const discountedPrice = screen.getByText(
|
|
195
|
-
priceFormatter(product.price_currency,
|
|
189
|
+
priceFormatter(product.price_currency, offer.discounted_price!).replace(
|
|
196
190
|
/(\u202F|\u00a0)/g,
|
|
197
191
|
' ',
|
|
198
192
|
),
|
|
@@ -212,7 +206,7 @@ describe('CourseProductItem', () => {
|
|
|
212
206
|
});
|
|
213
207
|
|
|
214
208
|
it('renders discount amount for anonymous user', async () => {
|
|
215
|
-
const
|
|
209
|
+
const offer = OfferFactory({
|
|
216
210
|
product: CredentialProductFactory({
|
|
217
211
|
price: 840,
|
|
218
212
|
price_currency: 'EUR',
|
|
@@ -223,11 +217,8 @@ describe('CourseProductItem', () => {
|
|
|
223
217
|
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
224
218
|
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
225
219
|
}).one();
|
|
226
|
-
const { product } =
|
|
227
|
-
fetchMock.get(
|
|
228
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
229
|
-
relation,
|
|
230
|
-
);
|
|
220
|
+
const { product } = offer;
|
|
221
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
231
222
|
|
|
232
223
|
render(
|
|
233
224
|
<CourseProductItem
|
|
@@ -253,7 +244,7 @@ describe('CourseProductItem', () => {
|
|
|
253
244
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
254
245
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
255
246
|
const discountedPrice = screen.getByText(
|
|
256
|
-
priceFormatter(product.price_currency,
|
|
247
|
+
priceFormatter(product.price_currency, offer.discounted_price!).replace(
|
|
257
248
|
/(\u202F|\u00a0)/g,
|
|
258
249
|
' ',
|
|
259
250
|
),
|
|
@@ -273,16 +264,13 @@ describe('CourseProductItem', () => {
|
|
|
273
264
|
});
|
|
274
265
|
|
|
275
266
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
276
|
-
const
|
|
267
|
+
const offer = OfferFactory({
|
|
277
268
|
product: ProductFactory({
|
|
278
269
|
certificate_definition: undefined,
|
|
279
270
|
}).one(),
|
|
280
271
|
}).one();
|
|
281
|
-
const { product } =
|
|
282
|
-
fetchMock.get(
|
|
283
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
284
|
-
relation,
|
|
285
|
-
);
|
|
272
|
+
const { product } = offer;
|
|
273
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
286
274
|
|
|
287
275
|
render(
|
|
288
276
|
<CourseProductItem
|
|
@@ -300,16 +288,16 @@ describe('CourseProductItem', () => {
|
|
|
300
288
|
});
|
|
301
289
|
|
|
302
290
|
it('renders product information in compact mode', async () => {
|
|
303
|
-
const
|
|
291
|
+
const offer = OfferFactory().one();
|
|
304
292
|
fetchMock.get(
|
|
305
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
306
|
-
|
|
293
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
|
|
294
|
+
offer,
|
|
307
295
|
);
|
|
308
296
|
|
|
309
297
|
const { container } = render(
|
|
310
298
|
<CourseProductItem
|
|
311
299
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
312
|
-
productId={
|
|
300
|
+
productId={offer.product.id}
|
|
313
301
|
compact
|
|
314
302
|
/>,
|
|
315
303
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -317,14 +305,14 @@ describe('CourseProductItem', () => {
|
|
|
317
305
|
|
|
318
306
|
// In the header, we should display the product title, the product price
|
|
319
307
|
// and product date range and languages
|
|
320
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
308
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
321
309
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
322
310
|
// but we want to it to visually look like a h6
|
|
323
311
|
|
|
324
312
|
const $price = screen.getByText(
|
|
325
313
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
326
314
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
327
|
-
priceFormatter(
|
|
315
|
+
priceFormatter(offer.product.price_currency, offer.product.price).replace(
|
|
328
316
|
/(\u202F|\u00a0)/g,
|
|
329
317
|
' ',
|
|
330
318
|
),
|
|
@@ -340,7 +328,7 @@ describe('CourseProductItem', () => {
|
|
|
340
328
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
341
329
|
|
|
342
330
|
// - Any target courses information should be displayed
|
|
343
|
-
|
|
331
|
+
offer.product.target_courses.forEach((course) => {
|
|
344
332
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
345
333
|
expect($item).not.toBeInTheDocument();
|
|
346
334
|
});
|
|
@@ -349,7 +337,7 @@ describe('CourseProductItem', () => {
|
|
|
349
337
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
350
338
|
|
|
351
339
|
// - Render a login button
|
|
352
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
340
|
+
screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
|
|
353
341
|
// - Does not render PurchaseButton cta
|
|
354
342
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
355
343
|
});
|
|
@@ -357,8 +345,8 @@ describe('CourseProductItem', () => {
|
|
|
357
345
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
358
346
|
'renders product informations for %s order',
|
|
359
347
|
async (state) => {
|
|
360
|
-
const
|
|
361
|
-
const { product } =
|
|
348
|
+
const offer = OfferFactory().one();
|
|
349
|
+
const { product } = offer;
|
|
362
350
|
const order = CredentialOrderFactory({
|
|
363
351
|
product_id: product.id,
|
|
364
352
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -368,7 +356,7 @@ describe('CourseProductItem', () => {
|
|
|
368
356
|
|
|
369
357
|
fetchMock.get(
|
|
370
358
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
371
|
-
|
|
359
|
+
offer,
|
|
372
360
|
);
|
|
373
361
|
const orderQueryParameters = {
|
|
374
362
|
course_code: order.course.code,
|
|
@@ -382,13 +370,13 @@ describe('CourseProductItem', () => {
|
|
|
382
370
|
render(
|
|
383
371
|
<CourseProductItem
|
|
384
372
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
385
|
-
productId={
|
|
373
|
+
productId={offer.product.id}
|
|
386
374
|
/>,
|
|
387
375
|
);
|
|
388
376
|
|
|
389
377
|
// In the header, we should display the product title, the product price
|
|
390
378
|
// and product date range and languages
|
|
391
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
379
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
392
380
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
393
381
|
// but we want to it to visually look like a h6
|
|
394
382
|
|
|
@@ -418,8 +406,8 @@ describe('CourseProductItem', () => {
|
|
|
418
406
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
419
407
|
'renders product informations for %s order in compact mode',
|
|
420
408
|
async (state) => {
|
|
421
|
-
const
|
|
422
|
-
const { product } =
|
|
409
|
+
const offer = OfferFactory().one();
|
|
410
|
+
const { product } = offer;
|
|
423
411
|
const order = CredentialOrderFactory({
|
|
424
412
|
product_id: product.id,
|
|
425
413
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -429,7 +417,7 @@ describe('CourseProductItem', () => {
|
|
|
429
417
|
|
|
430
418
|
fetchMock.get(
|
|
431
419
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
432
|
-
|
|
420
|
+
offer,
|
|
433
421
|
);
|
|
434
422
|
const orderQueryParameters = {
|
|
435
423
|
course_code: order.course.code,
|
|
@@ -443,14 +431,14 @@ describe('CourseProductItem', () => {
|
|
|
443
431
|
render(
|
|
444
432
|
<CourseProductItem
|
|
445
433
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
446
|
-
productId={
|
|
434
|
+
productId={offer.product.id}
|
|
447
435
|
compact
|
|
448
436
|
/>,
|
|
449
437
|
);
|
|
450
438
|
|
|
451
439
|
// In the header, we should display the product title, the product price
|
|
452
440
|
// and product date range and languages
|
|
453
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
441
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
454
442
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
455
443
|
// but we want to it to visually look like a h6
|
|
456
444
|
|
|
@@ -460,7 +448,7 @@ describe('CourseProductItem', () => {
|
|
|
460
448
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
461
449
|
|
|
462
450
|
// - Any target courses information should be displayed
|
|
463
|
-
|
|
451
|
+
offer.product.target_courses.forEach((course) => {
|
|
464
452
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
465
453
|
expect($item).not.toBeInTheDocument();
|
|
466
454
|
});
|
|
@@ -471,8 +459,8 @@ describe('CourseProductItem', () => {
|
|
|
471
459
|
);
|
|
472
460
|
|
|
473
461
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
474
|
-
const
|
|
475
|
-
const { product } =
|
|
462
|
+
const offer = OfferFactory().one();
|
|
463
|
+
const { product } = offer;
|
|
476
464
|
const order = CredentialOrderFactory({
|
|
477
465
|
product_id: product.id,
|
|
478
466
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -480,10 +468,7 @@ describe('CourseProductItem', () => {
|
|
|
480
468
|
state,
|
|
481
469
|
}).one();
|
|
482
470
|
|
|
483
|
-
fetchMock.get(
|
|
484
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
485
|
-
relation,
|
|
486
|
-
);
|
|
471
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
487
472
|
const orderQueryParameters = {
|
|
488
473
|
course_code: order.course.code,
|
|
489
474
|
product_id: order.product_id,
|
|
@@ -533,17 +518,17 @@ describe('CourseProductItem', () => {
|
|
|
533
518
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
534
519
|
'renders product informations for a %s order in compact mode',
|
|
535
520
|
async (state) => {
|
|
536
|
-
const
|
|
521
|
+
const offer = OfferFactory().one();
|
|
537
522
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
538
|
-
product_id:
|
|
523
|
+
product_id: offer.product.id,
|
|
539
524
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
540
|
-
target_courses:
|
|
525
|
+
target_courses: offer.product.target_courses,
|
|
541
526
|
state,
|
|
542
527
|
}).one();
|
|
543
528
|
|
|
544
529
|
fetchMock.get(
|
|
545
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
546
|
-
|
|
530
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
|
|
531
|
+
offer,
|
|
547
532
|
);
|
|
548
533
|
const orderQueryParameters = {
|
|
549
534
|
product_id: order.product_id,
|
|
@@ -557,14 +542,14 @@ describe('CourseProductItem', () => {
|
|
|
557
542
|
|
|
558
543
|
render(
|
|
559
544
|
<CourseProductItem
|
|
560
|
-
productId={
|
|
545
|
+
productId={offer.product.id}
|
|
561
546
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
562
547
|
compact
|
|
563
548
|
/>,
|
|
564
549
|
);
|
|
565
550
|
|
|
566
551
|
// Wait for product information to be fetched
|
|
567
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
552
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
568
553
|
|
|
569
554
|
// - In place of product price, a label should be displayed
|
|
570
555
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -601,8 +586,8 @@ describe('CourseProductItem', () => {
|
|
|
601
586
|
);
|
|
602
587
|
|
|
603
588
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
604
|
-
const
|
|
605
|
-
const { product } =
|
|
589
|
+
const offer = OfferFactory().one();
|
|
590
|
+
const { product } = offer;
|
|
606
591
|
// - Create an order with an active enrollment
|
|
607
592
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
608
593
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -614,10 +599,7 @@ describe('CourseProductItem', () => {
|
|
|
614
599
|
target_enrollments: [enrollment],
|
|
615
600
|
}).one();
|
|
616
601
|
|
|
617
|
-
fetchMock.get(
|
|
618
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
619
|
-
relation,
|
|
620
|
-
);
|
|
602
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
621
603
|
const orderQueryParameters = {
|
|
622
604
|
product_id: order.product_id,
|
|
623
605
|
course_code: order.course?.code,
|
|
@@ -668,8 +650,8 @@ describe('CourseProductItem', () => {
|
|
|
668
650
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
669
651
|
'renders sale tunnel button if user already has a %s order',
|
|
670
652
|
async (state) => {
|
|
671
|
-
const
|
|
672
|
-
const { product } =
|
|
653
|
+
const offer = OfferFactory().one();
|
|
654
|
+
const { product } = offer;
|
|
673
655
|
const order = CredentialOrderFactory({
|
|
674
656
|
product_id: product.id,
|
|
675
657
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -678,7 +660,7 @@ describe('CourseProductItem', () => {
|
|
|
678
660
|
}).one();
|
|
679
661
|
fetchMock.get(
|
|
680
662
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
681
|
-
|
|
663
|
+
offer,
|
|
682
664
|
);
|
|
683
665
|
const orderQueryParameters = {
|
|
684
666
|
product_id: order.product_id,
|
|
@@ -709,7 +691,7 @@ describe('CourseProductItem', () => {
|
|
|
709
691
|
expect($price.classList.contains('h6')).toBe(true);
|
|
710
692
|
|
|
711
693
|
// - Render all target courses information
|
|
712
|
-
|
|
694
|
+
offer.product.target_courses.forEach((course) => {
|
|
713
695
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
714
696
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
715
697
|
// but we want to it to visually look like a h5
|
|
@@ -724,12 +706,9 @@ describe('CourseProductItem', () => {
|
|
|
724
706
|
);
|
|
725
707
|
|
|
726
708
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
727
|
-
const
|
|
728
|
-
const { product } =
|
|
729
|
-
fetchMock.get(
|
|
730
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
731
|
-
relation,
|
|
732
|
-
);
|
|
709
|
+
const offer = OfferFactory().one();
|
|
710
|
+
const { product } = offer;
|
|
711
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
733
712
|
const orderQueryParameters = {
|
|
734
713
|
product_id: product.id,
|
|
735
714
|
course_code: '00000',
|
|
@@ -759,7 +738,7 @@ describe('CourseProductItem', () => {
|
|
|
759
738
|
expect($price.classList.contains('h6')).toBe(true);
|
|
760
739
|
|
|
761
740
|
// - Render all target courses information
|
|
762
|
-
|
|
741
|
+
offer.product.target_courses.forEach((course) => {
|
|
763
742
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
764
743
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
765
744
|
// but we want to it to visually look like a h5
|
|
@@ -773,7 +752,7 @@ describe('CourseProductItem', () => {
|
|
|
773
752
|
});
|
|
774
753
|
|
|
775
754
|
it('renders error message when product fetching has failed', async () => {
|
|
776
|
-
const { product } =
|
|
755
|
+
const { product } = OfferFactory().one();
|
|
777
756
|
|
|
778
757
|
fetchMock.get(
|
|
779
758
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -794,21 +773,18 @@ describe('CourseProductItem', () => {
|
|
|
794
773
|
});
|
|
795
774
|
|
|
796
775
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
797
|
-
const
|
|
776
|
+
const offer = OfferFactory({
|
|
798
777
|
seats: 2,
|
|
799
778
|
nb_seats_available: 0,
|
|
800
779
|
}).one();
|
|
801
|
-
const { product } =
|
|
780
|
+
const { product } = offer;
|
|
802
781
|
const order = CredentialOrderFactory({
|
|
803
782
|
product_id: product.id,
|
|
804
783
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
805
784
|
target_courses: product.target_courses,
|
|
806
785
|
state: OrderState.DRAFT,
|
|
807
786
|
}).one();
|
|
808
|
-
fetchMock.get(
|
|
809
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
810
|
-
relation,
|
|
811
|
-
);
|
|
787
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
812
788
|
const orderQueryParameters = {
|
|
813
789
|
product_id: order.product_id,
|
|
814
790
|
course_code: order.course?.code,
|
|
@@ -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
|
+
OfferFactory,
|
|
7
7
|
CourseRunFactory,
|
|
8
8
|
CredentialOrderFactory,
|
|
9
9
|
CredentialProductFactory,
|
|
@@ -22,7 +22,7 @@ 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
|
+
OfferFactory({
|
|
26
26
|
product: CredentialProductFactory({
|
|
27
27
|
price: 840,
|
|
28
28
|
price_currency: 'EUR',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Children, useEffect, useMemo } from 'react';
|
|
2
2
|
import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
|
|
3
3
|
import c from 'classnames';
|
|
4
|
-
import {
|
|
4
|
+
import { Offer, CredentialOrder, Product, ProductType } from 'types/Joanie';
|
|
5
5
|
import { useCourseProduct } from 'hooks/useCourseProducts';
|
|
6
6
|
import { Spinner } from 'components/Spinner';
|
|
7
7
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
@@ -77,16 +77,9 @@ type HeaderProps = {
|
|
|
77
77
|
canPurchase: boolean;
|
|
78
78
|
order: Maybe<CredentialOrder>;
|
|
79
79
|
product: Product;
|
|
80
|
-
|
|
80
|
+
offer: Offer;
|
|
81
81
|
};
|
|
82
|
-
const Header = ({
|
|
83
|
-
product,
|
|
84
|
-
order,
|
|
85
|
-
courseProductRelation,
|
|
86
|
-
hasPurchased,
|
|
87
|
-
canPurchase,
|
|
88
|
-
compact,
|
|
89
|
-
}: HeaderProps) => {
|
|
82
|
+
const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
90
83
|
const intl = useIntl();
|
|
91
84
|
const formatDate = useDateFormat();
|
|
92
85
|
|
|
@@ -110,7 +103,7 @@ const Header = ({
|
|
|
110
103
|
return null;
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
if (
|
|
106
|
+
if (offer.discounted_price) {
|
|
114
107
|
return (
|
|
115
108
|
<>
|
|
116
109
|
<span id="original-price" className="offscreen">
|
|
@@ -129,7 +122,7 @@ const Header = ({
|
|
|
129
122
|
<ins aria-describedby="discount-price" className="product-widget__price-discount">
|
|
130
123
|
<FormattedNumber
|
|
131
124
|
currency={product.price_currency}
|
|
132
|
-
value={
|
|
125
|
+
value={offer.discounted_price}
|
|
133
126
|
style="currency"
|
|
134
127
|
/>
|
|
135
128
|
</ins>
|
|
@@ -140,7 +133,7 @@ const Header = ({
|
|
|
140
133
|
return (
|
|
141
134
|
<FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
|
|
142
135
|
);
|
|
143
|
-
}, [canPurchase,
|
|
136
|
+
}, [canPurchase, offer.discounted_price, product.price]);
|
|
144
137
|
|
|
145
138
|
return (
|
|
146
139
|
<header className="product-widget__header">
|
|
@@ -151,40 +144,37 @@ const Header = ({
|
|
|
151
144
|
{hasPurchased && <FormattedMessage {...messages.purchased} />}
|
|
152
145
|
{displayPrice}
|
|
153
146
|
</strong>
|
|
154
|
-
{
|
|
155
|
-
<p className="product-widget__header-description">{
|
|
147
|
+
{offer?.description && (
|
|
148
|
+
<p className="product-widget__header-description">{offer.description}</p>
|
|
156
149
|
)}
|
|
157
|
-
{
|
|
150
|
+
{offer?.discounted_price && (
|
|
158
151
|
<p className="product-widget__header-discount">
|
|
159
|
-
{
|
|
152
|
+
{offer.discount_rate ? (
|
|
160
153
|
<span className="product-widget__header-discount-rate">
|
|
161
|
-
<FormattedNumber value={-
|
|
154
|
+
<FormattedNumber value={-offer.discount_rate} style="percent" />
|
|
162
155
|
</span>
|
|
163
156
|
) : (
|
|
164
157
|
<span className="product-widget__header-discount-amount">
|
|
165
158
|
<FormattedNumber
|
|
166
159
|
currency={product.price_currency}
|
|
167
|
-
value={-
|
|
160
|
+
value={-offer.discount_amount!}
|
|
168
161
|
style="currency"
|
|
169
162
|
/>
|
|
170
163
|
</span>
|
|
171
164
|
)}
|
|
172
|
-
{
|
|
165
|
+
{offer.discount_start && (
|
|
173
166
|
<span className="product-widget__header-discount-date">
|
|
174
167
|
|
|
175
168
|
<FormattedMessage
|
|
176
169
|
{...messages.from}
|
|
177
|
-
values={{ from: formatDate(
|
|
170
|
+
values={{ from: formatDate(offer.discount_start) }}
|
|
178
171
|
/>
|
|
179
172
|
</span>
|
|
180
173
|
)}
|
|
181
|
-
{
|
|
174
|
+
{offer.discount_end && (
|
|
182
175
|
<span className="product-widget__header-discount-date">
|
|
183
176
|
|
|
184
|
-
<FormattedMessage
|
|
185
|
-
{...messages.to}
|
|
186
|
-
values={{ to: formatDate(courseProductRelation.discount_end) }}
|
|
187
|
-
/>
|
|
177
|
+
<FormattedMessage {...messages.to} values={{ to: formatDate(offer.discount_end) }} />
|
|
188
178
|
</span>
|
|
189
179
|
)}
|
|
190
180
|
</p>
|
|
@@ -246,12 +236,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
246
236
|
const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
|
|
247
237
|
// FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
|
|
248
238
|
// CredentialOrder
|
|
249
|
-
const { item:
|
|
239
|
+
const { item: offer, states: productQueryStates } = useCourseProduct({
|
|
250
240
|
product_id: productId,
|
|
251
241
|
course_id: course.code,
|
|
252
242
|
});
|
|
253
243
|
|
|
254
|
-
const product =
|
|
244
|
+
const product = offer?.product;
|
|
255
245
|
const { item: productOrder, states: orderQueryStates } = useProductOrder({
|
|
256
246
|
productId,
|
|
257
247
|
courseCode: course.code,
|
|
@@ -309,18 +299,14 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
309
299
|
<Header
|
|
310
300
|
product={product}
|
|
311
301
|
order={order}
|
|
312
|
-
|
|
302
|
+
offer={offer}
|
|
313
303
|
canPurchase={canPurchase}
|
|
314
304
|
hasPurchased={hasPurchased}
|
|
315
305
|
compact={compact}
|
|
316
306
|
/>
|
|
317
307
|
{canShowContent && <Content product={product} order={order} />}
|
|
318
308
|
<footer className="product-widget__footer">
|
|
319
|
-
<CourseProductItemFooter
|
|
320
|
-
course={course}
|
|
321
|
-
courseProductRelation={courseProductRelation}
|
|
322
|
-
canPurchase={canPurchase}
|
|
323
|
-
/>
|
|
309
|
+
<CourseProductItemFooter course={course} offer={offer} canPurchase={canPurchase} />
|
|
324
310
|
</footer>
|
|
325
311
|
</>
|
|
326
312
|
)}
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
import SyllabusCourseRunsList from 'widgets/SyllabusCourseRunsList/index';
|
|
23
23
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
24
24
|
import { CourseRun, Priority } from 'types';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { Offer } from 'types/Joanie';
|
|
26
|
+
import { OfferFactory } from 'utils/test/factories/joanie';
|
|
27
27
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
28
28
|
import { StringHelper } from 'utils/StringHelper';
|
|
29
29
|
import { computeStates } from 'utils/CourseRuns';
|
|
@@ -211,9 +211,9 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
211
211
|
});
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
-
const expectCourseProduct = async (container: HTMLElement,
|
|
214
|
+
const expectCourseProduct = async (container: HTMLElement, offer: Offer) => {
|
|
215
215
|
const heading = await findByRole(container, 'heading', {
|
|
216
|
-
name:
|
|
216
|
+
name: offer.product.title,
|
|
217
217
|
});
|
|
218
218
|
expect(Array.from(heading.classList)).toContain('product-widget__title');
|
|
219
219
|
};
|
|
@@ -383,9 +383,9 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
383
383
|
|
|
384
384
|
it('has one opened product', async () => {
|
|
385
385
|
const course = PacedCourseFactory().one();
|
|
386
|
-
const
|
|
387
|
-
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${
|
|
388
|
-
fetchMock.get(resourceLink,
|
|
386
|
+
const offer = OfferFactory().one();
|
|
387
|
+
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offer.product.id}/`;
|
|
388
|
+
fetchMock.get(resourceLink, offer);
|
|
389
389
|
|
|
390
390
|
const courseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
391
391
|
resource_link: resourceLink,
|
|
@@ -406,7 +406,7 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
406
406
|
expect(getHeaderContainer().querySelectorAll('.course-detail__run-descriptions').length).toBe(
|
|
407
407
|
1,
|
|
408
408
|
);
|
|
409
|
-
await expectCourseProduct(getHeaderContainer(),
|
|
409
|
+
await expectCourseProduct(getHeaderContainer(), offer);
|
|
410
410
|
|
|
411
411
|
// Portal.
|
|
412
412
|
expectEmptyPortalContainer();
|