richie-education 2.24.0 → 2.25.0-b2.dev101

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 (492) hide show
  1. package/.eslintignore +2 -0
  2. package/.nvmrc +1 -1
  3. package/.prettierignore +2 -0
  4. package/.storybook/preview-body.html +1 -1
  5. package/.storybook/preview.tsx +5 -2
  6. package/cunningham.cjs +52 -43
  7. package/i18n/locales/ar-SA.json +500 -100
  8. package/i18n/locales/es-ES.json +500 -100
  9. package/i18n/locales/fa-IR.json +500 -100
  10. package/i18n/locales/fr-CA.json +564 -164
  11. package/i18n/locales/fr-FR.json +517 -117
  12. package/i18n/locales/ko-KR.json +500 -100
  13. package/i18n/locales/pt-PT.json +526 -126
  14. package/i18n/locales/ru-RU.json +500 -100
  15. package/i18n/locales/vi-VN.json +1734 -0
  16. package/jest/setup.ts +11 -1
  17. package/js/api/enrollment.ts +1 -1
  18. package/js/api/joanie.spec.ts +63 -2
  19. package/js/api/joanie.ts +218 -141
  20. package/js/api/lms/dummy.spec.ts +9 -1
  21. package/js/api/lms/dummy.ts +63 -10
  22. package/js/api/lms/joanie.spec.ts +49 -31
  23. package/js/api/lms/joanie.ts +53 -35
  24. package/js/api/lms/openedx-hawthorn.spec.ts +27 -11
  25. package/js/api/lms/openedx-hawthorn.ts +7 -6
  26. package/js/components/AddressesManagement/AddressForm/index.spec.tsx +157 -0
  27. package/js/components/AddressesManagement/AddressForm/index.stories.tsx +36 -0
  28. package/js/components/AddressesManagement/AddressForm/index.tsx +163 -0
  29. package/js/components/AddressesManagement/{validationSchema.ts → AddressForm/validationSchema.ts} +1 -23
  30. package/js/components/AddressesManagement/_styles.scss +1 -1
  31. package/js/components/AddressesManagement/index.spec.tsx +171 -202
  32. package/js/components/AddressesManagement/index.stories.tsx +29 -0
  33. package/js/components/AddressesManagement/index.tsx +11 -3
  34. package/js/components/Badge/index.spec.tsx +17 -0
  35. package/js/components/Badge/index.stories.tsx +22 -0
  36. package/js/components/Badge/index.tsx +18 -0
  37. package/js/components/Banner/index.tsx +6 -1
  38. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +332 -0
  39. package/js/components/ContractFrame/AbstractContractFrame.tsx +289 -0
  40. package/js/components/ContractFrame/LearnerContractFrame.spec.tsx +125 -0
  41. package/js/components/ContractFrame/LearnerContractFrame.tsx +42 -0
  42. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +167 -0
  43. package/js/components/ContractFrame/OrganizationContractFrame.tsx +70 -0
  44. package/js/components/ContractFrame/_styles.scss +62 -0
  45. package/js/components/ContractFrame/iframe-manager.js +158 -0
  46. package/js/components/ContractFrame/index.ts +5 -0
  47. package/js/components/ContractStatus/index.spec.tsx +120 -0
  48. package/js/components/ContractStatus/index.tsx +67 -0
  49. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +7 -7
  50. package/js/components/CourseGlimpse/index.tsx +5 -1
  51. package/js/components/CourseGlimpse/utils.ts +24 -16
  52. package/js/components/CourseGlimpseList/index.spec.tsx +1 -1
  53. package/js/components/CourseGlimpseList/index.tsx +1 -1
  54. package/js/components/CourseGlimpseList/utils.ts +3 -2
  55. package/js/components/DjangoCMSTemplate/index.spec.tsx +2 -2
  56. package/js/components/DownloadCertificateButton/index.tsx +58 -0
  57. package/js/components/DownloadContractButton/index.spec.tsx +155 -0
  58. package/js/components/DownloadContractButton/index.tsx +48 -0
  59. package/js/components/Form/CountrySelectField.tsx +28 -16
  60. package/js/components/Form/Input/index.spec.tsx +76 -0
  61. package/js/components/Form/Input/index.tsx +47 -0
  62. package/js/components/Form/Select/index.spec.tsx +99 -0
  63. package/js/components/Form/Select/index.tsx +43 -0
  64. package/js/components/{AddressesManagement → Form}/ValidationErrors.ts +10 -5
  65. package/js/components/Form/index.ts +5 -1
  66. package/js/components/Form/messages.ts +14 -0
  67. package/js/components/Form/test-utils.ts +19 -0
  68. package/js/components/Form/utils.spec.ts +72 -0
  69. package/js/components/Form/utils.ts +37 -0
  70. package/js/components/Icon/index.stories.tsx +2 -1
  71. package/js/components/Modal/_styles.scss +0 -8
  72. package/js/components/Modal/index.spec.tsx +0 -6
  73. package/js/components/Modal/index.tsx +23 -17
  74. package/js/components/PaymentButton/_styles.scss +26 -0
  75. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/Dummy.tsx +1 -1
  76. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/PayplugLightbox.tsx +30 -7
  77. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/__mocks__/index.tsx +1 -1
  78. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/index.spec.tsx +5 -3
  79. package/js/{widgets/CourseProductItem → components/PaymentButton}/components/PaymentInterfaces/index.tsx +7 -5
  80. package/js/components/PaymentButton/hooks/useTerms.tsx +74 -0
  81. package/js/components/PaymentButton/index.spec.tsx +1038 -0
  82. package/js/{widgets/CourseProductItem/components → components}/PaymentButton/index.tsx +94 -41
  83. package/js/components/PurchaseButton/index.spec.tsx +377 -0
  84. package/js/components/PurchaseButton/index.stories.tsx +15 -0
  85. package/js/{widgets/CourseProductItem/components → components}/PurchaseButton/index.tsx +72 -23
  86. package/js/components/PurchaseButton/styles.scss +7 -0
  87. package/js/components/RegisteredAddress/_styles.scss +1 -3
  88. package/js/components/RegisteredAddress/index.spec.tsx +1 -1
  89. package/js/components/RegisteredAddress/index.stories.tsx +40 -0
  90. package/js/components/RegisteredAddress/index.tsx +17 -19
  91. package/js/components/SaleTunnel/_styles.scss +11 -0
  92. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/index.tsx +4 -10
  93. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/_styles.scss +7 -0
  94. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/index.spec.tsx +85 -61
  95. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepPayment/index.tsx +55 -57
  96. package/js/components/SaleTunnel/components/SaleTunnelStepResume/_styles.scss +63 -0
  97. package/js/components/SaleTunnel/components/SaleTunnelStepResume/index.spec.tsx +80 -0
  98. package/js/components/SaleTunnel/components/SaleTunnelStepResume/index.tsx +88 -0
  99. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/CourseRunsList.tsx +6 -2
  100. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/_styles.scss +5 -0
  101. package/js/components/SaleTunnel/components/SaleTunnelStepValidation/index.spec.tsx +170 -0
  102. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/index.tsx +41 -10
  103. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/index.spec.tsx +1 -1
  104. package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/index.tsx +1 -1
  105. package/js/components/SaleTunnel/context.tsx +44 -0
  106. package/js/{widgets/CourseProductItem/components → components}/SaleTunnel/index.spec.tsx +27 -22
  107. package/js/{widgets/CourseProductItem/components → components}/SaleTunnel/index.tsx +96 -24
  108. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +135 -0
  109. package/js/components/SignContractButton/index.spec.tsx +213 -0
  110. package/js/components/SignContractButton/index.tsx +97 -0
  111. package/js/components/SuccessIcon/_styles.scss +66 -0
  112. package/js/components/SuccessIcon/index.tsx +10 -0
  113. package/js/components/TeacherDashboardCourseList/_styles.scss +0 -1
  114. package/js/components/TeacherDashboardCourseList/index.spec.tsx +9 -9
  115. package/js/components/TeacherDashboardCourseList/index.tsx +25 -31
  116. package/js/contexts/JoanieApiContext/index.spec.tsx +1 -1
  117. package/js/contexts/SessionContext/BaseSessionProvider.tsx +12 -22
  118. package/js/contexts/SessionContext/JoanieSessionProvider.spec.tsx +14 -0
  119. package/js/contexts/SessionContext/JoanieSessionProvider.tsx +33 -34
  120. package/js/contexts/SessionContext/index.spec.tsx +6 -7
  121. package/js/hooks/useBreadcrumbsPlaceholders.tsx +1 -1
  122. package/js/hooks/useContractAbilities/index.spec.ts +27 -0
  123. package/js/hooks/useContractAbilities/index.ts +8 -0
  124. package/js/hooks/useContractArchive/index.download.spec.tsx +126 -0
  125. package/js/hooks/useContractArchive/index.spec.tsx +91 -0
  126. package/js/hooks/useContractArchive/index.ts +64 -0
  127. package/js/hooks/useContracts/index.tsx +68 -0
  128. package/js/hooks/useCourseProductRelation/index.ts +8 -5
  129. package/js/hooks/useCourseProductUnion/index.spec.tsx +14 -10
  130. package/js/hooks/useCourseProductUnion/index.ts +6 -2
  131. package/js/hooks/useCourseProducts.ts +45 -0
  132. package/js/hooks/useCourseSearchParams/computeNewFilterValue.ts +3 -3
  133. package/js/hooks/useCourses/index.spec.tsx +2 -2
  134. package/js/hooks/useCourses/index.ts +4 -4
  135. package/js/hooks/useCreditCards/index.spec.tsx +4 -4
  136. package/js/hooks/useDashboardAddressForm.tsx +85 -87
  137. package/js/hooks/useDownloadCertificate/index.spec.tsx +19 -6
  138. package/js/hooks/useDownloadCertificate/index.tsx +2 -20
  139. package/js/hooks/useEnrollments.ts +1 -1
  140. package/js/hooks/useJoanieUserAbilities/index.not.isJoanieEnabled.spec.tsx +17 -0
  141. package/js/hooks/useJoanieUserAbilities/index.spec.tsx +68 -0
  142. package/js/hooks/useJoanieUserAbilities/index.tsx +11 -0
  143. package/js/hooks/useJoanieUserProfile.tsx +34 -0
  144. package/js/hooks/useOrders.ts +69 -26
  145. package/js/hooks/useOrganizations/index.ts +1 -1
  146. package/js/hooks/useProductOrder/index.spec.tsx +113 -0
  147. package/js/hooks/useProductOrder/index.tsx +33 -0
  148. package/js/hooks/useQueryKeyInvalidateListener.tsx +16 -0
  149. package/js/hooks/useResources/index.spec.tsx +30 -29
  150. package/js/hooks/useResources/index.tsx +11 -4
  151. package/js/hooks/useResources/useResourcesOmniscient.ts +2 -2
  152. package/js/hooks/useResources/useResourcesRoot.ts +21 -17
  153. package/js/hooks/useTeacherPendingContractsCount/index.ts +34 -0
  154. package/js/hooks/useUnionResource/index.spec.tsx +5 -2
  155. package/js/hooks/useUnionResource/index.ts +20 -3
  156. package/js/hooks/useUnionResource/utils/fetchEntity.ts +5 -4
  157. package/js/index.tsx +2 -2
  158. package/js/pages/DashboardAddressesManagement/DashboardAddressBox.tsx +1 -1
  159. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.spec.tsx +79 -72
  160. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.tsx +4 -4
  161. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.spec.tsx +32 -23
  162. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.tsx +6 -6
  163. package/js/pages/DashboardAddressesManagement/DashboardEditAddressLoader.tsx +2 -2
  164. package/js/pages/DashboardAddressesManagement/index.spec.tsx +4 -3
  165. package/js/pages/DashboardAddressesManagement/index.tsx +5 -5
  166. package/js/pages/DashboardCertificates/index.spec.tsx +3 -2
  167. package/js/pages/DashboardCertificates/index.tsx +2 -1
  168. package/js/pages/DashboardContracts/_styles.scss +8 -0
  169. package/js/pages/DashboardContracts/index.spec.tsx +147 -0
  170. package/js/pages/DashboardContracts/index.tsx +76 -0
  171. package/js/pages/DashboardCourses/index.spec.tsx +81 -61
  172. package/js/pages/DashboardCourses/index.tsx +15 -12
  173. package/js/pages/DashboardCourses/useOrdersEnrollments.tsx +34 -8
  174. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +1 -1
  175. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +7 -6
  176. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +43 -45
  177. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCardLoader.tsx +2 -2
  178. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +13 -12
  179. package/js/pages/DashboardCreditCardsManagement/index.tsx +3 -3
  180. package/js/pages/DashboardOrderLayout/_styles.scss +5 -0
  181. package/js/pages/DashboardOrderLayout/index.spec.tsx +8 -8
  182. package/js/pages/DashboardOrderLayout/index.tsx +11 -6
  183. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +358 -0
  184. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +129 -0
  185. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardCourseContractsLayout/index.tsx +26 -0
  186. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardOrganizationContractsLayout/index.tsx +26 -0
  187. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +136 -0
  188. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +144 -0
  189. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +73 -0
  190. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +185 -0
  191. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +47 -0
  192. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.spec.tsx +179 -0
  193. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.tsx +86 -0
  194. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +109 -0
  195. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +60 -0
  196. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +124 -0
  197. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +73 -0
  198. package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.spec.tsx +134 -0
  199. package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.tsx +28 -0
  200. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +85 -0
  201. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +50 -0
  202. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +266 -0
  203. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +153 -0
  204. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.spec.tsx +100 -0
  205. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +27 -0
  206. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +193 -0
  207. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +44 -0
  208. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +32 -0
  209. package/js/pages/TeacherDashboardContractsLayout/index.ts +2 -0
  210. package/js/pages/TeacherDashboardContractsLayout/styles.scss +15 -0
  211. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/CourseRunListCell/index.spec.tsx +1 -1
  212. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/_styles.scss +1 -2
  213. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/index.spec.tsx +2 -2
  214. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/utils.spec.tsx +1 -1
  215. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/utils.tsx +11 -10
  216. package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/index.tsx +9 -8
  217. package/js/pages/{TeacherCoursesDashboardLoader → TeacherDashboardCoursesLoader}/index.spec.tsx +17 -17
  218. package/js/pages/{TeacherCoursesDashboardLoader → TeacherDashboardCoursesLoader}/index.tsx +7 -7
  219. package/js/pages/{TeacherOrganizationCourseDashboardLoader → TeacherDashboardOrganizationCourseLoader}/index.tsx +5 -5
  220. package/js/pages/{TeacherTrainingDashboard/TeacherTrainingDashboardLoader.tsx → TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx} +10 -9
  221. package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/index.spec.tsx +76 -6
  222. package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/index.tsx +6 -6
  223. package/js/settings.dev.dist.ts +3 -0
  224. package/js/settings.ts +29 -1
  225. package/js/translations/ar-SA.json +1 -1
  226. package/js/translations/es-ES.json +1 -1
  227. package/js/translations/fa-IR.json +1 -1
  228. package/js/translations/fr-CA.json +1 -1
  229. package/js/translations/fr-FR.json +1 -1
  230. package/js/translations/ko-KR.json +1 -1
  231. package/js/translations/pt-PT.json +1 -1
  232. package/js/translations/ru-RU.json +1 -1
  233. package/js/translations/vi-VN.json +1 -0
  234. package/js/types/Joanie.ts +263 -80
  235. package/js/types/Suggestion.ts +2 -2
  236. package/js/types/User.ts +19 -1
  237. package/js/types/commonDataProps.ts +3 -0
  238. package/js/types/index.ts +1 -1
  239. package/js/utils/AbilitiesHelper/contractAbilities.spec.ts +35 -0
  240. package/js/utils/AbilitiesHelper/contractAbilities.ts +14 -0
  241. package/js/utils/AbilitiesHelper/index.ts +71 -0
  242. package/js/utils/AbilitiesHelper/joanieUserProfileAbilities.spec.ts +55 -0
  243. package/js/utils/AbilitiesHelper/joanieUserProfileAbilities.ts +16 -0
  244. package/js/utils/AbilitiesHelper/types.ts +36 -0
  245. package/js/utils/ContractHelper/index.spec.ts +73 -0
  246. package/js/utils/ContractHelper/index.ts +72 -0
  247. package/js/utils/CourseRuns/index.spec.tsx +20 -1
  248. package/js/utils/CourseRuns/index.ts +14 -2
  249. package/js/utils/CoursesHelper/index.spec.ts +45 -55
  250. package/js/utils/CoursesHelper/index.ts +6 -7
  251. package/js/utils/CreditCardHelper/index.spec.tsx +26 -22
  252. package/js/utils/CreditCardHelper/index.tsx +19 -6
  253. package/js/utils/ObjectHelper/index.spec.ts +18 -10
  254. package/js/utils/ObjectHelper/index.ts +9 -0
  255. package/js/utils/OrderHelper/index.ts +32 -0
  256. package/js/utils/ProductHelper/index.ts +5 -1
  257. package/js/utils/StringHelper/index.spec.tsx +11 -0
  258. package/js/utils/StringHelper/index.ts +8 -0
  259. package/js/utils/UserHelper/index.spec.ts +18 -0
  260. package/js/utils/UserHelper/index.ts +8 -0
  261. package/js/utils/download.ts +43 -0
  262. package/js/utils/errors/HttpError.ts +10 -0
  263. package/js/utils/indirection/window.ts +1 -1
  264. package/js/utils/react-query/createQueryClient.ts +12 -21
  265. package/js/utils/react-query/useLocalizedQueryKey.ts +1 -1
  266. package/js/utils/react-query/useSessionMutation/index.spec.tsx +8 -8
  267. package/js/utils/react-query/useSessionMutation/index.ts +6 -11
  268. package/js/utils/react-query/useSessionQuery/index.spec.tsx +36 -8
  269. package/js/utils/react-query/useSessionQuery/index.ts +14 -21
  270. package/js/utils/search/getSuggestionsSection/index.spec.ts +4 -3
  271. package/js/utils/search/getSuggestionsSection/index.ts +4 -1
  272. package/js/utils/search/index.tsx +8 -3
  273. package/js/utils/test/createTestQueryClient.ts +7 -7
  274. package/js/utils/test/expectBanner.ts +16 -3
  275. package/js/utils/test/factories/factories.ts +4 -4
  276. package/js/utils/test/factories/joanie.spec.ts +7 -0
  277. package/js/utils/test/factories/joanie.ts +214 -63
  278. package/js/utils/test/factories/reactQuery.ts +1 -1
  279. package/js/utils/test/factories/richie.ts +4 -2
  280. package/js/utils/test/mockCourseProductWithOrder.ts +28 -0
  281. package/js/utils/test/mockPaginatedResponse.ts +1 -1
  282. package/js/utils/test/render.tsx +72 -0
  283. package/js/utils/test/wrappers/IntlWrapper.tsx +23 -0
  284. package/js/utils/test/wrappers/JoanieAppWrapper.tsx +42 -0
  285. package/js/utils/test/wrappers/PresentationalAppWrapper.tsx +18 -0
  286. package/js/utils/test/wrappers/ReactQueryWrapper.tsx +16 -0
  287. package/js/utils/test/wrappers/RouterWrapper.tsx +29 -0
  288. package/js/utils/test/wrappers/types.ts +26 -0
  289. package/js/widgets/Dashboard/components/DashboardAvatar/_styles.scss +17 -5
  290. package/js/widgets/Dashboard/components/DashboardAvatar/index.spec.tsx +9 -2
  291. package/js/widgets/Dashboard/components/DashboardAvatar/index.tsx +16 -5
  292. package/js/widgets/Dashboard/components/DashboardBox/index.stories.tsx +1 -1
  293. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/_styles.scss +1 -0
  294. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.tsx +7 -4
  295. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +1 -1
  296. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +3 -3
  297. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +2 -2
  298. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +49 -9
  299. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.tsx +27 -73
  300. package/js/widgets/Dashboard/components/DashboardItem/CertificateStatus/index.spec.tsx +65 -0
  301. package/js/widgets/Dashboard/components/DashboardItem/CertificateStatus/index.tsx +59 -0
  302. package/js/widgets/Dashboard/components/DashboardItem/Contract/_styles.scss +29 -0
  303. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +197 -0
  304. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.stories.tsx +34 -0
  305. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.tsx +53 -0
  306. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
  307. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.spec.tsx +158 -0
  308. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +6 -6
  309. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +141 -84
  310. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +40 -37
  311. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +29 -11
  312. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +248 -0
  313. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +89 -0
  314. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +188 -117
  315. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +124 -78
  316. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +299 -0
  317. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +286 -0
  318. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderReadonly.stories.tsx +9 -5
  319. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderWritable.stories.tsx +10 -6
  320. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.spec.tsx +121 -0
  321. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +98 -0
  322. package/js/widgets/Dashboard/components/DashboardItem/Order/_styles.scss +43 -0
  323. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +34 -37
  324. package/js/widgets/Dashboard/components/DashboardItem/index.spec.tsx +74 -4
  325. package/js/widgets/Dashboard/components/DashboardItem/index.stories.tsx +18 -0
  326. package/js/widgets/Dashboard/components/DashboardItem/index.tsx +91 -26
  327. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +4 -8
  328. package/js/widgets/Dashboard/components/DashboardLayout/_styles.scss +14 -5
  329. package/js/widgets/Dashboard/components/DashboardLayout/index.tsx +10 -3
  330. package/js/widgets/Dashboard/components/DashboardListAvatar/_styles.scss +8 -0
  331. package/js/widgets/Dashboard/components/DashboardListAvatar/index.tsx +11 -0
  332. package/js/widgets/Dashboard/components/DashboardOrderLoader/_styles.scss +5 -0
  333. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +50 -14
  334. package/js/widgets/Dashboard/components/DashboardSidebar/_styles.scss +37 -24
  335. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +244 -0
  336. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +49 -0
  337. package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.spec.tsx +40 -0
  338. package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.tsx +28 -0
  339. package/js/widgets/Dashboard/components/DashboardSidebar/components/NavigationSelect.tsx +58 -0
  340. package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +11 -1
  341. package/js/widgets/Dashboard/components/DashboardSidebar/index.tsx +18 -69
  342. package/js/widgets/Dashboard/components/DashboardSidebar/utils.ts +6 -0
  343. package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +58 -0
  344. package/js/widgets/Dashboard/components/FiltersBar/index.tsx +9 -0
  345. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +4 -2
  346. package/js/widgets/Dashboard/components/NavigateWithParams/index.spec.tsx +31 -40
  347. package/js/widgets/Dashboard/components/RouterButton/index.tsx +2 -1
  348. package/js/widgets/Dashboard/components/Signature/DummyContractPlaceholder.tsx +25 -0
  349. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +58 -0
  350. package/js/widgets/Dashboard/components/Signature/SignatureLexPersona.tsx +72 -0
  351. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +215 -0
  352. package/js/widgets/Dashboard/components/{TeacherCourseDashboardSidebar → TeacherDashboardCourseSidebar}/index.tsx +75 -31
  353. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +23 -0
  354. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +154 -0
  355. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.stories.tsx +42 -0
  356. package/js/widgets/Dashboard/components/{TeacherOrganizationDashboardSidebar → TeacherDashboardOrganizationSidebar}/index.tsx +40 -29
  357. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/components/OrganizationLinks/_styles.scss +27 -10
  358. package/js/widgets/Dashboard/components/TeacherDashboardProfileSidebar/components/OrganizationLinks/index.spec.tsx +49 -0
  359. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/components/OrganizationLinks/index.tsx +18 -7
  360. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.spec.tsx +13 -39
  361. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.stories.tsx +5 -5
  362. package/js/widgets/Dashboard/components/{TeacherProfileDashboardSidebar → TeacherDashboardProfileSidebar}/index.tsx +4 -3
  363. package/js/widgets/Dashboard/hooks/useEnroll/index.ts +8 -8
  364. package/js/widgets/Dashboard/index.spec.tsx +22 -12
  365. package/js/widgets/Dashboard/utils/learnerRouteMessages.tsx +12 -1
  366. package/js/widgets/Dashboard/utils/learnerRoutes.tsx +6 -0
  367. package/js/widgets/Dashboard/utils/teacherRouteMessages.tsx +60 -4
  368. package/js/widgets/Dashboard/utils/teacherRoutes.tsx +87 -13
  369. package/js/widgets/LtiConsumer/index.spec.tsx +44 -33
  370. package/js/widgets/LtiConsumer/index.tsx +11 -15
  371. package/js/widgets/Search/components/SearchFilterGroup/index.spec.tsx +0 -5
  372. package/js/widgets/Search/components/SearchFilterGroupModal/_styles.scss +0 -9
  373. package/js/widgets/Search/components/SearchFilterGroupModal/index.spec.tsx +0 -5
  374. package/js/widgets/Search/components/SearchFilterGroupModal/index.tsx +86 -60
  375. package/js/widgets/Search/components/SearchFilterValueParent/index.stories.tsx +51 -0
  376. package/js/widgets/Search/components/SearchFilterValueParent/index.tsx +7 -7
  377. package/js/widgets/Search/components/SearchFiltersPane/_styles.scss +2 -16
  378. package/js/widgets/Search/components/SearchFiltersPane/index.tsx +9 -6
  379. package/js/widgets/Search/hooks/useCourseSearch/index.ts +13 -7
  380. package/js/widgets/Search/index.spec.tsx +3 -2
  381. package/js/widgets/Search/utils/getResourceList/index.spec.ts +12 -5
  382. package/js/widgets/Search/utils/getResourceList/index.ts +6 -2
  383. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +74 -0
  384. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/_styles.scss +11 -14
  385. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/_styles.scss +1 -1
  386. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/index.spec.tsx +13 -2
  387. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.stories.tsx +33 -0
  388. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCertificateItem/index.tsx +5 -8
  389. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
  390. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +16 -8
  391. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/EnrolledCourseRun.tsx +5 -3
  392. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/_styles.scss +1 -0
  393. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/index.spec.tsx +100 -35
  394. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseRunItem/index.spec.tsx +7 -4
  395. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.stories.tsx +36 -0
  396. package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseRunItem/index.tsx +5 -3
  397. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +40 -0
  398. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +898 -0
  399. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +83 -0
  400. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +253 -0
  401. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/CourseRunUnenrollmentButton/index.tsx +3 -2
  402. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/_styles.scss +0 -25
  403. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +73 -30
  404. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.openedx.spec.tsx +10 -5
  405. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +54 -26
  406. package/js/widgets/SyllabusCourseRunsList/components/CourseRunItemWithEnrollment/index.tsx +1 -0
  407. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/_styles.scss +0 -3
  408. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/hooks/useCourseWish/index.spec.tsx +12 -9
  409. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.login.spec.tsx +14 -12
  410. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.logout.spec.tsx +4 -6
  411. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +5 -5
  412. package/js/widgets/SyllabusCourseRunsList/components/SyllabusAsideList/index.tsx +2 -2
  413. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +19 -13
  414. package/js/widgets/SyllabusCourseRunsList/components/SyllabusSimpleCourseRunsList/index.tsx +1 -0
  415. package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.spec.tsx +2 -1
  416. package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.ts +14 -19
  417. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -14
  418. package/js/widgets/SyllabusCourseRunsList/index.tsx +5 -8
  419. package/js/widgets/UserLogin/components/UserMenu/DesktopUserMenu.tsx +2 -1
  420. package/js/widgets/UserLogin/components/UserMenu/MobileUserMenu.tsx +2 -1
  421. package/js/widgets/UserLogin/components/UserMenu/index.tsx +6 -5
  422. package/js/widgets/UserLogin/index.not.isJoanieEnabled.spec.tsx +120 -0
  423. package/js/widgets/UserLogin/index.spec.tsx +108 -43
  424. package/js/widgets/UserLogin/index.stories.tsx +29 -0
  425. package/js/widgets/UserLogin/index.tsx +33 -15
  426. package/js/widgets/index.tsx +3 -6
  427. package/mocks/browser.ts +1 -1
  428. package/mocks/handlers/contracts.ts +16 -0
  429. package/mocks/handlers.ts +3 -1
  430. package/package.json +82 -78
  431. package/scss/_main.scss +2 -0
  432. package/scss/colors/_palette.scss +2 -2
  433. package/scss/colors/_theme.scss +6 -16
  434. package/scss/components/_content.scss +1 -1
  435. package/scss/components/_index.scss +24 -14
  436. package/scss/generic/_type.scss +1 -1
  437. package/scss/objects/_characteristics.scss +7 -14
  438. package/scss/objects/_course_glimpses.scss +3 -7
  439. package/scss/objects/_dashboard.scss +28 -0
  440. package/scss/objects/_form.scss +14 -355
  441. package/scss/objects/_index.scss +1 -0
  442. package/scss/objects/_list.scss +8 -0
  443. package/scss/objects/_organization_glimpses.scss +2 -8
  444. package/scss/objects/_selector.scss +1 -0
  445. package/scss/trumps/_bootstrap.scss +4 -0
  446. package/scss/vendors/css/cunningham-tokens.css +89 -25
  447. package/scss/vendors/cunningham-tokens.scss +208 -128
  448. package/js/components/AddressesManagement/AddressForm.spec.tsx +0 -206
  449. package/js/components/AddressesManagement/AddressForm.tsx +0 -169
  450. package/js/components/Button/index.spec.tsx +0 -36
  451. package/js/components/Button/index.stories.tsx +0 -26
  452. package/js/components/Button/index.tsx +0 -38
  453. package/js/components/Form/CheckboxField.stories.tsx +0 -12
  454. package/js/components/Form/Field.stories.config.tsx +0 -24
  455. package/js/components/Form/Inputs.tsx +0 -295
  456. package/js/components/Form/RadioField.stories.tsx +0 -18
  457. package/js/components/Form/SelectField.stories.tsx +0 -27
  458. package/js/components/Form/TextAreaField.stories.tsx +0 -12
  459. package/js/components/Form/TextField.stories.tsx +0 -12
  460. package/js/components/Form/index.spec.tsx +0 -297
  461. package/js/hooks/useProduct.ts +0 -28
  462. package/js/utils/test/mockProductWithOrder.ts +0 -17
  463. package/js/widgets/CourseProductItem/components/PaymentButton/_styles.scss +0 -12
  464. package/js/widgets/CourseProductItem/components/PaymentButton/index.spec.tsx +0 -473
  465. package/js/widgets/CourseProductItem/components/PurchaseButton/index.spec.tsx +0 -259
  466. package/js/widgets/CourseProductItem/components/SaleTunnel/_styles.scss +0 -41
  467. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/_styles.scss +0 -130
  468. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/index.spec.tsx +0 -29
  469. package/js/widgets/CourseProductItem/components/SaleTunnelStepResume/index.tsx +0 -59
  470. package/js/widgets/CourseProductItem/components/SaleTunnelStepValidation/index.spec.tsx +0 -71
  471. package/js/widgets/CourseProductItem/contexts/CourseProductContext/index.spec.tsx +0 -35
  472. package/js/widgets/CourseProductItem/contexts/CourseProductContext/index.tsx +0 -45
  473. package/js/widgets/CourseProductItem/index.spec.tsx +0 -486
  474. package/js/widgets/CourseProductItem/index.tsx +0 -205
  475. package/js/widgets/Dashboard/components/DashboardItem/DashboardItemCourseEnrolling.spec.tsx +0 -64
  476. package/js/widgets/Dashboard/components/TeacherCourseDashboardSidebar/index.spec.tsx +0 -105
  477. package/js/widgets/Dashboard/components/TeacherOrganizationDashboardSidebar/index.stories.tsx +0 -28
  478. /package/js/components/AddressesManagement/{validationSchema.spec.ts → AddressForm/validationSchema.spec.ts} +0 -0
  479. /package/js/{widgets/CourseProductItem/components → components}/EnrollmentDate/index.tsx +0 -0
  480. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/_styles.scss +0 -0
  481. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/RegisteredCreditCard/index.spec.tsx +0 -0
  482. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/SaleTunnelStepValidation/TargetCourseDetail.tsx +0 -0
  483. /package/js/{widgets/CourseProductItem → components/SaleTunnel}/components/StepBreadcrumb/_styles.scss +0 -0
  484. /package/js/{widgets/CourseProductItem/hooks → hooks}/useStepManager/index.spec.ts +0 -0
  485. /package/js/{widgets/CourseProductItem/hooks → hooks}/useStepManager/index.ts +0 -0
  486. /package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/CourseRunListCell/index.tsx +0 -0
  487. /package/js/pages/{TeacherCourseDashboardLoader → TeacherDashboardCourseLoader}/CourseRunList/index.tsx +0 -0
  488. /package/js/pages/{TeacherTrainingDashboard → TeacherDashboardTraining}/_styles.scss +0 -0
  489. /package/js/widgets/Dashboard/components/{TeacherCourseDashboardSidebar → TeacherDashboardCourseSidebar}/_styles.scss +0 -0
  490. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/CourseRunSection.tsx +0 -0
  491. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/components/CourseProductCourseRuns/index.tsx +0 -0
  492. /package/js/widgets/{CourseProductItem → SyllabusCourseRunsList/components/CourseProductItem}/types/payments/payplug.d.ts +0 -0
