richie-education 2.28.2-dev39 → 2.28.2-dev58

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.
Files changed (102) hide show
  1. package/.eslintrc.json +11 -2
  2. package/i18n/locales/ar-SA.json +209 -125
  3. package/i18n/locales/es-ES.json +210 -126
  4. package/i18n/locales/fa-IR.json +209 -125
  5. package/i18n/locales/fr-CA.json +209 -125
  6. package/i18n/locales/fr-FR.json +209 -125
  7. package/i18n/locales/ko-KR.json +209 -125
  8. package/i18n/locales/pt-PT.json +212 -128
  9. package/i18n/locales/ru-RU.json +209 -125
  10. package/i18n/locales/vi-VN.json +209 -125
  11. package/js/api/joanie.ts +14 -17
  12. package/js/api/lms/dummy.ts +1 -12
  13. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  14. package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
  15. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  16. package/js/components/ContractFrame/_styles.scss +6 -14
  17. package/js/components/CreditCardSelector/index.spec.tsx +7 -7
  18. package/js/components/CreditCardSelector/index.tsx +2 -2
  19. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  20. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  21. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  22. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  23. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  24. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  25. package/js/components/PaymentInterfaces/types.ts +5 -2
  26. package/js/components/PurchaseButton/index.spec.tsx +69 -37
  27. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  28. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  29. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  30. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  31. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  32. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  33. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  34. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  35. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  36. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  37. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
  38. package/js/components/SaleTunnel/_styles.scss +10 -1
  39. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  40. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  41. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  42. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  43. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  44. package/js/components/SignContractButton/index.spec.tsx +16 -20
  45. package/js/components/SignContractButton/index.tsx +3 -1
  46. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  47. package/js/hooks/useCreditCards/index.ts +49 -11
  48. package/js/hooks/useOrders/index.spec.tsx +322 -0
  49. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  50. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  51. package/js/hooks/useProductOrder/index.tsx +2 -2
  52. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  53. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  54. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  55. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  56. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  57. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  58. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  59. package/js/settings/settings.test.ts +11 -2
  60. package/js/translations/ar-SA.json +1 -1
  61. package/js/translations/es-ES.json +1 -1
  62. package/js/translations/fa-IR.json +1 -1
  63. package/js/translations/fr-CA.json +1 -1
  64. package/js/translations/fr-FR.json +1 -1
  65. package/js/translations/ko-KR.json +1 -1
  66. package/js/translations/pt-PT.json +1 -1
  67. package/js/translations/ru-RU.json +1 -1
  68. package/js/translations/vi-VN.json +1 -1
  69. package/js/types/Joanie.ts +49 -34
  70. package/js/utils/OrderHelper/index.ts +38 -42
  71. package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
  72. package/js/utils/test/factories/joanie.ts +36 -51
  73. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  74. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  75. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  76. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  77. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  78. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  79. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  80. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  81. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +4 -6
  82. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  83. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  84. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  85. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  86. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  87. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  88. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  89. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  90. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  91. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  92. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  93. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  94. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  95. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  96. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  97. package/package.json +27 -27
  98. package/scss/components/_index.scss +2 -1
  99. package/js/components/PaymentButton/_styles.scss +0 -27
  100. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  101. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  102. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
@@ -18,7 +18,9 @@ import {
18
18
  Enrollment,
19
19
  CredentialOrder,
20
20
  OrderState,
21
- ACTIVE_ORDER_STATES,
21
+ PURCHASABLE_ORDER_STATES,
22
+ ENROLLABLE_ORDER_STATES,
23
+ NOT_CANCELED_ORDER_STATES,
22
24
  } from 'types/Joanie';
23
25
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
24
26
  import { Deferred } from 'utils/test/deferred';
@@ -176,7 +178,7 @@ describe('CourseProductItem', () => {
176
178
  expect(screen.queryByTestId('CertificateItem')).toBeNull();
177
179
  });
178
180
 
