richie-education 3.1.3-dev15 → 3.1.3-dev17
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 +12 -11
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +28 -22
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -3
- 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 -16
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -4
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -4
- package/js/hooks/useOffering/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 +17 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -8
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +6 -3
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -10
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -8
- 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 -21
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +19 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +6 -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 +7 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -5
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +55 -55
- 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 -25
- package/js/pages/TeacherDashboardTraining/index.tsx +16 -12
- package/js/types/Joanie.ts +21 -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 -23
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +20 -17
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +22 -16
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -3
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +14 -10
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +87 -63
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +2 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +24 -20
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useOffer/index.ts +0 -32
|
@@ -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,14 +92,17 @@ 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 } =
|
|
102
|
-
fetchMock.get(
|
|
100
|
+
const offering = OfferingFactory().one();
|
|
101
|
+
const { product } = offering;
|
|
102
|
+
fetchMock.get(
|
|
103
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
104
|
+
offering,
|
|
105
|
+
);
|
|
103
106
|
|
|
104
107
|
render(
|
|
105
108
|
<CourseProductItem
|
|
@@ -128,7 +131,7 @@ describe('CourseProductItem', () => {
|
|
|
128
131
|
).not.toBeInTheDocument();
|
|
129
132
|
|
|
130
133
|
// - Render all target courses information
|
|
131
|
-
|
|
134
|
+
offering.product.target_courses.forEach((course) => {
|
|
132
135
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
133
136
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
134
137
|
// but we want to it to visually look like a h5
|
|
@@ -148,7 +151,7 @@ describe('CourseProductItem', () => {
|
|
|
148
151
|
});
|
|
149
152
|
|
|
150
153
|
it('renders discount rate for anonymous user', async () => {
|
|
151
|
-
const
|
|
154
|
+
const offering = OfferingFactory({
|
|
152
155
|
product: CredentialProductFactory({
|
|
153
156
|
price: 840,
|
|
154
157
|
price_currency: 'EUR',
|
|
@@ -161,8 +164,11 @@ describe('CourseProductItem', () => {
|
|
|
161
164
|
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
162
165
|
},
|
|
163
166
|
}).one();
|
|
164
|
-
const { product } =
|
|
165
|
-
fetchMock.get(
|
|
167
|
+
const { product } = offering;
|
|
168
|
+
fetchMock.get(
|
|
169
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
170
|
+
offering,
|
|
171
|
+
);
|
|
166
172
|
|
|
167
173
|
render(
|
|
168
174
|
<CourseProductItem
|
|
@@ -188,7 +194,7 @@ describe('CourseProductItem', () => {
|
|
|
188
194
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
189
195
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
190
196
|
const discountedPrice = screen.getByText(
|
|
191
|
-
priceFormatter(product.price_currency,
|
|
197
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
192
198
|
/(\u202F|\u00a0)/g,
|
|
193
199
|
' ',
|
|
194
200
|
),
|
|
@@ -208,7 +214,7 @@ describe('CourseProductItem', () => {
|
|
|
208
214
|
});
|
|
209
215
|
|
|
210
216
|
it('renders discount amount for anonymous user', async () => {
|
|
211
|
-
const
|
|
217
|
+
const offering = OfferingFactory({
|
|
212
218
|
product: CredentialProductFactory({
|
|
213
219
|
price: 840,
|
|
214
220
|
price_currency: 'EUR',
|
|
@@ -221,8 +227,11 @@ describe('CourseProductItem', () => {
|
|
|
221
227
|
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
222
228
|
},
|
|
223
229
|
}).one();
|
|
224
|
-
const { product } =
|
|
225
|
-
fetchMock.get(
|
|
230
|
+
const { product } = offering;
|
|
231
|
+
fetchMock.get(
|
|
232
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
233
|
+
offering,
|
|
234
|
+
);
|
|
226
235
|
|
|
227
236
|
render(
|
|
228
237
|
<CourseProductItem
|
|
@@ -248,7 +257,7 @@ describe('CourseProductItem', () => {
|
|
|
248
257
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
249
258
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
250
259
|
const discountedPrice = screen.getByText(
|
|
251
|
-
priceFormatter(product.price_currency,
|
|
260
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
252
261
|
/(\u202F|\u00a0)/g,
|
|
253
262
|
' ',
|
|
254
263
|
),
|
|
@@ -268,13 +277,16 @@ describe('CourseProductItem', () => {
|
|
|
268
277
|
});
|
|
269
278
|
|
|
270
279
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
271
|
-
const
|
|
280
|
+
const offering = OfferingFactory({
|
|
272
281
|
product: ProductFactory({
|
|
273
282
|
certificate_definition: undefined,
|
|
274
283
|
}).one(),
|
|
275
284
|
}).one();
|
|
276
|
-
const { product } =
|
|
277
|
-
fetchMock.get(
|
|
285
|
+
const { product } = offering;
|
|
286
|
+
fetchMock.get(
|
|
287
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
288
|
+
offering,
|
|
289
|
+
);
|
|
278
290
|
|
|
279
291
|
render(
|
|
280
292
|
<CourseProductItem
|
|
@@ -292,16 +304,16 @@ describe('CourseProductItem', () => {
|
|
|
292
304
|
});
|
|
293
305
|
|
|
294
306
|
it('renders product information in compact mode', async () => {
|
|
295
|
-
const
|
|
307
|
+
const offering = OfferingFactory().one();
|
|
296
308
|
fetchMock.get(
|
|
297
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
298
|
-
|
|
309
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
310
|
+
offering,
|
|
299
311
|
);
|
|
300
312
|
|
|
301
313
|
const { container } = render(
|
|
302
314
|
<CourseProductItem
|
|
303
315
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
304
|
-
productId={
|
|
316
|
+
productId={offering.product.id}
|
|
305
317
|
compact
|
|
306
318
|
/>,
|
|
307
319
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -309,14 +321,14 @@ describe('CourseProductItem', () => {
|
|
|
309
321
|
|
|
310
322
|
// In the header, we should display the product title, the product price
|
|
311
323
|
// and product date range and languages
|
|
312
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
324
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
313
325
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
314
326
|
// but we want to it to visually look like a h6
|
|
315
327
|
|
|
316
328
|
const $price = screen.getByText(
|
|
317
329
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
318
330
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
319
|
-
priceFormatter(
|
|
331
|
+
priceFormatter(offering.product.price_currency, offering.product.price).replace(
|
|
320
332
|
/(\u202F|\u00a0)/g,
|
|
321
333
|
' ',
|
|
322
334
|
),
|
|
@@ -332,7 +344,7 @@ describe('CourseProductItem', () => {
|
|
|
332
344
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
333
345
|
|
|
334
346
|
// - Any target courses information should be displayed
|
|
335
|
-
|
|
347
|
+
offering.product.target_courses.forEach((course) => {
|
|
336
348
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
337
349
|
expect($item).not.toBeInTheDocument();
|
|
338
350
|
});
|
|
@@ -341,7 +353,7 @@ describe('CourseProductItem', () => {
|
|
|
341
353
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
342
354
|
|
|
343
355
|
// - Render a login button
|
|
344
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
356
|
+
screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
|
|
345
357
|
// - Does not render PurchaseButton cta
|
|
346
358
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
347
359
|
});
|
|
@@ -349,8 +361,8 @@ describe('CourseProductItem', () => {
|
|
|
349
361
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
350
362
|
'renders product informations for %s order',
|
|
351
363
|
async (state) => {
|
|
352
|
-
const
|
|
353
|
-
const { product } =
|
|
364
|
+
const offering = OfferingFactory().one();
|
|
365
|
+
const { product } = offering;
|
|
354
366
|
const order = CredentialOrderFactory({
|
|
355
367
|
product_id: product.id,
|
|
356
368
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -360,7 +372,7 @@ describe('CourseProductItem', () => {
|
|
|
360
372
|
|
|
361
373
|
fetchMock.get(
|
|
362
374
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
363
|
-
|
|
375
|
+
offering,
|
|
364
376
|
);
|
|
365
377
|
const orderQueryParameters = {
|
|
366
378
|
course_code: order.course.code,
|
|
@@ -374,13 +386,13 @@ describe('CourseProductItem', () => {
|
|
|
374
386
|
render(
|
|
375
387
|
<CourseProductItem
|
|
376
388
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
377
|
-
productId={
|
|
389
|
+
productId={offering.product.id}
|
|
378
390
|
/>,
|
|
379
391
|
);
|
|
380
392
|
|
|
381
393
|
// In the header, we should display the product title, the product price
|
|
382
394
|
// and product date range and languages
|
|
383
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
395
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
384
396
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
385
397
|
// but we want to it to visually look like a h6
|
|
386
398
|
|
|
@@ -410,8 +422,8 @@ describe('CourseProductItem', () => {
|
|
|
410
422
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
411
423
|
'renders product informations for %s order in compact mode',
|
|
412
424
|
async (state) => {
|
|
413
|
-
const
|
|
414
|
-
const { product } =
|
|
425
|
+
const offering = OfferingFactory().one();
|
|
426
|
+
const { product } = offering;
|
|
415
427
|
const order = CredentialOrderFactory({
|
|
416
428
|
product_id: product.id,
|
|
417
429
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -421,7 +433,7 @@ describe('CourseProductItem', () => {
|
|
|
421
433
|
|
|
422
434
|
fetchMock.get(
|
|
423
435
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
424
|
-
|
|
436
|
+
offering,
|
|
425
437
|
);
|
|
426
438
|
const orderQueryParameters = {
|
|
427
439
|
course_code: order.course.code,
|
|
@@ -435,14 +447,14 @@ describe('CourseProductItem', () => {
|
|
|
435
447
|
render(
|
|
436
448
|
<CourseProductItem
|
|
437
449
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
438
|
-
productId={
|
|
450
|
+
productId={offering.product.id}
|
|
439
451
|
compact
|
|
440
452
|
/>,
|
|
441
453
|
);
|
|
442
454
|
|
|
443
455
|
// In the header, we should display the product title, the product price
|
|
444
456
|
// and product date range and languages
|
|
445
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
457
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
446
458
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
447
459
|
// but we want to it to visually look like a h6
|
|
448
460
|
|
|
@@ -452,7 +464,7 @@ describe('CourseProductItem', () => {
|
|
|
452
464
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
453
465
|
|
|
454
466
|
// - Any target courses information should be displayed
|
|
455
|
-
|
|
467
|
+
offering.product.target_courses.forEach((course) => {
|
|
456
468
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
457
469
|
expect($item).not.toBeInTheDocument();
|
|
458
470
|
});
|
|
@@ -463,8 +475,8 @@ describe('CourseProductItem', () => {
|
|
|
463
475
|
);
|
|
464
476
|
|
|
465
477
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
466
|
-
const
|
|
467
|
-
const { product } =
|
|
478
|
+
const offering = OfferingFactory().one();
|
|
479
|
+
const { product } = offering;
|
|
468
480
|
const order = CredentialOrderFactory({
|
|
469
481
|
product_id: product.id,
|
|
470
482
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -472,7 +484,10 @@ describe('CourseProductItem', () => {
|
|
|
472
484
|
state,
|
|
473
485
|
}).one();
|
|
474
486
|
|
|
475
|
-
fetchMock.get(
|
|
487
|
+
fetchMock.get(
|
|
488
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
489
|
+
offering,
|
|
490
|
+
);
|
|
476
491
|
const orderQueryParameters = {
|
|
477
492
|
course_code: order.course.code,
|
|
478
493
|
product_id: order.product_id,
|
|
@@ -522,17 +537,17 @@ describe('CourseProductItem', () => {
|
|
|
522
537
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
523
538
|
'renders product informations for a %s order in compact mode',
|
|
524
539
|
async (state) => {
|
|
525
|
-
const
|
|
540
|
+
const offering = OfferingFactory().one();
|
|
526
541
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
527
|
-
product_id:
|
|
542
|
+
product_id: offering.product.id,
|
|
528
543
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
529
|
-
target_courses:
|
|
544
|
+
target_courses: offering.product.target_courses,
|
|
530
545
|
state,
|
|
531
546
|
}).one();
|
|
532
547
|
|
|
533
548
|
fetchMock.get(
|
|
534
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
535
|
-
|
|
549
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
550
|
+
offering,
|
|
536
551
|
);
|
|
537
552
|
const orderQueryParameters = {
|
|
538
553
|
product_id: order.product_id,
|
|
@@ -546,14 +561,14 @@ describe('CourseProductItem', () => {
|
|
|
546
561
|
|
|
547
562
|
render(
|
|
548
563
|
<CourseProductItem
|
|
549
|
-
productId={
|
|
564
|
+
productId={offering.product.id}
|
|
550
565
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
551
566
|
compact
|
|
552
567
|
/>,
|
|
553
568
|
);
|
|
554
569
|
|
|
555
570
|
// Wait for product information to be fetched
|
|
556
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
571
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
557
572
|
|
|
558
573
|
// - In place of product price, a label should be displayed
|
|
559
574
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -590,8 +605,8 @@ describe('CourseProductItem', () => {
|
|
|
590
605
|
);
|
|
591
606
|
|
|
592
607
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
593
|
-
const
|
|
594
|
-
const { product } =
|
|
608
|
+
const offering = OfferingFactory().one();
|
|
609
|
+
const { product } = offering;
|
|
595
610
|
// - Create an order with an active enrollment
|
|
596
611
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
597
612
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -603,7 +618,10 @@ describe('CourseProductItem', () => {
|
|
|
603
618
|
target_enrollments: [enrollment],
|
|
604
619
|
}).one();
|
|
605
620
|
|
|
606
|
-
fetchMock.get(
|
|
621
|
+
fetchMock.get(
|
|
622
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
623
|
+
offering,
|
|
624
|
+
);
|
|
607
625
|
const orderQueryParameters = {
|
|
608
626
|
product_id: order.product_id,
|
|
609
627
|
course_code: order.course?.code,
|
|
@@ -654,8 +672,8 @@ describe('CourseProductItem', () => {
|
|
|
654
672
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
655
673
|
'renders sale tunnel button if user already has a %s order',
|
|
656
674
|
async (state) => {
|
|
657
|
-
const
|
|
658
|
-
const { product } =
|
|
675
|
+
const offering = OfferingFactory().one();
|
|
676
|
+
const { product } = offering;
|
|
659
677
|
const order = CredentialOrderFactory({
|
|
660
678
|
product_id: product.id,
|
|
661
679
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -664,7 +682,7 @@ describe('CourseProductItem', () => {
|
|
|
664
682
|
}).one();
|
|
665
683
|
fetchMock.get(
|
|
666
684
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
667
|
-
|
|
685
|
+
offering,
|
|
668
686
|
);
|
|
669
687
|
const orderQueryParameters = {
|
|
670
688
|
product_id: order.product_id,
|
|
@@ -695,7 +713,7 @@ describe('CourseProductItem', () => {
|
|
|
695
713
|
expect($price.classList.contains('h6')).toBe(true);
|
|
696
714
|
|
|
697
715
|
// - Render all target courses information
|
|
698
|
-
|
|
716
|
+
offering.product.target_courses.forEach((course) => {
|
|
699
717
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
700
718
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
701
719
|
// but we want to it to visually look like a h5
|
|
@@ -710,9 +728,12 @@ describe('CourseProductItem', () => {
|
|
|
710
728
|
);
|
|
711
729
|
|
|
712
730
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
713
|
-
const
|
|
714
|
-
const { product } =
|
|
715
|
-
fetchMock.get(
|
|
731
|
+
const offering = OfferingFactory().one();
|
|
732
|
+
const { product } = offering;
|
|
733
|
+
fetchMock.get(
|
|
734
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
735
|
+
offering,
|
|
736
|
+
);
|
|
716
737
|
const orderQueryParameters = {
|
|
717
738
|
product_id: product.id,
|
|
718
739
|
course_code: '00000',
|
|
@@ -742,7 +763,7 @@ describe('CourseProductItem', () => {
|
|
|
742
763
|
expect($price.classList.contains('h6')).toBe(true);
|
|
743
764
|
|
|
744
765
|
// - Render all target courses information
|
|
745
|
-
|
|
766
|
+
offering.product.target_courses.forEach((course) => {
|
|
746
767
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
747
768
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
748
769
|
// but we want to it to visually look like a h5
|
|
@@ -756,7 +777,7 @@ describe('CourseProductItem', () => {
|
|
|
756
777
|
});
|
|
757
778
|
|
|
758
779
|
it('renders error message when product fetching has failed', async () => {
|
|
759
|
-
const { product } =
|
|
780
|
+
const { product } = OfferingFactory().one();
|
|
760
781
|
|
|
761
782
|
fetchMock.get(
|
|
762
783
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -777,20 +798,23 @@ describe('CourseProductItem', () => {
|
|
|
777
798
|
});
|
|
778
799
|
|
|
779
800
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
780
|
-
const
|
|
801
|
+
const offering = OfferingFactory({
|
|
781
802
|
rules: {
|
|
782
803
|
nb_available_seats: 0,
|
|
783
804
|
has_seats_left: false,
|
|
784
805
|
},
|
|
785
806
|
}).one();
|
|
786
|
-
const { product } =
|
|
807
|
+
const { product } = offering;
|
|
787
808
|
const order = CredentialOrderFactory({
|
|
788
809
|
product_id: product.id,
|
|
789
810
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
790
811
|
target_courses: product.target_courses,
|
|
791
812
|
state: OrderState.DRAFT,
|
|
792
813
|
}).one();
|
|
793
|
-
fetchMock.get(
|
|
814
|
+
fetchMock.get(
|
|
815
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
816
|
+
offering,
|
|
817
|
+
);
|
|
794
818
|
const orderQueryParameters = {
|
|
795
819
|
product_id: order.product_id,
|
|
796
820
|
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
|
+
OfferingFactory,
|
|
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
|
+
OfferingFactory({
|
|
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 { Offering, 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,9 +77,9 @@ type HeaderProps = {
|
|
|
77
77
|
canPurchase: boolean;
|
|
78
78
|
order: Maybe<CredentialOrder>;
|
|
79
79
|
product: Product;
|
|
80
|
-
|
|
80
|
+
offering: Offering;
|
|
81
81
|
};
|
|
82
|
-
const Header = ({ product, order,
|
|
82
|
+
const Header = ({ product, order, offering, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
83
83
|
const intl = useIntl();
|
|
84
84
|
const formatDate = useDateFormat();
|
|
85
85
|
|
|
@@ -103,7 +103,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
if (
|
|
106
|
+
if (offering.rules.discounted_price != null) {
|
|
107
107
|
return (
|
|
108
108
|
<>
|
|
109
109
|
<span id="original-price" className="offscreen">
|
|
@@ -122,7 +122,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
|
|
|
122
122
|
<ins aria-describedby="discount-price" className="product-widget__price-discount">
|
|
123
123
|
<FormattedNumber
|
|
124
124
|
currency={product.price_currency}
|
|
125
|
-
value={
|
|
125
|
+
value={offering.rules.discounted_price}
|
|
126
126
|
style="currency"
|
|
127
127
|
/>
|
|
128
128
|
</ins>
|
|
@@ -133,7 +133,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
|
|
|
133
133
|
return (
|
|
134
134
|
<FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
|
|
135
135
|
);
|
|
136
|
-
}, [canPurchase,
|
|
136
|
+
}, [canPurchase, offering.rules.discounted_price, product.price]);
|
|
137
137
|
|
|
138
138
|
return (
|
|
139
139
|
<header className="product-widget__header">
|
|
@@ -144,39 +144,39 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
|
|
|
144
144
|
{hasPurchased && <FormattedMessage {...messages.purchased} />}
|
|
145
145
|
{displayPrice}
|
|
146
146
|
</strong>
|
|
147
|
-
{
|
|
148
|
-
<p className="product-widget__header-description">{
|
|
147
|
+
{offering?.rules.description && (
|
|
148
|
+
<p className="product-widget__header-description">{offering.rules.description}</p>
|
|
149
149
|
)}
|
|
150
|
-
{
|
|
150
|
+
{offering?.rules.discounted_price && (
|
|
151
151
|
<p className="product-widget__header-discount">
|
|
152
|
-
{
|
|
152
|
+
{offering.rules.discount_rate ? (
|
|
153
153
|
<span className="product-widget__header-discount-rate">
|
|
154
|
-
<FormattedNumber value={-
|
|
154
|
+
<FormattedNumber value={-offering.rules.discount_rate} style="percent" />
|
|
155
155
|
</span>
|
|
156
156
|
) : (
|
|
157
157
|
<span className="product-widget__header-discount-amount">
|
|
158
158
|
<FormattedNumber
|
|
159
159
|
currency={product.price_currency}
|
|
160
|
-
value={-
|
|
160
|
+
value={-offering.rules.discount_amount!}
|
|
161
161
|
style="currency"
|
|
162
162
|
/>
|
|
163
163
|
</span>
|
|
164
164
|
)}
|
|
165
|
-
{
|
|
165
|
+
{offering.rules.discount_start && (
|
|
166
166
|
<span className="product-widget__header-discount-date">
|
|
167
167
|
|
|
168
168
|
<FormattedMessage
|
|
169
169
|
{...messages.from}
|
|
170
|
-
values={{ from: formatDate(
|
|
170
|
+
values={{ from: formatDate(offering.rules.discount_start) }}
|
|
171
171
|
/>
|
|
172
172
|
</span>
|
|
173
173
|
)}
|
|
174
|
-
{
|
|
174
|
+
{offering.rules.discount_end && (
|
|
175
175
|
<span className="product-widget__header-discount-date">
|
|
176
176
|
|
|
177
177
|
<FormattedMessage
|
|
178
178
|
{...messages.to}
|
|
179
|
-
values={{ to: formatDate(
|
|
179
|
+
values={{ to: formatDate(offering.rules.discount_end) }}
|
|
180
180
|
/>
|
|
181
181
|
</span>
|
|
182
182
|
)}
|
|
@@ -239,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
239
239
|
const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
|
|
240
240
|
// FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
|
|
241
241
|
// CredentialOrder
|
|
242
|
-
const { item:
|
|
242
|
+
const { item: offering, states: productQueryStates } = useCourseProduct({
|
|
243
243
|
product_id: productId,
|
|
244
244
|
course_id: course.code,
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
const product =
|
|
247
|
+
const product = offering?.product;
|
|
248
248
|
const { item: productOrder, states: orderQueryStates } = useProductOrder({
|
|
249
249
|
productId,
|
|
250
250
|
courseCode: course.code,
|
|
@@ -302,14 +302,18 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
302
302
|
<Header
|
|
303
303
|
product={product}
|
|
304
304
|
order={order}
|
|
305
|
-
|
|
305
|
+
offering={offering}
|
|
306
306
|
canPurchase={canPurchase}
|
|
307
307
|
hasPurchased={hasPurchased}
|
|
308
308
|
compact={compact}
|
|
309
309
|
/>
|
|
310
310
|
{canShowContent && <Content product={product} order={order} />}
|
|
311
311
|
<footer className="product-widget__footer">
|
|
312
|
-
<CourseProductItemFooter
|
|
312
|
+
<CourseProductItemFooter
|
|
313
|
+
course={course}
|
|
314
|
+
offering={offering}
|
|
315
|
+
canPurchase={canPurchase}
|
|
316
|
+
/>
|
|
313
317
|
</footer>
|
|
314
318
|
</>
|
|
315
319
|
)}
|
|
@@ -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 { Offering } from 'types/Joanie';
|
|
26
|
+
import { OfferingFactory } 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, offering: Offering) => {
|
|
215
215
|
const heading = await findByRole(container, 'heading', {
|
|
216
|
-
name:
|
|
216
|
+
name: offering.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 offering = OfferingFactory().one();
|
|
387
|
+
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offering.product.id}/`;
|
|
388
|
+
fetchMock.get(resourceLink, offering);
|
|
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(), offering);
|
|
410
410
|
|
|
411
411
|
// Portal.
|
|
412
412
|
expectEmptyPortalContainer();
|