richie-education 3.1.3-dev12 → 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 +5 -3
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +76 -63
- 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 +27 -20
- package/js/utils/test/factories/joanie.ts +14 -11
- 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 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +105 -75
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +27 -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,19 +151,24 @@ 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',
|
|
155
158
|
}).one(),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
},
|
|
161
166
|
}).one();
|
|
162
|
-
const { product } =
|
|
163
|
-
fetchMock.get(
|
|
167
|
+
const { product } = offering;
|
|
168
|
+
fetchMock.get(
|
|
169
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
170
|
+
offering,
|
|
171
|
+
);
|
|
164
172
|
|
|
165
173
|
render(
|
|
166
174
|
<CourseProductItem
|
|
@@ -186,7 +194,7 @@ describe('CourseProductItem', () => {
|
|
|
186
194
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
187
195
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
188
196
|
const discountedPrice = screen.getByText(
|
|
189
|
-
priceFormatter(product.price_currency,
|
|
197
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
190
198
|
/(\u202F|\u00a0)/g,
|
|
191
199
|
' ',
|
|
192
200
|
),
|
|
@@ -206,19 +214,24 @@ describe('CourseProductItem', () => {
|
|
|
206
214
|
});
|
|
207
215
|
|
|
208
216
|
it('renders discount amount for anonymous user', async () => {
|
|
209
|
-
const
|
|
217
|
+
const offering = OfferingFactory({
|
|
210
218
|
product: CredentialProductFactory({
|
|
211
219
|
price: 840,
|
|
212
220
|
price_currency: 'EUR',
|
|
213
221
|
}).one(),
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
},
|
|
219
229
|
}).one();
|
|
220
|
-
const { product } =
|
|
221
|
-
fetchMock.get(
|
|
230
|
+
const { product } = offering;
|
|
231
|
+
fetchMock.get(
|
|
232
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
233
|
+
offering,
|
|
234
|
+
);
|
|
222
235
|
|
|
223
236
|
render(
|
|
224
237
|
<CourseProductItem
|
|
@@ -244,7 +257,7 @@ describe('CourseProductItem', () => {
|
|
|
244
257
|
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
245
258
|
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
246
259
|
const discountedPrice = screen.getByText(
|
|
247
|
-
priceFormatter(product.price_currency,
|
|
260
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
248
261
|
/(\u202F|\u00a0)/g,
|
|
249
262
|
' ',
|
|
250
263
|
),
|
|
@@ -264,13 +277,16 @@ describe('CourseProductItem', () => {
|
|
|
264
277
|
});
|
|
265
278
|
|
|
266
279
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
267
|
-
const
|
|
280
|
+
const offering = OfferingFactory({
|
|
268
281
|
product: ProductFactory({
|
|
269
282
|
certificate_definition: undefined,
|
|
270
283
|
}).one(),
|
|
271
284
|
}).one();
|
|
272
|
-
const { product } =
|
|
273
|
-
fetchMock.get(
|
|
285
|
+
const { product } = offering;
|
|
286
|
+
fetchMock.get(
|
|
287
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
288
|
+
offering,
|
|
289
|
+
);
|
|
274
290
|
|
|
275
291
|
render(
|
|
276
292
|
<CourseProductItem
|
|
@@ -288,16 +304,16 @@ describe('CourseProductItem', () => {
|
|
|
288
304
|
});
|
|
289
305
|
|
|
290
306
|
it('renders product information in compact mode', async () => {
|
|
291
|
-
const
|
|
307
|
+
const offering = OfferingFactory().one();
|
|
292
308
|
fetchMock.get(
|
|
293
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
294
|
-
|
|
309
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
310
|
+
offering,
|
|
295
311
|
);
|
|
296
312
|
|
|
297
313
|
const { container } = render(
|
|
298
314
|
<CourseProductItem
|
|
299
315
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
300
|
-
productId={
|
|
316
|
+
productId={offering.product.id}
|
|
301
317
|
compact
|
|
302
318
|
/>,
|
|
303
319
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -305,14 +321,14 @@ describe('CourseProductItem', () => {
|
|
|
305
321
|
|
|
306
322
|
// In the header, we should display the product title, the product price
|
|
307
323
|
// and product date range and languages
|
|
308
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
324
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
309
325
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
310
326
|
// but we want to it to visually look like a h6
|
|
311
327
|
|
|
312
328
|
const $price = screen.getByText(
|
|
313
329
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
314
330
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
315
|
-
priceFormatter(
|
|
331
|
+
priceFormatter(offering.product.price_currency, offering.product.price).replace(
|
|
316
332
|
/(\u202F|\u00a0)/g,
|
|
317
333
|
' ',
|
|
318
334
|
),
|
|
@@ -328,7 +344,7 @@ describe('CourseProductItem', () => {
|
|
|
328
344
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
329
345
|
|
|
330
346
|
// - Any target courses information should be displayed
|
|
331
|
-
|
|
347
|
+
offering.product.target_courses.forEach((course) => {
|
|
332
348
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
333
349
|
expect($item).not.toBeInTheDocument();
|
|
334
350
|
});
|
|
@@ -337,7 +353,7 @@ describe('CourseProductItem', () => {
|
|
|
337
353
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
338
354
|
|
|
339
355
|
// - Render a login button
|
|
340
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
356
|
+
screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
|
|
341
357
|
// - Does not render PurchaseButton cta
|
|
342
358
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
343
359
|
});
|
|
@@ -345,8 +361,8 @@ describe('CourseProductItem', () => {
|
|
|
345
361
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
346
362
|
'renders product informations for %s order',
|
|
347
363
|
async (state) => {
|
|
348
|
-
const
|
|
349
|
-
const { product } =
|
|
364
|
+
const offering = OfferingFactory().one();
|
|
365
|
+
const { product } = offering;
|
|
350
366
|
const order = CredentialOrderFactory({
|
|
351
367
|
product_id: product.id,
|
|
352
368
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -356,7 +372,7 @@ describe('CourseProductItem', () => {
|
|
|
356
372
|
|
|
357
373
|
fetchMock.get(
|
|
358
374
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
359
|
-
|
|
375
|
+
offering,
|
|
360
376
|
);
|
|
361
377
|
const orderQueryParameters = {
|
|
362
378
|
course_code: order.course.code,
|
|
@@ -370,13 +386,13 @@ describe('CourseProductItem', () => {
|
|
|
370
386
|
render(
|
|
371
387
|
<CourseProductItem
|
|
372
388
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
373
|
-
productId={
|
|
389
|
+
productId={offering.product.id}
|
|
374
390
|
/>,
|
|
375
391
|
);
|
|
376
392
|
|
|
377
393
|
// In the header, we should display the product title, the product price
|
|
378
394
|
// and product date range and languages
|
|
379
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
395
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
380
396
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
381
397
|
// but we want to it to visually look like a h6
|
|
382
398
|
|
|
@@ -406,8 +422,8 @@ describe('CourseProductItem', () => {
|
|
|
406
422
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
407
423
|
'renders product informations for %s order in compact mode',
|
|
408
424
|
async (state) => {
|
|
409
|
-
const
|
|
410
|
-
const { product } =
|
|
425
|
+
const offering = OfferingFactory().one();
|
|
426
|
+
const { product } = offering;
|
|
411
427
|
const order = CredentialOrderFactory({
|
|
412
428
|
product_id: product.id,
|
|
413
429
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -417,7 +433,7 @@ describe('CourseProductItem', () => {
|
|
|
417
433
|
|
|
418
434
|
fetchMock.get(
|
|
419
435
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
420
|
-
|
|
436
|
+
offering,
|
|
421
437
|
);
|
|
422
438
|
const orderQueryParameters = {
|
|
423
439
|
course_code: order.course.code,
|
|
@@ -431,14 +447,14 @@ describe('CourseProductItem', () => {
|
|
|
431
447
|
render(
|
|
432
448
|
<CourseProductItem
|
|
433
449
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
434
|
-
productId={
|
|
450
|
+
productId={offering.product.id}
|
|
435
451
|
compact
|
|
436
452
|
/>,
|
|
437
453
|
);
|
|
438
454
|
|
|
439
455
|
// In the header, we should display the product title, the product price
|
|
440
456
|
// and product date range and languages
|
|
441
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
457
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
442
458
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
443
459
|
// but we want to it to visually look like a h6
|
|
444
460
|
|
|
@@ -448,7 +464,7 @@ describe('CourseProductItem', () => {
|
|
|
448
464
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
449
465
|
|
|
450
466
|
// - Any target courses information should be displayed
|
|
451
|
-
|
|
467
|
+
offering.product.target_courses.forEach((course) => {
|
|
452
468
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
453
469
|
expect($item).not.toBeInTheDocument();
|
|
454
470
|
});
|
|
@@ -459,8 +475,8 @@ describe('CourseProductItem', () => {
|
|
|
459
475
|
);
|
|
460
476
|
|
|
461
477
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
462
|
-
const
|
|
463
|
-
const { product } =
|
|
478
|
+
const offering = OfferingFactory().one();
|
|
479
|
+
const { product } = offering;
|
|
464
480
|
const order = CredentialOrderFactory({
|
|
465
481
|
product_id: product.id,
|
|
466
482
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -468,7 +484,10 @@ describe('CourseProductItem', () => {
|
|
|
468
484
|
state,
|
|
469
485
|
}).one();
|
|
470
486
|
|
|
471
|
-
fetchMock.get(
|
|
487
|
+
fetchMock.get(
|
|
488
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
489
|
+
offering,
|
|
490
|
+
);
|
|
472
491
|
const orderQueryParameters = {
|
|
473
492
|
course_code: order.course.code,
|
|
474
493
|
product_id: order.product_id,
|
|
@@ -518,17 +537,17 @@ describe('CourseProductItem', () => {
|
|
|
518
537
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
519
538
|
'renders product informations for a %s order in compact mode',
|
|
520
539
|
async (state) => {
|
|
521
|
-
const
|
|
540
|
+
const offering = OfferingFactory().one();
|
|
522
541
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
523
|
-
product_id:
|
|
542
|
+
product_id: offering.product.id,
|
|
524
543
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
525
|
-
target_courses:
|
|
544
|
+
target_courses: offering.product.target_courses,
|
|
526
545
|
state,
|
|
527
546
|
}).one();
|
|
528
547
|
|
|
529
548
|
fetchMock.get(
|
|
530
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
531
|
-
|
|
549
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
550
|
+
offering,
|
|
532
551
|
);
|
|
533
552
|
const orderQueryParameters = {
|
|
534
553
|
product_id: order.product_id,
|
|
@@ -542,14 +561,14 @@ describe('CourseProductItem', () => {
|
|
|
542
561
|
|
|
543
562
|
render(
|
|
544
563
|
<CourseProductItem
|
|
545
|
-
productId={
|
|
564
|
+
productId={offering.product.id}
|
|
546
565
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
547
566
|
compact
|
|
548
567
|
/>,
|
|
549
568
|
);
|
|
550
569
|
|
|
551
570
|
// Wait for product information to be fetched
|
|
552
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
571
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
553
572
|
|
|
554
573
|
// - In place of product price, a label should be displayed
|
|
555
574
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -586,8 +605,8 @@ describe('CourseProductItem', () => {
|
|
|
586
605
|
);
|
|
587
606
|
|
|
588
607
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
589
|
-
const
|
|
590
|
-
const { product } =
|
|
608
|
+
const offering = OfferingFactory().one();
|
|
609
|
+
const { product } = offering;
|
|
591
610
|
// - Create an order with an active enrollment
|
|
592
611
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
593
612
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -599,7 +618,10 @@ describe('CourseProductItem', () => {
|
|
|
599
618
|
target_enrollments: [enrollment],
|
|
600
619
|
}).one();
|
|
601
620
|
|
|
602
|
-
fetchMock.get(
|
|
621
|
+
fetchMock.get(
|
|
622
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
623
|
+
offering,
|
|
624
|
+
);
|
|
603
625
|
const orderQueryParameters = {
|
|
604
626
|
product_id: order.product_id,
|
|
605
627
|
course_code: order.course?.code,
|
|
@@ -650,8 +672,8 @@ describe('CourseProductItem', () => {
|
|
|
650
672
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
651
673
|
'renders sale tunnel button if user already has a %s order',
|
|
652
674
|
async (state) => {
|
|
653
|
-
const
|
|
654
|
-
const { product } =
|
|
675
|
+
const offering = OfferingFactory().one();
|
|
676
|
+
const { product } = offering;
|
|
655
677
|
const order = CredentialOrderFactory({
|
|
656
678
|
product_id: product.id,
|
|
657
679
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -660,7 +682,7 @@ describe('CourseProductItem', () => {
|
|
|
660
682
|
}).one();
|
|
661
683
|
fetchMock.get(
|
|
662
684
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
663
|
-
|
|
685
|
+
offering,
|
|
664
686
|
);
|
|
665
687
|
const orderQueryParameters = {
|
|
666
688
|
product_id: order.product_id,
|
|
@@ -691,7 +713,7 @@ describe('CourseProductItem', () => {
|
|
|
691
713
|
expect($price.classList.contains('h6')).toBe(true);
|
|
692
714
|
|
|
693
715
|
// - Render all target courses information
|
|
694
|
-
|
|
716
|
+
offering.product.target_courses.forEach((course) => {
|
|
695
717
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
696
718
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
697
719
|
// but we want to it to visually look like a h5
|
|
@@ -706,9 +728,12 @@ describe('CourseProductItem', () => {
|
|
|
706
728
|
);
|
|
707
729
|
|
|
708
730
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
709
|
-
const
|
|
710
|
-
const { product } =
|
|
711
|
-
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
|
+
);
|
|
712
737
|
const orderQueryParameters = {
|
|
713
738
|
product_id: product.id,
|
|
714
739
|
course_code: '00000',
|
|
@@ -738,7 +763,7 @@ describe('CourseProductItem', () => {
|
|
|
738
763
|
expect($price.classList.contains('h6')).toBe(true);
|
|
739
764
|
|
|
740
765
|
// - Render all target courses information
|
|
741
|
-
|
|
766
|
+
offering.product.target_courses.forEach((course) => {
|
|
742
767
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
743
768
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
744
769
|
// but we want to it to visually look like a h5
|
|
@@ -752,7 +777,7 @@ describe('CourseProductItem', () => {
|
|
|
752
777
|
});
|
|
753
778
|
|
|
754
779
|
it('renders error message when product fetching has failed', async () => {
|
|
755
|
-
const { product } =
|
|
780
|
+
const { product } = OfferingFactory().one();
|
|
756
781
|
|
|
757
782
|
fetchMock.get(
|
|
758
783
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -773,18 +798,23 @@ describe('CourseProductItem', () => {
|
|
|
773
798
|
});
|
|
774
799
|
|
|
775
800
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
801
|
+
const offering = OfferingFactory({
|
|
802
|
+
rules: {
|
|
803
|
+
nb_available_seats: 0,
|
|
804
|
+
has_seats_left: false,
|
|
805
|
+
},
|
|
779
806
|
}).one();
|
|
780
|
-
const { product } =
|
|
807
|
+
const { product } = offering;
|
|
781
808
|
const order = CredentialOrderFactory({
|
|
782
809
|
product_id: product.id,
|
|
783
810
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
784
811
|
target_courses: product.target_courses,
|
|
785
812
|
state: OrderState.DRAFT,
|
|
786
813
|
}).one();
|
|
787
|
-
fetchMock.get(
|
|
814
|
+
fetchMock.get(
|
|
815
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
816
|
+
offering,
|
|
817
|
+
);
|
|
788
818
|
const orderQueryParameters = {
|
|
789
819
|
product_id: order.product_id,
|
|
790
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,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
|
);
|
|
@@ -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,37 +144,40 @@ 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
|
-
<FormattedMessage
|
|
177
|
+
<FormattedMessage
|
|
178
|
+
{...messages.to}
|
|
179
|
+
values={{ to: formatDate(offering.rules.discount_end) }}
|
|
180
|
+
/>
|
|
178
181
|
</span>
|
|
179
182
|
)}
|
|
180
183
|
</p>
|
|
@@ -236,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
236
239
|
const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
|
|
237
240
|
// FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
|
|
238
241
|
// CredentialOrder
|
|
239
|
-
const { item:
|
|
242
|
+
const { item: offering, states: productQueryStates } = useCourseProduct({
|
|
240
243
|
product_id: productId,
|
|
241
244
|
course_id: course.code,
|
|
242
245
|
});
|
|
243
246
|
|
|
244
|
-
const product =
|
|
247
|
+
const product = offering?.product;
|
|
245
248
|
const { item: productOrder, states: orderQueryStates } = useProductOrder({
|
|
246
249
|
productId,
|
|
247
250
|
courseCode: course.code,
|
|
@@ -299,14 +302,18 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
299
302
|
<Header
|
|
300
303
|
product={product}
|
|
301
304
|
order={order}
|
|
302
|
-
|
|
305
|
+
offering={offering}
|
|
303
306
|
canPurchase={canPurchase}
|
|
304
307
|
hasPurchased={hasPurchased}
|
|
305
308
|
compact={compact}
|
|
306
309
|
/>
|
|
307
310
|
{canShowContent && <Content product={product} order={order} />}
|
|
308
311
|
<footer className="product-widget__footer">
|
|
309
|
-
<CourseProductItemFooter
|
|
312
|
+
<CourseProductItemFooter
|
|
313
|
+
course={course}
|
|
314
|
+
offering={offering}
|
|
315
|
+
canPurchase={canPurchase}
|
|
316
|
+
/>
|
|
310
317
|
</footer>
|
|
311
318
|
</>
|
|
312
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();
|