179
- it('renders product informations in compact mode', async () => {
181
+ it('renders product information in compact mode', async () => {
180
182
  const relation = CourseProductRelationFactory().one();
181
183
  fetchMock.get(
182
184
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
@@ -231,13 +233,130 @@ describe('CourseProductItem', () => {
231
233
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
232
234
  });
233
235
 
234
- it('renders product informations for a purchased product', async () => {
236
+ it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
237
+ 'renders product informations for %s order',
238
+ async (state) => {
239
+ const relation = CourseProductRelationFactory().one();
240
+ const { product } = relation;
241
+ const order = CredentialOrderFactory({
242
+ product_id: product.id,
243
+ course: PacedCourseFactory({ code: '00000' }).one(),
244
+ target_courses: product.target_courses,
245
+ state,
246
+ }).one();
247
+
248
+ fetchMock.get(
249
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
250
+ relation,
251
+ );
252
+ const orderQueryParameters = {
253
+ course_code: order.course.code,
254
+ product_id: order.product_id,
255
+ state: NOT_CANCELED_ORDER_STATES,
256
+ };
257
+ const queryParams = queryString.stringify(orderQueryParameters);
258
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
259
+ fetchMock.get(url, [order]);
260
+
261
+ render(
262
+ <CourseProductItem
263
+ course={PacedCourseFactory({ code: '00000' }).one()}
264
+ productId={relation.product.id}
265
+ />,
266
+ );
267
+
268
+ // In the header, we should display the product title, the product price
269
+ // and product date range and languages
270
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
271
+ // the price shouldn't be a heading to prevent misdirection for screen reader users,
272
+ // but we want to it to visually look like a h6
273
+
274
+ // - In place of product price, a label should be displayed
275
+ const $enrolledInfo = await screen.findByText('Purchased');
276
+ expect($enrolledInfo.tagName).toBe('STRONG');
277
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
278
+
279
+ // - Render all order's target courses information with CourseRunList component
280
+ await waitFor(() => {
281
+ order.target_courses.forEach((course) => {
282
+ const $item = screen.getByTestId(`course-item-${course.code}`);
283
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
284
+ // but we want to it to visually look like a h5
285
+ const $courseTitle = getByText($item, course.title);
286
+ expect($courseTitle.tagName).toBe('STRONG');
287
+ expect($courseTitle.classList.contains('h5')).toBe(true);
288
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
289
+ });
290
+ });
291
+
292
+ // - Does not render PurchaseButton cta
293
+ expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
294
+ },
295
+ );
296
+
297
+ it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
298
+ 'renders product informations for %s order in compact mode',
299
+ async (state) => {
300
+ const relation = CourseProductRelationFactory().one();
301
+ const { product } = relation;
302
+ const order = CredentialOrderFactory({
303
+ product_id: product.id,
304
+ course: PacedCourseFactory({ code: '00000' }).one(),
305
+ target_courses: product.target_courses,
306
+ state,
307
+ }).one();
308
+
309
+ fetchMock.get(
310
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
311
+ relation,
312
+ );
313
+ const orderQueryParameters = {
314
+ course_code: order.course.code,
315
+ product_id: order.product_id,
316
+ state: NOT_CANCELED_ORDER_STATES,
317
+ };
318
+ const queryParams = queryString.stringify(orderQueryParameters);
319
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
320
+ fetchMock.get(url, [order]);
321
+
322
+ render(
323
+ <CourseProductItem
324
+ course={PacedCourseFactory({ code: '00000' }).one()}
325
+ productId={relation.product.id}
326
+ compact
327
+ />,
328
+ );
329
+
330
+ // In the header, we should display the product title, the product price
331
+ // and product date range and languages
332
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
333
+ // the price shouldn't be a heading to prevent misdirection for screen reader users,
334
+ // but we want to it to visually look like a h6
335
+
336
+ // - In place of product price, a label should be displayed
337
+ const $enrolledInfo = await screen.findByText('Purchased');
338
+ expect($enrolledInfo.tagName).toBe('STRONG');
339
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
340
+
341
+ // - Any target courses information should be displayed
342
+ relation.product.target_courses.forEach((course) => {
343
+ const $item = screen.queryByTestId(`course-item-${course.code}`);
344
+ expect($item).not.toBeInTheDocument();
345
+ });
346
+
347
+ // - Does not render PurchaseButton cta
348
+ expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
349
+ },
350
+ );
351
+
352
+ it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
235
353
  const relation = CourseProductRelationFactory().one();
236
354
  const { product } = relation;
237
355
  const order = CredentialOrderFactory({
238
356
  product_id: product.id,
239
357
  course: PacedCourseFactory({ code: '00000' }).one(),
240
358
  target_courses: product.target_courses,
359
+ state,
241
360
  }).one();
242
361
 
243
362
  fetchMock.get(
@@ -245,14 +364,13 @@ describe('CourseProductItem', () => {
245
364
  relation,
246
365
  );
247
366
  const orderQueryParameters = {
248
- product_id: order.product_id,
249
367
  course_code: order.course.code,
250
- state: ACTIVE_ORDER_STATES,
368
+ product_id: order.product_id,
369
+ state: NOT_CANCELED_ORDER_STATES,
251
370
  };
252
- fetchMock.get(
253
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
254
- [order],
255
- );
371
+ const queryParams = queryString.stringify(orderQueryParameters);
372
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
373
+ fetchMock.get(url, [order]);
256
374
 
257
375
  render(
258
376
  <CourseProductItem
@@ -291,71 +409,75 @@ describe('CourseProductItem', () => {
291
409
  expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
292
410
  });
293
411
 
294
- it('renders product informations for a purchased product in compact mode', async () => {
295
- const relation = CourseProductRelationFactory().one();
296
- const order: CredentialOrder = CredentialOrderFactory({
297
- product_id: relation.product.id,
298
- course: PacedCourseFactory({ code: '00000' }).one(),
299
- target_courses: relation.product.target_courses,
300
- }).one();
412
+ it.each(ENROLLABLE_ORDER_STATES)(
413
+ 'renders product informations for a %s order in compact mode',
414
+ async (state) => {
415
+ const relation = CourseProductRelationFactory().one();
416
+ const order: CredentialOrder = CredentialOrderFactory({
417
+ product_id: relation.product.id,
418
+ course: PacedCourseFactory({ code: '00000' }).one(),
419
+ target_courses: relation.product.target_courses,
420
+ state,
421
+ }).one();
301
422
 
302
- fetchMock.get(
303
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
304
- relation,
305
- );
306
- const orderQueryParameters = {
307
- product_id: order.product_id,
308
- course_code: order.course?.code,
309
- state: ACTIVE_ORDER_STATES,
310
- };
311
- fetchMock.get(
312
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
313
- [order],
314
- );
423
+ fetchMock.get(
424
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
425
+ relation,
426
+ );
427
+ const orderQueryParameters = {
428
+ product_id: order.product_id,
429
+ course_code: order.course?.code,
430
+ state: NOT_CANCELED_ORDER_STATES,
431
+ };
432
+ fetchMock.get(
433
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
434
+ [order],
435
+ );
315
436
 
316
- render(
317
- <CourseProductItem
318
- productId={relation.product.id}
319
- course={PacedCourseFactory({ code: '00000' }).one()}
320
- compact
321
- />,
322
- );
437
+ render(
438
+ <CourseProductItem
439
+ productId={relation.product.id}
440
+ course={PacedCourseFactory({ code: '00000' }).one()}
441
+ compact
442
+ />,
443
+ );
323
444
 
324
- // Wait for product information to be fetched
325
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
445
+ // Wait for product information to be fetched
446
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
326
447
 
327
- // - In place of product price, a label should be displayed
328
- const $enrolledInfo = await screen.findByText('Purchased');
329
- expect($enrolledInfo.tagName).toBe('STRONG');
330
- expect($enrolledInfo.classList.contains('h6')).toBe(true);
448
+ // - In place of product price, a label should be displayed
449
+ const $enrolledInfo = await screen.findByText('Purchased');
450
+ expect($enrolledInfo.tagName).toBe('STRONG');
451
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
331
452
 
332
- // - Product date range and languages should not be displayed anymore
333
- expect(screen.queryByTestId('product-widget__header-metadata-dates')).not.toBeInTheDocument();
334
- expect(
335
- screen.queryByTestId('product-widget__header-metadata-languages'),
336
- ).not.toBeInTheDocument();
453
+ // - Product date range and languages should not be displayed anymore
454
+ expect(screen.queryByTestId('product-widget__header-metadata-dates')).not.toBeInTheDocument();
455
+ expect(
456
+ screen.queryByTestId('product-widget__header-metadata-languages'),
457
+ ).not.toBeInTheDocument();
337
458
 
338
- // - Render all order's target courses information with EnrollableCourseRunList component
339
- await waitFor(() => {
340
- order.target_courses.forEach((course) => {
341
- const $item = screen.getByTestId(`course-item-${course.code}`);
342
- // the course title shouldn't be a heading to prevent misdirection for screen reader users,
343
- // but we want to it to visually look like a h5
344
- const $courseTitle = getByText($item, course.title);
345
- expect($courseTitle.tagName).toBe('STRONG');
346
- expect($courseTitle.classList.contains('h5')).toBe(true);
347
- screen.getByTestId(
348
- `EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
349
- );
459
+ // - Render all order's target courses information with EnrollableCourseRunList component
460
+ await waitFor(() => {
461
+ order.target_courses.forEach((course) => {
462
+ const $item = screen.getByTestId(`course-item-${course.code}`);
463
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
464
+ // but we want to it to visually look like a h5
465
+ const $courseTitle = getByText($item, course.title);
466
+ expect($courseTitle.tagName).toBe('STRONG');
467
+ expect($courseTitle.classList.contains('h5')).toBe(true);
468
+ screen.getByTestId(
469
+ `EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
470
+ );
471
+ });
350
472
  });
351
- });
352
473
 
353
- // - Render <CertificateItem />
354
- screen.getByTestId('CertificateItem');
474
+ // - Render <CertificateItem />
475
+ screen.getByTestId('CertificateItem');
355
476
 
356
- // - Does not Render PurchaseButton cta
357
- expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
358
- });
477
+ // - Does not Render PurchaseButton cta
478
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
479
+ },
480
+ );
359
481
 
360
482
  it('renders enrollment information when user is enrolled to a course run', async () => {
361
483
  const relation = CourseProductRelationFactory().one();
@@ -378,7 +500,7 @@ describe('CourseProductItem', () => {
378
500
  const orderQueryParameters = {
379
501
  product_id: order.product_id,
380
502
  course_code: order.course?.code,
381
- state: ACTIVE_ORDER_STATES,
503
+ state: NOT_CANCELED_ORDER_STATES,
382
504
  };
383
505
  fetchMock.get(
384
506
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
@@ -422,188 +544,16 @@ describe('CourseProductItem', () => {
422
544
  });
423
545
  });
424
546
 
425
- it('renders sale tunnel button if user already has a pending order', async () => {
426
- const relation = CourseProductRelationFactory().one();
427
- const { product } = relation;
428
- const order = CredentialOrderFactory({
429
- product_id: product.id,
430
- course: PacedCourseFactory({ code: '00000' }).one(),
431
- target_courses: product.target_courses,
432
- state: OrderState.PENDING,
433
- }).one();
434
- fetchMock.get(
435
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
436
- relation,
437
- );
438
- const orderQueryParameters = {
439
- product_id: order.product_id,
440
- course_code: order.course?.code,
441
- state: ACTIVE_ORDER_STATES,
442
- };
443
- fetchMock.get(
444
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
445
- [order],
446
- );
447
-
448
- render(
449
- <CourseProductItem
450
- productId={product.id}
451
- course={PacedCourseFactory({ code: '00000' }).one()}
452
- />,
453
- );
454
-
455
- // Wait for product information to be fetched
456
- await screen.findByRole('heading', { level: 3, name: product.title });
457
-
458
- const $price = screen.getByText(
459
- // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
460
- // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
461
- priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
462
- );
463
- expect($price.tagName).toBe('STRONG');
464
- expect($price.classList.contains('h6')).toBe(true);
465
-
466
- // - Render all target courses information
467
- relation.product.target_courses.forEach((course) => {
468
- const $item = screen.getByTestId(`course-item-${course.code}`);
469
- // the course title shouldn't be a heading to prevent misdirection for screen reader users,
470
- // but we want to it to visually look like a h5
471
- const $courseTitle = getByText($item, course.title);
472
- expect($courseTitle.tagName).toBe('STRONG');
473
- expect($courseTitle.classList.contains('h5')).toBe(true);
474
- screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
475
- });
476
-
477
- screen.getByRole('button', { name: product.call_to_action });
478
- });
479
-
480
- it('renders sale tunnel button if user already has a canceled order', async () => {
481
- const relation = CourseProductRelationFactory().one();
482
- const { product } = relation;
483
- fetchMock.get(
484
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
485
- relation,
486
- );
487
- const orderQueryParameters = {
488
- product_id: product.id,
489
- course_code: '00000',
490
- state: ACTIVE_ORDER_STATES,
491
- };
492
- fetchMock.get(
493
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
494
- [],
495
- );
496
-
497
- render(
498
- <CourseProductItem
499
- productId={product.id}
500
- course={PacedCourseFactory({ code: '00000' }).one()}
501
- />,
502
- );
503
-
504
- // Wait for product information to be fetched
505
- await screen.findByRole('heading', { level: 3, name: product.title });
506
-
507
- const $price = screen.getByText(
508
- // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
509
- // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
510
- priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
511
- );
512
- expect($price.tagName).toBe('STRONG');
513
- expect($price.classList.contains('h6')).toBe(true);
514
-
515
- // - Render all target courses information
516
- relation.product.target_courses.forEach((course) => {
517
- const $item = screen.getByTestId(`course-item-${course.code}`);
518
- // the course title shouldn't be a heading to prevent misdirection for screen reader users,
519
- // but we want to it to visually look like a h5
520
- const $courseTitle = getByText($item, course.title);
521
- expect($courseTitle.tagName).toBe('STRONG');
522
- expect($courseTitle.classList.contains('h5')).toBe(true);
523
- screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
524
- });
525
-
526
- screen.getByRole('button', { name: product.call_to_action });
527
- });
528
-
529
- it('does not render sale tunnel button if user already has a submitted order', async () => {
530
- const relation = CourseProductRelationFactory().one();
531
- const { product } = relation;
532
- const order = CredentialOrderFactory({
533
- product_id: product.id,
534
- course: PacedCourseFactory({ code: '00000' }).one(),
535
- target_courses: product.target_courses,
536
- state: OrderState.SUBMITTED,
537
- }).one();
538
- fetchMock.get(
539
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
540
- relation,
541
- );
542
- const orderQueryParameters = {
543
- product_id: order.product_id,
544
- course_code: order.course?.code,
545
- state: ACTIVE_ORDER_STATES,
546
- };
547
- fetchMock.get(
548
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
549
- [order],
550
- );
551
-
552
- render(
553
- <CourseProductItem
554
- productId={product.id}
555
- course={PacedCourseFactory({ code: '00000' }).one()}
556
- />,
557
- );
558
-
559
- // Wait for product information to be fetched
560
- await screen.findByRole('heading', { level: 3, name: product.title });
561
-
562
- // - In place of product price, a label "Pending" should be displayed
563
- const $enrolledInfo = await screen.findByText('Pending');
564
- expect($enrolledInfo.tagName).toBe('STRONG');
565
- expect($enrolledInfo.classList.contains('h6')).toBe(true);
566
-
567
- // - As order is pending, the user should not be able to enroll to course runs.
568
- await waitFor(() => {
569
- order.target_courses.forEach((course) => {
570
- const $item = screen.getByTestId(`course-item-${course.code}`);
571
- // the course title shouldn't be a heading to prevent misdirection for screen reader users,
572
- // but we want to it to visually look like a h5
573
- const $courseTitle = getByText($item, course.title);
574
- expect($courseTitle.tagName).toBe('STRONG');
575
- expect($courseTitle.classList.contains('h5')).toBe(true);
576
- screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
577
- });
578
- });
579
-
580
- // - Render <CertificateItem />
581
- screen.getByTestId('CertificateItem');
582
-
583
- // - Does not Render PurchaseButton cta
584
- expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
585
- });
586
-
587
- it.each([
588
- {
589
- orderState: OrderState.PENDING,
590
- },
591
- {
592
- orderState: OrderState.SUBMITTED,
593
- },
594
- {
595
- orderState: OrderState.DRAFT,
596
- },
597
- ])(
598
- "should not render sign button and banner for order's state $orderState",
599
- async ({ orderState }) => {
547
+ it.each(PURCHASABLE_ORDER_STATES)(
548
+ 'renders sale tunnel button if user already has a %s order',
549
+ async (state) => {
600
550
  const relation = CourseProductRelationFactory().one();
601
551
  const { product } = relation;
602
552
  const order = CredentialOrderFactory({
603
553
  product_id: product.id,
604
- target_courses: product.target_courses,
605
554
  course: PacedCourseFactory({ code: '00000' }).one(),
606
- state: orderState,
555
+ target_courses: product.target_courses,
556
+ state,
607
557
  }).one();
608
558
  fetchMock.get(
609
559
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -611,10 +561,9 @@ describe('CourseProductItem', () => {
611
561
  );
612
562
  const orderQueryParameters = {
613
563
  product_id: order.product_id,
614
- course_code: order.course.code,
615
- state: ACTIVE_ORDER_STATES,
564
+ course_code: order.course?.code,
565
+ state: NOT_CANCELED_ORDER_STATES,
616
566
  };
617
-
618
567
  fetchMock.get(
619
568
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
620
569
  [order],
@@ -628,45 +577,46 @@ describe('CourseProductItem', () => {
628
577
  );
629
578
 
630
579
  // Wait for product information to be fetched
631
- expect(
632
- await screen.findByRole('heading', { level: 3, name: product.title }),
633
- ).toBeInTheDocument();
580
+ await screen.findByRole('heading', { level: 3, name: product.title });
634
581
 
635
- // - A banner should be displayed.
636
- expect(
637
- screen.queryByText(
638
- 'You need to sign your training contract before enrolling to course runs',
639
- ),
640
- ).not.toBeInTheDocument();
582
+ const $price = screen.getByText(
583
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
584
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
585
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
586
+ );
587
+ expect($price.tagName).toBe('STRONG');
588
+ expect($price.classList.contains('h6')).toBe(true);
641
589
 
642
- expect(
643
- screen.queryByRole('link', { name: 'Sign your training contract' }),
644
- ).not.toBeInTheDocument();
590
+ // - Render all target courses information
591
+ relation.product.target_courses.forEach((course) => {
592
+ const $item = screen.getByTestId(`course-item-${course.code}`);
593
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
594
+ // but we want to it to visually look like a h5
595
+ const $courseTitle = getByText($item, course.title);
596
+ expect($courseTitle.tagName).toBe('STRONG');
597
+ expect($courseTitle.classList.contains('h5')).toBe(true);
598
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
599
+ });
600
+
601
+ screen.getByRole('button', { name: product.call_to_action });
645
602
  },
646
603
  );
647
604
 
648
- it('renders a button and a banner if the contract needs to be signed', async () => {
605
+ it('renders sale tunnel button if user already has a canceled order', async () => {
649
606
  const relation = CourseProductRelationFactory().one();
650
607
  const { product } = relation;
651
- const order = CredentialOrderFactory({
652
- product_id: product.id,
653
- target_courses: product.target_courses,
654
- course: PacedCourseFactory({ code: '00000' }).one(),
655
- state: OrderState.VALIDATED,
656
- }).one();
657
608
  fetchMock.get(
658
609
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
659
610
  relation,
660
611
  );
661
612
  const orderQueryParameters = {
662
- product_id: order.product_id,
663
- course_code: order.course.code,
664
- state: ACTIVE_ORDER_STATES,
613
+ product_id: product.id,
614
+ course_code: '00000',
615
+ state: NOT_CANCELED_ORDER_STATES,
665
616
  };
666
-
667
617
  fetchMock.get(
668
618
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
669
- [order],
619
+ [],
670
620
  );
671
621
 
672
622
  render(
@@ -679,66 +629,26 @@ describe('CourseProductItem', () => {
679
629
  // Wait for product information to be fetched
680
630
  await screen.findByRole('heading', { level: 3, name: product.title });
681
631
 
682
- // - A banner should be displayed.
683
- screen.getByText('You need to sign your training contract before enrolling to course runs');
684
- screen.getByRole('link', { name: 'Sign your training contract' });
685
- });
686
-
687
- it('adapts layout when user has a pending order and compact prop is set', async () => {
688
- const relation = CourseProductRelationFactory().one();
689
- const order: CredentialOrder = CredentialOrderFactory({
690
- product_id: relation.product.id,
691
- course: PacedCourseFactory({ code: '00000' }).one(),
692
- target_courses: relation.product.target_courses,
693
- state: OrderState.SUBMITTED,
694
- }).one();
695
- fetchMock.get(
696
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
697
- relation,
698
- );
699
- const orderQueryParameters = {
700
- product_id: order.product_id,
701
- course_code: order.course?.code,
702
- state: ACTIVE_ORDER_STATES,
703
- };
704
- fetchMock.get(
705
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
706
- [order],
707
- );
708
-
709
- render(
710
- <CourseProductItem
711
- productId={relation.product.id}
712
- course={PacedCourseFactory({ code: '00000' }).one()}
713
- compact={true}
714
- />,
632
+ const $price = screen.getByText(
633
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
634
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
635
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
715
636
  );
637
+ expect($price.tagName).toBe('STRONG');
638
+ expect($price.classList.contains('h6')).toBe(true);
716
639
 
717
- // Wait for product information to be fetched
718
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
719
-
720
- // - In place of product price, a label should be displayed
721
- const $enrolledInfo = await screen.findByText('Pending');
722
- expect($enrolledInfo.tagName).toBe('STRONG');
723
- expect($enrolledInfo.classList.contains('h6')).toBe(true);
724
-
725
- // - Product date range and languages should be displayed
726
- screen.getByTestId('product-widget__header-metadata-dates');
727
- screen.getByTestId('product-widget__header-metadata-languages');
728
-
729
- // - Target courses should not be rendered
730
- await waitFor(() => {
731
- order.target_courses.forEach((course) => {
732
- const $item = screen.queryByTestId(`course-item-${course.code}`);
733
- expect($item).not.toBeInTheDocument();
734
- });
640
+ // - Render all target courses information
641
+ relation.product.target_courses.forEach((course) => {
642
+ const $item = screen.getByTestId(`course-item-${course.code}`);
643
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
644
+ // but we want to it to visually look like a h5
645
+ const $courseTitle = getByText($item, course.title);
646
+ expect($courseTitle.tagName).toBe('STRONG');
647
+ expect($courseTitle.classList.contains('h5')).toBe(true);
648
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
735
649
  });
736
650
 
737
- // - <CertificateItem /> should not be rendered
738
- expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
739
-
740
- // - Does not Render PurchaseButton cta
741
- expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
651
+ screen.getByRole('button', { name: product.call_to_action });
742
652
  });
743
653
 
744
654
  it('renders error message when product fetching has failed', async () => {
@@ -771,7 +681,7 @@ describe('CourseProductItem', () => {
771
681
  product_id: product.id,
772
682
  course: PacedCourseFactory({ code: '00000' }).one(),
773
683
  target_courses: product.target_courses,
774
- state: OrderState.PENDING,
684
+ state: OrderState.DRAFT,
775
685
  }).one();
776
686
  fetchMock.get(
777
687
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -780,7 +690,7 @@ describe('CourseProductItem', () => {
780
690
  const orderQueryParameters = {
781
691
  product_id: order.product_id,
782
692
  course_code: order.course?.code,
783
- state: ACTIVE_ORDER_STATES,
693
+ state: NOT_CANCELED_ORDER_STATES,
784
694
  };
785
695
  fetchMock.get(
786
696
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
@@ -810,7 +720,7 @@ describe('CourseProductItem', () => {
810
720
  product_id: product.id,
811
721
  course: PacedCourseFactory({ code: '00000' }).one(),
812
722
  target_courses: product.target_courses,
813
- state: OrderState.PENDING,
723
+ state: OrderState.DRAFT,
814
724
  }).one();
815
725
  fetchMock.get(
816
726
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -819,7 +729,7 @@ describe('CourseProductItem', () => {
819
729
  const orderQueryParameters = {
820
730
  product_id: order.product_id,
821
731
  course_code: order.course?.code,
822
- state: ACTIVE_ORDER_STATES,
732
+ state: NOT_CANCELED_ORDER_STATES,
823
733
  };
824
734
  fetchMock.get(
825
735
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
@@ -850,7 +760,7 @@ describe('CourseProductItem', () => {
850
760
  product_id: product.id,
851
761
  course: PacedCourseFactory({ code: '00000' }).one(),
852
762
  target_courses: product.target_courses,
853
- state: OrderState.PENDING,
763
+ state: OrderState.DRAFT,
854
764
  }).one();
855
765
  fetchMock.get(
856
766
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -859,7 +769,7 @@ describe('CourseProductItem', () => {
859
769
  const orderQueryParameters = {
860
770
  product_id: order.product_id,
861
771
  course_code: order.course?.code,
862
- state: ACTIVE_ORDER_STATES,
772
+ state: NOT_CANCELED_ORDER_STATES,
863
773
  };
864
774
  fetchMock.get(
865
775
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,