@@ -0,0 +1,898 @@
1
+ import { getByText, render, screen, waitFor } from '@testing-library/react';
2
+ import fetchMock from 'fetch-mock';
3
+ import type { PropsWithChildren } from 'react';
4
+ import { IntlProvider } from 'react-intl';
5
+ import { QueryClientProvider } from '@tanstack/react-query';
6
+ import queryString from 'query-string';
7
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
+ import {
9
+ CourseLightFactory,
10
+ CourseProductRelationFactory,
11
+ EnrollmentFactory,
12
+ CredentialOrderFactory,
13
+ ProductFactory,
14
+ OrderGroupFullFactory,
15
+ OrderGroupFactory,
16
+ } from 'utils/test/factories/joanie';
17
+ import JoanieApiProvider from 'contexts/JoanieApiContext';
18
+ import {
19
+ CourseRun,
20
+ Enrollment,
21
+ CredentialOrder,
22
+ OrderState,
23
+ ACTIVE_ORDER_STATES,
24
+ } from 'types/Joanie';
25
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
26
+ import { Deferred } from 'utils/test/deferred';
27
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
28
+ import { HttpStatusCode } from 'utils/errors/HttpError';
29
+ import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
30
+ import CourseProductItem from '.';
31
+
32
+ jest.mock('utils/context', () => ({
33
+ __esModule: true,
34
+ default: mockRichieContextFactory({
35
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
36
+ joanie_backend: { endpoint: 'https://joanie.test' },
37
+ }).one(),
38
+ }));
39
+
40
+ jest.mock('./components/CourseProductCertificateItem', () => ({
41
+ __esModule: true,
42
+ default: () => <div data-testid="CertificateItem" />,
43
+ }));
44
+
45
+ jest.mock('./components/CourseProductCourseRuns', () => ({
46
+ CourseRunList: ({ courseRuns }: { courseRuns: CourseRun[] }) => (
47
+ <div data-testid={`CourseRunList-${courseRuns.map(({ id }) => id).join('-')}`} />
48
+ ),
49
+ EnrollableCourseRunList: ({
50
+ courseRuns,
51
+ order,
52
+ }: {
53
+ courseRuns: CourseRun[];
54
+ order: CredentialOrder;
55
+ }) => (
56
+ <div
57
+ data-testid={`EnrollableCourseRunList-${courseRuns.map(({ id }) => id).join('-')}-${
58
+ order.id
59
+ }`}
60
+ />
61
+ ),
62
+ EnrolledCourseRun: ({ enrollment }: { enrollment: Enrollment }) => (
63
+ <div data-testid={`EnrolledCourseRun-${enrollment.id}`} />
64
+ ),
65
+ }));
66
+
67
+ describe('CourseProductItem', () => {
68
+ const priceFormatter = (currency: string, price: number) =>
69
+ new Intl.NumberFormat('en', {
70
+ currency,
71
+ style: 'currency',
72
+ }).format(price);
73
+
74
+ beforeEach(() => {
75
+ // JoanieSessionProvider requests
76
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
77
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
78
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
79
+ });
80
+
81
+ afterEach(() => {
82
+ fetchMock.restore();
83
+ });
84
+
85
+ const Wrapper = ({ withSession, children }: PropsWithChildren<{ withSession?: boolean }>) => (
86
+ <IntlProvider locale="en">
87
+ <JoanieApiProvider>
88
+ <QueryClientProvider client={createTestQueryClient({ user: withSession || null })}>
89
+ <JoanieSessionProvider>{children}</JoanieSessionProvider>
90
+ </QueryClientProvider>
91
+ </JoanieApiProvider>
92
+ </IntlProvider>
93
+ );
94
+
95
+ it('should display a loader until product is loaded', async () => {
96
+ const relation = CourseProductRelationFactory().one();
97
+ const { product } = relation;
98
+ const productDeferred = new Deferred();
99
+ fetchMock.get(
100
+ `https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`,
101
+ productDeferred.promise,
102
+ );
103
+
104
+ render(
105
+ <Wrapper>
106
+ <CourseProductItem
107
+ course={CourseLightFactory({ code: '00000' }).one()}
108
+ productId={product.id}
109
+ />
110
+ </Wrapper>,
111
+ );
112
+
113
+ // - A loader should be displayed while product information are fetching
114
+ await expectSpinner('Loading product information...');
115
+ productDeferred.resolve(relation);
116
+ await expectNoSpinner('Loading product information...');
117
+ });
118
+
119
+ it('renders product information for anonymous user', async () => {
120
+ const relation = CourseProductRelationFactory().one();
121
+ const { product } = relation;
122
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
123
+
124
+ render(
125
+ <Wrapper>
126
+ <CourseProductItem
127
+ course={CourseLightFactory({ code: '00000' }).one()}
128
+ productId={product.id}
129
+ />
130
+ </Wrapper>,
131
+ );
132
+
133
+ await screen.findByRole('heading', { level: 3, name: product.title });
134
+ // the price shouldn't be a heading to prevent misdirection for screen reader users,
135
+ // but we want to it to visually look like a h6
136
+
137
+ const $price = screen.getByText(
138
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
139
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
140
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
141
+ );
142
+ expect($price.tagName).toBe('STRONG');
143
+ expect($price.classList.contains('h6')).toBe(true);
144
+
145
+ // Languages and date range should not be displayed
146
+ expect(screen.queryByTestId('product-widget__header-metadata-dates')).not.toBeInTheDocument();
147
+ expect(
148
+ screen.queryByTestId('product-widget__header-metadata-languages'),
149
+ ).not.toBeInTheDocument();
150
+
151
+ // - Render all target courses information
152
+ relation.product.target_courses.forEach((course) => {
153
+ const $item = screen.getByTestId(`course-item-${course.code}`);
154
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
155
+ // but we want to it to visually look like a h5
156
+ const $courseTitle = getByText($item, course.title);
157
+ expect($courseTitle.tagName).toBe('STRONG');
158
+ expect($courseTitle.classList.contains('h5')).toBe(true);
159
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
160
+ });
161
+
162
+ // - Render <CertificateItem />
163
+ screen.getByTestId('CertificateItem');
164
+
165
+ // - Render a login button
166
+ screen.getByRole('button', { name: `Login to purchase "${product.title}"` });
167
+ // - Does not render PurchaseButton cta
168
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
169
+ });
170
+
171
+ it('does not render <CertificateItem /> if product do not have a certificate', async () => {
172
+ const relation = CourseProductRelationFactory({
173
+ product: ProductFactory({
174
+ certificate_definition: undefined,
175
+ }).one(),
176
+ }).one();
177
+ const { product } = relation;
178
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
179
+
180
+ render(
181
+ <Wrapper>
182
+ <CourseProductItem
183
+ productId={product.id}
184
+ course={CourseLightFactory({ code: '00000' }).one()}
185
+ />
186
+ </Wrapper>,
187
+ );
188
+
189
+ // Wait for product information to be fetched
190
+ await screen.findByRole('heading', { level: 3, name: product.title });
191
+
192
+ // - Does not render <CertificateItem />
193
+ expect(screen.queryByTestId('CertificateItem')).toBeNull();
194
+ });
195
+
196
+ it('renders product informations in compact mode', async () => {
197
+ const relation = CourseProductRelationFactory().one();
198
+ fetchMock.get(
199
+ `https://joanie.test/api/v1.0/courses/00000/products/${relation.product.id}/`,
200
+ relation,
201
+ );
202
+
203
+ const { container } = render(
204
+ <Wrapper>
205
+ <CourseProductItem
206
+ course={CourseLightFactory({ code: '00000' }).one()}
207
+ productId={relation.product.id}
208
+ compact
209
+ />
210
+ </Wrapper>,
211
+ );
212
+
213
+ // In the header, we should display the product title, the product price
214
+ // and product date range and languages
215
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
216
+ // the price shouldn't be a heading to prevent misdirection for screen reader users,
217
+ // but we want to it to visually look like a h6
218
+
219
+ const $price = screen.getByText(
220
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
221
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
222
+ priceFormatter(relation.product.price_currency, relation.product.price).replace(
223
+ /(\u202F|\u00a0)/g,
224
+ ' ',
225
+ ),
226
+ );
227
+ expect($price.tagName).toBe('STRONG');
228
+ expect($price.classList.contains('h6')).toBe(true);
229
+
230
+ screen.getByTestId('product-widget__header-metadata-dates');
231
+ screen.getByTestId('product-widget__header-metadata-languages');
232
+
233
+ // Then the content block should only display the purchase button.
234
+ const $productWidgetContent = container.querySelector('.product-widget__content');
235
+ expect($productWidgetContent).not.toBeInTheDocument();
236
+
237
+ // - Any target courses information should be displayed
238
+ relation.product.target_courses.forEach((course) => {
239
+ const $item = screen.queryByTestId(`course-item-${course.code}`);
240
+ expect($item).not.toBeInTheDocument();
241
+ });
242
+
243
+ // - Any <CertificateItem /> should be displayed
244
+ expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
245
+
246
+ // - Render a login button
247
+ screen.getByRole('button', { name: `Login to purchase "${relation.product.title}"` });
248
+ // - Does not render PurchaseButton cta
249
+ expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
250
+ });
251
+
252
+ it('renders product informations for a purchased product', async () => {
253
+ const relation = CourseProductRelationFactory().one();
254
+ const { product } = relation;
255
+ const order = CredentialOrderFactory({
256
+ product_id: product.id,
257
+ course: CourseLightFactory({ code: '00000' }).one(),
258
+ target_courses: product.target_courses,
259
+ }).one();
260
+
261
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
262
+ const orderQueryParameters = {
263
+ product_id: order.product_id,
264
+ course_code: order.course.code,
265
+ state: ACTIVE_ORDER_STATES,
266
+ };
267
+ fetchMock.get(
268
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
269
+ [order],
270
+ );
271
+
272
+ render(
273
+ <Wrapper withSession>
274
+ <CourseProductItem
275
+ productId={product.id}
276
+ course={CourseLightFactory({ code: '00000' }).one()}
277
+ />
278
+ </Wrapper>,
279
+ );
280
+
281
+ // Wait for product information to be fetched
282
+ await screen.findByRole('heading', { level: 3, name: product.title });
283
+
284
+ // - In place of product price, a label should be displayed
285
+ const $enrolledInfo = await screen.findByText('Purchased');
286
+ expect($enrolledInfo.tagName).toBe('STRONG');
287
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
288
+
289
+ // - Render all order's target courses information with EnrollableCourseRunList component
290
+ await waitFor(() => {
291
+ order.target_courses.forEach((course) => {
292
+ const $item = screen.getByTestId(`course-item-${course.code}`);
293
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
294
+ // but we want to it to visually look like a h5
295
+ const $courseTitle = getByText($item, course.title);
296
+ expect($courseTitle.tagName).toBe('STRONG');
297
+ expect($courseTitle.classList.contains('h5')).toBe(true);
298
+ screen.getByTestId(
299
+ `EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
300
+ );
301
+ });
302
+ });
303
+
304
+ // - Render <CertificateItem />
305
+ screen.getByTestId('CertificateItem');
306
+
307
+ // - Does not Render PurchaseButton cta
308
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
309
+ });
310
+
311
+ it('renders product informations for a purchased product in compact mode', async () => {
312
+ const relation = CourseProductRelationFactory().one();
313
+ const order: CredentialOrder = CredentialOrderFactory({
314
+ product_id: relation.product.id,
315
+ course: CourseLightFactory({ code: '00000' }).one(),
316
+ target_courses: relation.product.target_courses,
317
+ }).one();
318
+
319
+ fetchMock.get(
320
+ `https://joanie.test/api/v1.0/courses/00000/products/${relation.product.id}/`,
321
+ relation,
322
+ );
323
+ const orderQueryParameters = {
324
+ product_id: order.product_id,
325
+ course_code: order.course?.code,
326
+ state: ACTIVE_ORDER_STATES,
327
+ };
328
+ fetchMock.get(
329
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
330
+ [order],
331
+ );
332
+
333
+ render(
334
+ <Wrapper withSession>
335
+ <CourseProductItem
336
+ productId={relation.product.id}
337
+ course={CourseLightFactory({ code: '00000' }).one()}
338
+ compact
339
+ />
340
+ </Wrapper>,
341
+ );
342
+
343
+ // Wait for product information to be fetched
344
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
345
+
346
+ // - In place of product price, a label should be displayed
347
+ const $enrolledInfo = await screen.findByText('Purchased');
348
+ expect($enrolledInfo.tagName).toBe('STRONG');
349
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
350
+
351
+ // - Product date range and languages should not be displayed anymore
352
+ expect(screen.queryByTestId('product-widget__header-metadata-dates')).not.toBeInTheDocument();
353
+ expect(
354
+ screen.queryByTestId('product-widget__header-metadata-languages'),
355
+ ).not.toBeInTheDocument();
356
+
357
+ // - Render all order's target courses information with EnrollableCourseRunList component
358
+ await waitFor(() => {
359
+ order.target_courses.forEach((course) => {
360
+ const $item = screen.getByTestId(`course-item-${course.code}`);
361
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
362
+ // but we want to it to visually look like a h5
363
+ const $courseTitle = getByText($item, course.title);
364
+ expect($courseTitle.tagName).toBe('STRONG');
365
+ expect($courseTitle.classList.contains('h5')).toBe(true);
366
+ screen.getByTestId(
367
+ `EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
368
+ );
369
+ });
370
+ });
371
+
372
+ // - Render <CertificateItem />
373
+ screen.getByTestId('CertificateItem');
374
+
375
+ // - Does not Render PurchaseButton cta
376
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
377
+ });
378
+
379
+ it('renders enrollment information when user is enrolled to a course run', async () => {
380
+ const relation = CourseProductRelationFactory().one();
381
+ const { product } = relation;
382
+ // - Create an order with an active enrollment
383
+ const enrollment: Enrollment = EnrollmentFactory({
384
+ course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
385
+ }).one();
386
+ const order: CredentialOrder = CredentialOrderFactory({
387
+ product_id: product.id,
388
+ course: CourseLightFactory({ code: '00000' }).one(),
389
+ target_courses: product.target_courses,
390
+ target_enrollments: [enrollment],
391
+ }).one();
392
+
393
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
394
+ const orderQueryParameters = {
395
+ product_id: order.product_id,
396
+ course_code: order.course?.code,
397
+ state: ACTIVE_ORDER_STATES,
398
+ };
399
+ fetchMock.get(
400
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
401
+ [order],
402
+ );
403
+
404
+ render(
405
+ <Wrapper withSession>
406
+ <CourseProductItem
407
+ productId={product.id}
408
+ course={CourseLightFactory({ code: '00000' }).one()}
409
+ />
410
+ </Wrapper>,
411
+ );
412
+
413
+ // Wait for product information to be fetched
414
+ await screen.findByRole('heading', { level: 3, name: product.title });
415
+
416
+ // - In place of product price, a label should be displayed
417
+ const $enrolledInfo: HTMLElement = await screen.findByText('Purchased');
418
+ expect($enrolledInfo!.tagName).toBe('STRONG');
419
+ expect($enrolledInfo!.classList.contains('h6')).toBe(true);
420
+
421
+ const [targetCourse, ...targetCourses] = product.target_courses;
422
+ // - The first target course should display the EnrolledCourseRun component
423
+ const $courseTitle = screen.getByText(targetCourse.title);
424
+ expect($courseTitle.tagName).toBe('STRONG');
425
+ expect($courseTitle.classList.contains('h5')).toBe(true);
426
+ await waitFor(() => {
427
+ screen.getByTestId(`EnrolledCourseRun-${enrollment.id}`);
428
+ });
429
+
430
+ // - Other target courses should display EnrollableCourseRunList component
431
+ targetCourses.forEach((course) => {
432
+ const $item = screen.getByTestId(`course-item-${course.code}`);
433
+ const $itemTitle = getByText($item, course.title);
434
+ expect($itemTitle.tagName).toBe('STRONG');
435
+ expect($itemTitle.classList.contains('h5')).toBe(true);
436
+
437
+ screen.getByTestId(
438
+ `EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
439
+ );
440
+ });
441
+ });
442
+
443
+ it('renders sale tunnel button if user already has a pending order', async () => {
444
+ const relation = CourseProductRelationFactory().one();
445
+ const { product } = relation;
446
+ const order = CredentialOrderFactory({
447
+ product_id: product.id,
448
+ course: CourseLightFactory({ code: '00000' }).one(),
449
+ target_courses: product.target_courses,
450
+ state: OrderState.PENDING,
451
+ }).one();
452
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
453
+ const orderQueryParameters = {
454
+ product_id: order.product_id,
455
+ course_code: order.course?.code,
456
+ state: ACTIVE_ORDER_STATES,
457
+ };
458
+ fetchMock.get(
459
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
460
+ [order],
461
+ );
462
+
463
+ render(
464
+ <Wrapper withSession>
465
+ <CourseProductItem
466
+ productId={product.id}
467
+ course={CourseLightFactory({ code: '00000' }).one()}
468
+ />
469
+ </Wrapper>,
470
+ );
471
+
472
+ // Wait for product information to be fetched
473
+ await screen.findByRole('heading', { level: 3, name: product.title });
474
+
475
+ const $price = screen.getByText(
476
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
477
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
478
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
479
+ );
480
+ expect($price.tagName).toBe('STRONG');
481
+ expect($price.classList.contains('h6')).toBe(true);
482
+
483
+ // - Render all target courses information
484
+ relation.product.target_courses.forEach((course) => {
485
+ const $item = screen.getByTestId(`course-item-${course.code}`);
486
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
487
+ // but we want to it to visually look like a h5
488
+ const $courseTitle = getByText($item, course.title);
489
+ expect($courseTitle.tagName).toBe('STRONG');
490
+ expect($courseTitle.classList.contains('h5')).toBe(true);
491
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
492
+ });
493
+
494
+ screen.getByRole('button', { name: product.call_to_action });
495
+ });
496
+
497
+ it('renders sale tunnel button if user already has a canceled order', async () => {
498
+ const relation = CourseProductRelationFactory().one();
499
+ const { product } = relation;
500
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
501
+ const orderQueryParameters = {
502
+ product_id: product.id,
503
+ course_code: '00000',
504
+ state: ACTIVE_ORDER_STATES,
505
+ };
506
+ fetchMock.get(
507
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
508
+ [],
509
+ );
510
+
511
+ render(
512
+ <Wrapper withSession>
513
+ <CourseProductItem
514
+ productId={product.id}
515
+ course={CourseLightFactory({ code: '00000' }).one()}
516
+ />
517
+ </Wrapper>,
518
+ );
519
+
520
+ // Wait for product information to be fetched
521
+ await screen.findByRole('heading', { level: 3, name: product.title });
522
+
523
+ const $price = screen.getByText(
524
+ // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
525
+ // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
526
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
527
+ );
528
+ expect($price.tagName).toBe('STRONG');
529
+ expect($price.classList.contains('h6')).toBe(true);
530
+
531
+ // - Render all target courses information
532
+ relation.product.target_courses.forEach((course) => {
533
+ const $item = screen.getByTestId(`course-item-${course.code}`);
534
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
535
+ // but we want to it to visually look like a h5
536
+ const $courseTitle = getByText($item, course.title);
537
+ expect($courseTitle.tagName).toBe('STRONG');
538
+ expect($courseTitle.classList.contains('h5')).toBe(true);
539
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
540
+ });
541
+
542
+ screen.getByRole('button', { name: product.call_to_action });
543
+ });
544
+
545
+ it('does not render sale tunnel button if user already has a submitted order', async () => {
546
+ const relation = CourseProductRelationFactory().one();
547
+ const { product } = relation;
548
+ const order = CredentialOrderFactory({
549
+ product_id: product.id,
550
+ course: CourseLightFactory({ code: '00000' }).one(),
551
+ target_courses: product.target_courses,
552
+ state: OrderState.SUBMITTED,
553
+ }).one();
554
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
555
+ const orderQueryParameters = {
556
+ product_id: order.product_id,
557
+ course_code: order.course?.code,
558
+ state: ACTIVE_ORDER_STATES,
559
+ };
560
+ fetchMock.get(
561
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
562
+ [order],
563
+ );
564
+
565
+ render(
566
+ <Wrapper withSession>
567
+ <CourseProductItem
568
+ productId={product.id}
569
+ course={CourseLightFactory({ code: '00000' }).one()}
570
+ />
571
+ </Wrapper>,
572
+ );
573
+
574
+ // Wait for product information to be fetched
575
+ await screen.findByRole('heading', { level: 3, name: product.title });
576
+
577
+ // - In place of product price, a label "Pending" should be displayed
578
+ const $enrolledInfo = await screen.findByText('Pending');
579
+ expect($enrolledInfo.tagName).toBe('STRONG');
580
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
581
+
582
+ // - As order is pending, the user should not be able to enroll to course runs.
583
+ await waitFor(() => {
584
+ order.target_courses.forEach((course) => {
585
+ const $item = screen.getByTestId(`course-item-${course.code}`);
586
+ // the course title shouldn't be a heading to prevent misdirection for screen reader users,
587
+ // but we want to it to visually look like a h5
588
+ const $courseTitle = getByText($item, course.title);
589
+ expect($courseTitle.tagName).toBe('STRONG');
590
+ expect($courseTitle.classList.contains('h5')).toBe(true);
591
+ screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
592
+ });
593
+ });
594
+
595
+ // - Render <CertificateItem />
596
+ screen.getByTestId('CertificateItem');
597
+
598
+ // - Does not Render PurchaseButton cta
599
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
600
+ });
601
+
602
+ it.each([
603
+ {
604
+ orderState: OrderState.PENDING,
605
+ },
606
+ {
607
+ orderState: OrderState.SUBMITTED,
608
+ },
609
+ {
610
+ orderState: OrderState.DRAFT,
611
+ },
612
+ ])(
613
+ "should not render sign button and banner for order's state $orderState",
614
+ async ({ orderState }) => {
615
+ const relation = CourseProductRelationFactory().one();
616
+ const { product } = relation;
617
+ const order = CredentialOrderFactory({
618
+ product_id: product.id,
619
+ target_courses: product.target_courses,
620
+ course: CourseLightFactory({ code: '00000' }).one(),
621
+ state: orderState,
622
+ }).one();
623
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
624
+ const orderQueryParameters = {
625
+ product_id: order.product_id,
626
+ course_code: order.course.code,
627
+ state: ACTIVE_ORDER_STATES,
628
+ };
629
+
630
+ fetchMock.get(
631
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
632
+ [order],
633
+ );
634
+
635
+ render(
636
+ <Wrapper withSession>
637
+ <CourseProductItem
638
+ productId={product.id}
639
+ course={CourseLightFactory({ code: '00000' }).one()}
640
+ />
641
+ </Wrapper>,
642
+ );
643
+
644
+ // Wait for product information to be fetched
645
+ expect(
646
+ await screen.findByRole('heading', { level: 3, name: product.title }),
647
+ ).toBeInTheDocument();
648
+
649
+ // - A banner should be displayed.
650
+ expect(
651
+ screen.queryByText(
652
+ 'You need to sign your training contract before enrolling to course runs',
653
+ ),
654
+ ).not.toBeInTheDocument();
655
+
656
+ expect(
657
+ screen.queryByRole('link', { name: 'Sign your training contract' }),
658
+ ).not.toBeInTheDocument();
659
+ },
660
+ );
661
+
662
+ it('renders a button and a banner if the contract needs to be signed', async () => {
663
+ const relation = CourseProductRelationFactory().one();
664
+ const { product } = relation;
665
+ const order = CredentialOrderFactory({
666
+ product_id: product.id,
667
+ target_courses: product.target_courses,
668
+ course: CourseLightFactory({ code: '00000' }).one(),
669
+ state: OrderState.VALIDATED,
670
+ }).one();
671
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
672
+ const orderQueryParameters = {
673
+ product_id: order.product_id,
674
+ course_code: order.course.code,
675
+ state: ACTIVE_ORDER_STATES,
676
+ };
677
+
678
+ fetchMock.get(
679
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
680
+ [order],
681
+ );
682
+
683
+ render(
684
+ <Wrapper withSession>
685
+ <CourseProductItem
686
+ productId={product.id}
687
+ course={CourseLightFactory({ code: '00000' }).one()}
688
+ />
689
+ </Wrapper>,
690
+ );
691
+
692
+ // Wait for product information to be fetched
693
+ await screen.findByRole('heading', { level: 3, name: product.title });
694
+
695
+ // - A banner should be displayed.
696
+ screen.getByText('You need to sign your training contract before enrolling to course runs');
697
+ screen.getByRole('link', { name: 'Sign your training contract' });
698
+ });
699
+
700
+ it('adapts layout when user has a pending order and compact prop is set', async () => {
701
+ const relation = CourseProductRelationFactory().one();
702
+ const order: CredentialOrder = CredentialOrderFactory({
703
+ product_id: relation.product.id,
704
+ course: CourseLightFactory({ code: '00000' }).one(),
705
+ target_courses: relation.product.target_courses,
706
+ state: OrderState.SUBMITTED,
707
+ }).one();
708
+ fetchMock.get(
709
+ `https://joanie.test/api/v1.0/courses/00000/products/${relation.product.id}/`,
710
+ relation,
711
+ );
712
+ const orderQueryParameters = {
713
+ product_id: order.product_id,
714
+ course_code: order.course?.code,
715
+ state: ACTIVE_ORDER_STATES,
716
+ };
717
+ fetchMock.get(
718
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
719
+ [order],
720
+ );
721
+
722
+ render(
723
+ <Wrapper withSession>
724
+ <CourseProductItem
725
+ productId={relation.product.id}
726
+ course={CourseLightFactory({ code: '00000' }).one()}
727
+ compact={true}
728
+ />
729
+ </Wrapper>,
730
+ );
731
+
732
+ // Wait for product information to be fetched
733
+ await screen.findByRole('heading', { level: 3, name: relation.product.title });
734
+
735
+ // - In place of product price, a label should be displayed
736
+ const $enrolledInfo = await screen.findByText('Pending');
737
+ expect($enrolledInfo.tagName).toBe('STRONG');
738
+ expect($enrolledInfo.classList.contains('h6')).toBe(true);
739
+
740
+ // - Product date range and languages should be displayed
741
+ screen.getByTestId('product-widget__header-metadata-dates');
742
+ screen.getByTestId('product-widget__header-metadata-languages');
743
+
744
+ // - Target courses should not be rendered
745
+ await waitFor(() => {
746
+ order.target_courses.forEach((course) => {
747
+ const $item = screen.queryByTestId(`course-item-${course.code}`);
748
+ expect($item).not.toBeInTheDocument();
749
+ });
750
+ });
751
+
752
+ // - <CertificateItem /> should not be rendered
753
+ expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
754
+
755
+ // - Does not Render PurchaseButton cta
756
+ expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
757
+ });
758
+
759
+ it('renders error message when product fetching has failed', async () => {
760
+ const { product } = CourseProductRelationFactory().one();
761
+
762
+ fetchMock.get(
763
+ `https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`,
764
+ HttpStatusCode.NOT_FOUND,
765
+ {},
766
+ );
767
+
768
+ render(
769
+ <Wrapper>
770
+ <CourseProductItem
771
+ productId={product.id}
772
+ course={CourseLightFactory({ code: '00000' }).one()}
773
+ />
774
+ </Wrapper>,
775
+ );
776
+
777
+ // - As product fetching has failed, an error message should be displayed
778
+ await screen.findByText('An error occurred while fetching product. Please retry later.');
779
+ });
780
+
781
+ it('renders a warning message that tells that no seats are left', async () => {
782
+ const relation = CourseProductRelationFactory({
783
+ order_groups: [OrderGroupFullFactory().one()],
784
+ }).one();
785
+ const { product } = relation;
786
+ const order = CredentialOrderFactory({
787
+ product_id: product.id,
788
+ course: CourseLightFactory({ code: '00000' }).one(),
789
+ target_courses: product.target_courses,
790
+ state: OrderState.PENDING,
791
+ }).one();
792
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
793
+ const orderQueryParameters = {
794
+ product_id: order.product_id,
795
+ course_code: order.course?.code,
796
+ state: ACTIVE_ORDER_STATES,
797
+ };
798
+ fetchMock.get(
799
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
800
+ [order],
801
+ );
802
+
803
+ render(
804
+ <Wrapper withSession>
805
+ <CourseProductItem
806
+ productId={product.id}
807
+ course={CourseLightFactory({ code: '00000' }).one()}
808
+ />
809
+ </Wrapper>,
810
+ );
811
+
812
+ // wait for component to be fully loaded
813
+ await screen.findByRole('heading', { level: 3, name: product.title });
814
+
815
+ expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
816
+ screen.getByText('Sorry, no seats available for now');
817
+ });
818
+
819
+ it('renders one payment button when one of two order groups is full', async () => {
820
+ const relation = CourseProductRelationFactory({
821
+ order_groups: [OrderGroupFullFactory().one(), OrderGroupFactory().one()],
822
+ }).one();
823
+ const { product } = relation;
824
+ const order = CredentialOrderFactory({
825
+ product_id: product.id,
826
+ course: CourseLightFactory({ code: '00000' }).one(),
827
+ target_courses: product.target_courses,
828
+ state: OrderState.PENDING,
829
+ }).one();
830
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
831
+ const orderQueryParameters = {
832
+ product_id: order.product_id,
833
+ course_code: order.course?.code,
834
+ state: ACTIVE_ORDER_STATES,
835
+ };
836
+ fetchMock.get(
837
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
838
+ [order],
839
+ );
840
+
841
+ render(
842
+ <Wrapper withSession>
843
+ <CourseProductItem
844
+ productId={product.id}
845
+ course={CourseLightFactory({ code: '00000' }).one()}
846
+ />
847
+ </Wrapper>,
848
+ );
849
+
850
+ // wait for component to be fully loaded
851
+ await screen.findByRole('heading', { level: 3, name: product.title });
852
+
853
+ expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
854
+ screen.getByRole('button', { name: product.call_to_action });
855
+ screen.getByText(relation.order_groups[1].nb_available_seats + ' remaining seats');
856
+ });
857
+
858
+ it('renders mutliple payment button when there are multiple order groups', async () => {
859
+ const relation = CourseProductRelationFactory({
860
+ order_groups: [OrderGroupFactory().one(), OrderGroupFactory({ nb_available_seats: 1 }).one()],
861
+ }).one();
862
+ const { product } = relation;
863
+ const order = CredentialOrderFactory({
864
+ product_id: product.id,
865
+ course: CourseLightFactory({ code: '00000' }).one(),
866
+ target_courses: product.target_courses,
867
+ state: OrderState.PENDING,
868
+ }).one();
869
+ fetchMock.get(`https://joanie.test/api/v1.0/courses/00000/products/${product.id}/`, relation);
870
+ const orderQueryParameters = {
871
+ product_id: order.product_id,
872
+ course_code: order.course?.code,
873
+ state: ACTIVE_ORDER_STATES,
874
+ };
875
+ fetchMock.get(
876
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
877
+ [order],
878
+ );
879
+
880
+ render(
881
+ <Wrapper withSession>
882
+ <CourseProductItem
883
+ productId={product.id}
884
+ course={CourseLightFactory({ code: '00000' }).one()}
885
+ />
886
+ </Wrapper>,
887
+ );
888
+
889
+ // wait for component to be fully loaded
890
+ await screen.findByRole('heading', { level: 3, name: product.title });
891
+
892
+ expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
893
+ expect(screen.getAllByTestId('PurchaseButton__cta')).toHaveLength(2);
894
+ expect(screen.getAllByRole('button', { name: product.call_to_action })).toHaveLength(2);
895
+ screen.getByText(relation.order_groups[0].nb_available_seats + ' remaining seats');
896
+ screen.getByText('Last remaining seat!');
897
+ });
898
+ });