richie-education 2.34.1-dev5 → 2.34.1-dev9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/types/index.ts CHANGED
@@ -35,6 +35,11 @@ export interface CourseRun {
35
35
  title?: string;
36
36
  snapshot?: string;
37
37
  display_mode: CourseRunDisplayMode;
38
+ price?: number;
39
+ price_currency?: string;
40
+ offer?: string;
41
+ certificate_price?: number;
42
+ certificate_offer?: string;
38
43
  }
39
44
 
40
45
  export enum Priority {
@@ -45,7 +45,27 @@ export const CourseStateFutureOpenFactory = factory<CourseState>(() => {
45
45
  };
46
46
  });
47
47
 
48
+ enum OfferType {
49
+ PAID = 'PAID',
50
+ FREE = 'FREE',
51
+ PARTIALLY_FREE = 'PARTIALLY_FREE',
52
+ SUBSCRIPTION = 'SUBSCRIPTION',
53
+ }
54
+
48
55
  export const CourseRunFactory = factory<CourseRun>(() => {
56
+ const offerValues = Object.values(OfferType);
57
+ const offer = offerValues[Math.floor(Math.random() * offerValues.length)];
58
+ const certificateOfferValues = [OfferType.PAID, OfferType.FREE, OfferType.SUBSCRIPTION];
59
+ const certificateOffer =
60
+ certificateOfferValues[Math.floor(Math.random() * certificateOfferValues.length)];
61
+ const currency = faker.finance.currency().code;
62
+ const price = [OfferType.FREE, OfferType.PARTIALLY_FREE].includes(offer)
63
+ ? 0
64
+ : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
65
+ const certificatePrice =
66
+ certificateOffer === OfferType.FREE
67
+ ? 0
68
+ : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
49
69
  return {
50
70
  id: faker.number.int(),
51
71
  resource_link: FactoryHelper.unique(faker.internet.url),
@@ -58,6 +78,11 @@ export const CourseRunFactory = factory<CourseRun>(() => {
58
78
  dashboard_link: null,
59
79
  title: faker.lorem.sentence(3),
60
80
  display_mode: CourseRunDisplayMode.DETAILED,
81
+ price,
82
+ price_currency: currency,
83
+ offer,
84
+ certificate_price: certificatePrice,
85
+ certificate_offer: certificateOffer,
61
86
  };
62
87
  });
63
88
 
@@ -46,6 +46,51 @@ const messages = defineMessages({
46
46
  description: 'Course date of an opened course run block',
47
47
  defaultMessage: 'From {startDate} {endDate, select, undefined {} other {to {endDate}}}',
48
48
  },
49
+ coursePrice: {
50
+ id: 'components.SyllabusCourseRun.coursePrice',
51
+ description: 'Title of the course enrollment price section of an opened course run block',
52
+ defaultMessage: 'Enrollment price',
53
+ },
54
+ certificationPrice: {
55
+ id: 'components.SyllabusCourseRun.certificationPrice',
56
+ description: 'Title of the certification price section of an opened course run block',
57
+ defaultMessage: 'Certification price',
58
+ },
59
+ coursePaidOffer: {
60
+ id: 'components.SyllabusCourseRun.coursePaidOffer',
61
+ description: 'Message for the paid course offer of an opened course run block',
62
+ defaultMessage: 'The course content is paid.',
63
+ },
64
+ courseFreeOffer: {
65
+ id: 'components.SyllabusCourseRun.courseFreeOffer',
66
+ description: 'Message for the free course offer of an opened course run block',
67
+ defaultMessage: 'The course content is free.',
68
+ },
69
+ coursePartiallyFree: {
70
+ id: 'components.SyllabusCourseRun.coursePartiallyFree',
71
+ description: 'Message for the partially free course offer of an opened course run block',
72
+ defaultMessage: 'The course content is free.',
73
+ },
74
+ courseSubscriptionOffer: {
75
+ id: 'components.SyllabusCourseRun.courseSubscriptionOffer',
76
+ description: 'Message for the subscription course offer of an opened course run block',
77
+ defaultMessage: 'Subscribe to access the course content.',
78
+ },
79
+ certificatePaidOffer: {
80
+ id: 'components.SyllabusCourseRun.certificatePaidOffer',
81
+ description: 'Messagge for the paid certification offer of an opened course run block',
82
+ defaultMessage: 'The certification process is paid.',
83
+ },
84
+ certificateFreeOffer: {
85
+ id: 'components.SyllabusCourseRun.certificateFreeOffer',
86
+ description: 'Message for the free certification offer of an opened course run block',
87
+ defaultMessage: 'The certification process is free.',
88
+ },
89
+ certificateSubscriptionOffer: {
90
+ id: 'components.SyllabusCourseRun.certificateSubscriptionOffer',
91
+ description: 'Message for the subscription certification offer of an opened course run block',
92
+ defaultMessage: 'The certification process is offered through subscription.',
93
+ },
49
94
  });
50
95
 
51
96
  const OpenedCourseRun = ({
@@ -63,6 +108,44 @@ const OpenedCourseRun = ({
63
108
  const enrollmentEnd = courseRun.enrollment_end ? formatDate(courseRun.enrollment_end) : '...';
64
109
  const start = courseRun.start ? formatDate(courseRun.start) : '...';
65
110
  const end = courseRun.end ? formatDate(courseRun.end) : '...';
111
+ let courseOfferMessage = null;
112
+ let certificationOfferMessage = null;
113
+ let enrollmentPrice = '';
114
+ let certificatePrice = '';
115
+
116
+ if (courseRun.offer) {
117
+ const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
118
+ courseOfferMessage = {
119
+ PAID: messages.coursePaidOffer,
120
+ FREE: messages.courseFreeOffer,
121
+ PARTIALLY_FREE: messages.coursePartiallyFree,
122
+ SUBSCRIPTION: messages.courseSubscriptionOffer,
123
+ }[offer];
124
+
125
+ if ((courseRun.price ?? -1) >= 0) {
126
+ enrollmentPrice = intl.formatNumber(courseRun.price!, {
127
+ style: 'currency',
128
+ currency: courseRun.price_currency,
129
+ });
130
+ }
131
+ }
132
+
133
+ if (courseRun.certificate_offer) {
134
+ const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
135
+ certificationOfferMessage = {
136
+ PAID: messages.certificatePaidOffer,
137
+ FREE: messages.certificateFreeOffer,
138
+ SUBSCRIPTION: messages.certificateSubscriptionOffer,
139
+ }[certificationOffer];
140
+
141
+ if ((courseRun.certificate_price ?? -1) >= 0) {
142
+ certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
143
+ style: 'currency',
144
+ currency: courseRun.price_currency,
145
+ });
146
+ }
147
+ }
148
+
66
149
  return (
67
150
  <>
68
151
  {courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
@@ -99,6 +182,30 @@ const OpenedCourseRun = ({
99
182
  <dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
100
183
  </>
101
184
  )}
185
+ {courseOfferMessage && (
186
+ <>
187
+ <dt>
188
+ <FormattedMessage {...messages.coursePrice} />
189
+ </dt>
190
+ <dd>
191
+ <FormattedMessage {...courseOfferMessage} />
192
+ <br />
193
+ {enrollmentPrice}
194
+ </dd>
195
+ </>
196
+ )}
197
+ {certificationOfferMessage && (
198
+ <>
199
+ <dt>
200
+ <FormattedMessage {...messages.certificationPrice} />
201
+ </dt>
202
+ <dd>
203
+ <FormattedMessage {...certificationOfferMessage} />
204
+ <br />
205
+ {certificatePrice}
206
+ </dd>
207
+ </>
208
+ )}
102
209
  </dl>
103
210
  {findLmsBackend(courseRun.resource_link) ? (
104
211
  <CourseRunEnrollment courseRun={courseRun} />
@@ -41,6 +41,51 @@ const messages = defineMessages({
41
41
  description: 'Self paced course run block with no end date',
42
42
  defaultMessage: 'Available',
43
43
  },
44
+ coursePrice: {
45
+ id: 'components.SyllabusCourseRunCompacted.coursePrice',
46
+ description: 'Title of the course enrollment price section of an opened course run block',
47
+ defaultMessage: 'Enrollment price',
48
+ },
49
+ certificationPrice: {
50
+ id: 'components.SyllabusCourseRunCompacted.certificationPrice',
51
+ description: 'Title of the certification price section of an opened course run block',
52
+ defaultMessage: 'Certification price',
53
+ },
54
+ coursePaidOffer: {
55
+ id: 'components.SyllabusCourseRunCompacted.coursePaidOffer',
56
+ description: 'Message for the paid course offer of an opened course run block',
57
+ defaultMessage: 'The course content is paid.',
58
+ },
59
+ courseFreeOffer: {
60
+ id: 'components.SyllabusCourseRunCompacted.courseFreeOffer',
61
+ description: 'Message for the free course offer of an opened course run block',
62
+ defaultMessage: 'The course content is free.',
63
+ },
64
+ coursePartiallyFree: {
65
+ id: 'components.SyllabusCourseRunCompacted.coursePartiallyFree',
66
+ description: 'Message for the partially free course offer of an opened course run block',
67
+ defaultMessage: 'The course content is free.',
68
+ },
69
+ courseSubscriptionOffer: {
70
+ id: 'components.SyllabusCourseRunCompacted.courseSubscriptionOffer',
71
+ description: 'Message for the subscription course offer of an opened course run block',
72
+ defaultMessage: 'Subscribe to access the course content.',
73
+ },
74
+ certificatePaidOffer: {
75
+ id: 'components.SyllabusCourseRunCompacted.certificatePaidOffer',
76
+ description: 'Messagge for the paid certification offer of an opened course run block',
77
+ defaultMessage: 'The certification process is paid.',
78
+ },
79
+ certificateFreeOffer: {
80
+ id: 'components.SyllabusCourseRunCompacted.certificateFreeOffer',
81
+ description: 'Message for the free certification offer of an opened course run block',
82
+ defaultMessage: 'The certification process is free.',
83
+ },
84
+ certificateSubscriptionOffer: {
85
+ id: 'components.SyllabusCourseRunCompacted.certificateSubscriptionOffer',
86
+ description: 'Message for the subscription certification offer of an opened course run block',
87
+ defaultMessage: 'The certification process is offered through subscription.',
88
+ },
44
89
  });
45
90
 
46
91
  const OpenedSelfPacedCourseRun = ({
@@ -54,6 +99,44 @@ const OpenedSelfPacedCourseRun = ({
54
99
  const intl = useIntl();
55
100
  const end = courseRun.end ? formatDate(courseRun.end) : '...';
56
101
  const hasEndDate = end !== '...';
102
+ let courseOfferMessage = null;
103
+ let certificationOfferMessage = null;
104
+ let enrollmentPrice = '';
105
+ let certificatePrice = '';
106
+
107
+ if (courseRun.offer) {
108
+ const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
109
+ courseOfferMessage = {
110
+ PAID: messages.coursePaidOffer,
111
+ FREE: messages.courseFreeOffer,
112
+ PARTIALLY_FREE: messages.coursePartiallyFree,
113
+ SUBSCRIPTION: messages.courseSubscriptionOffer,
114
+ }[offer];
115
+
116
+ if ((courseRun.price ?? -1) >= 0) {
117
+ enrollmentPrice = intl.formatNumber(courseRun.price!, {
118
+ style: 'currency',
119
+ currency: courseRun.price_currency,
120
+ });
121
+ }
122
+ }
123
+
124
+ if (courseRun.certificate_offer) {
125
+ const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
126
+ certificationOfferMessage = {
127
+ PAID: messages.certificatePaidOffer,
128
+ FREE: messages.certificateFreeOffer,
129
+ SUBSCRIPTION: messages.certificateSubscriptionOffer,
130
+ }[certificationOffer];
131
+
132
+ if ((courseRun.certificate_price ?? -1) >= 0) {
133
+ certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
134
+ style: 'currency',
135
+ currency: courseRun.price_currency,
136
+ });
137
+ }
138
+ }
139
+
57
140
  return (
58
141
  <>
59
142
  {courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
@@ -83,6 +166,30 @@ const OpenedSelfPacedCourseRun = ({
83
166
  <dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
84
167
  </>
85
168
  )}
169
+ {courseOfferMessage && (
170
+ <>
171
+ <dt>
172
+ <FormattedMessage {...messages.coursePrice} />
173
+ </dt>
174
+ <dd>
175
+ <FormattedMessage {...courseOfferMessage} />
176
+ <br />
177
+ {enrollmentPrice}
178
+ </dd>
179
+ </>
180
+ )}
181
+ {certificationOfferMessage && (
182
+ <>
183
+ <dt>
184
+ <FormattedMessage {...messages.certificationPrice} />
185
+ </dt>
186
+ <dd>
187
+ <FormattedMessage {...certificationOfferMessage} />
188
+ <br />
189
+ {certificatePrice}
190
+ </dd>
191
+ </>
192
+ )}
86
193
  </dl>
87
194
  {findLmsBackend(courseRun.resource_link) ? (
88
195
  <CourseRunEnrollment courseRun={courseRun} />
@@ -30,6 +30,8 @@ import { computeStates } from 'utils/CourseRuns';
30
30
  import { IntlHelper } from 'utils/IntlHelper';
31
31
  import { render } from 'utils/test/render';
32
32
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
33
+ import { SyllabusCourseRunCompacted } from './components/SyllabusCourseRunCompacted';
34
+ import { SyllabusCourseRun } from './components/SyllabusCourseRun';
33
35
 
34
36
  jest.mock('utils/context', () => {
35
37
  const mock = mockRichieContextFactory().one();
@@ -127,7 +129,6 @@ describe('<SyllabusCourseRunsList/>', () => {
127
129
  const languagesContainer = languagesNode.nextSibling! as HTMLElement;
128
130
  getByText(languagesContainer, IntlHelper.getLocalizedLanguages(courseRun.languages, intl));
129
131
 
130
- expect(languagesContainer.nextSibling).toBeNull();
131
132
  getByRole(runContainer, 'link', {
132
133
  name: StringHelper.capitalizeFirst(courseRun.state.call_to_action)!,
133
134
  });
@@ -568,7 +569,7 @@ describe('<SyllabusCourseRunsList/>', () => {
568
569
 
569
570
  const portalContainer = getPortalContainer();
570
571
 
571
- // Expect that "is-hidden" class is set only to course runs in (MAX_ARCHIVED_COURSE_RUNS)nth-plus position.
572
+ // Expect that 'is-hidden' class is set only to course runs in (MAX_ARCHIVED_COURSE_RUNS)nth-plus position.
572
573
  expect(portalContainer.querySelectorAll('li').length).toBe(MAX_ARCHIVED_COURSE_RUNS * 2);
573
574
  portalContainer.querySelectorAll('li').forEach((listElement, i) => {
574
575
  expectCourseRunInList(listElement, courseRuns[i]);
@@ -585,7 +586,7 @@ describe('<SyllabusCourseRunsList/>', () => {
585
586
  // click on view more.
586
587
  await act(async () => user.click(button));
587
588
 
588
- // expect that "is-hidden" are removed.
589
+ // expect that 'is-hidden' are removed.
589
590
  portalContainer.querySelectorAll('li').forEach((listElement, i) => {
590
591
  expectCourseRunInList(listElement, courseRuns[i]);
591
592
  expect(listElement.classList).not.toContain('is-hidden');
@@ -615,7 +616,7 @@ describe('<SyllabusCourseRunsList/>', () => {
615
616
  const portalContainer = getPortalContainer();
616
617
  expect(screen.queryByRole('button', { name: 'View more' })).not.toBeInTheDocument();
617
618
 
618
- // expect that "is-hidden" is not set.
619
+ // expect that 'is-hidden' is not set.
619
620
  expect(portalContainer.querySelectorAll('li').length).toBe(MAX_ARCHIVED_COURSE_RUNS - 1);
620
621
  portalContainer.querySelectorAll('li').forEach((listElement, i) => {
621
622
  expectCourseRunInList(listElement, courseRuns[i]);
@@ -1009,7 +1010,7 @@ describe('<SyllabusCourseRunsList/>', () => {
1009
1010
  const listElements = screen.getAllByRole('listitem');
1010
1011
  expect(listElements.length).toBe(4);
1011
1012
 
1012
- // Assert there is only one link, one label "Enrolled" and one request to retrieve enrollment.
1013
+ // Assert there is only one link, one label 'Enrolled' and one request to retrieve enrollment.
1013
1014
  expect(await screen.findAllByText('Enrolled')).toHaveLength(1);
1014
1015
 
1015
1016
  const links = screen.getAllByRole('link');
@@ -1024,4 +1025,448 @@ describe('<SyllabusCourseRunsList/>', () => {
1024
1025
  `https://demo.endpoint/api/enrollment/v1/enrollment/${user.username},${onGoingCourseRun.resource_link}`,
1025
1026
  );
1026
1027
  });
1028
+
1029
+ it('renders price information as paid and paid on SyllabusCourseRunCompacted', async () => {
1030
+ const course = PacedCourseFactory().one();
1031
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1032
+ languages: ['en'],
1033
+ offer: 'paid',
1034
+ certificate_offer: 'paid',
1035
+ price_currency: 'EUR',
1036
+ price: 49.99,
1037
+ certificate_price: 59.99,
1038
+ }).one();
1039
+
1040
+ render(
1041
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1042
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1043
+ </div>,
1044
+ );
1045
+
1046
+ const content = getHeaderContainer().innerHTML;
1047
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1048
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1049
+ });
1050
+
1051
+ it('renders price information as subscription on SyllabusCourseRunCompacted', async () => {
1052
+ const course = PacedCourseFactory().one();
1053
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1054
+ languages: ['en'],
1055
+ offer: 'Subscription',
1056
+ certificate_offer: 'Subscription',
1057
+ price_currency: 'EUR',
1058
+ price: 49.99,
1059
+ certificate_price: 59.99,
1060
+ }).one();
1061
+
1062
+ render(
1063
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1064
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1065
+ </div>,
1066
+ );
1067
+
1068
+ const content = getHeaderContainer().innerHTML;
1069
+ expect(content).toContain('<dd>Subscribe to access the course content.<br>€49.99</dd>');
1070
+ expect(content).toContain(
1071
+ '<dd>The certification process is offered through subscription.<br>€59.99</dd>',
1072
+ );
1073
+ });
1074
+
1075
+ it('renders price information as Partially free on SyllabusCourseRunCompacted', async () => {
1076
+ const course = PacedCourseFactory().one();
1077
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1078
+ languages: ['en'],
1079
+ offer: 'Partially free',
1080
+ certificate_offer: 'paid',
1081
+ price_currency: 'EUR',
1082
+ price: 0,
1083
+ certificate_price: 59.99,
1084
+ }).one();
1085
+
1086
+ render(
1087
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1088
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1089
+ </div>,
1090
+ );
1091
+
1092
+ const content = getHeaderContainer().innerHTML;
1093
+ expect(content).toContain('<dd>The course content is free.<br>€0.00</dd>');
1094
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1095
+ });
1096
+
1097
+ it('renders price information as paid and free on SyllabusCourseRunCompacted', async () => {
1098
+ const course = PacedCourseFactory().one();
1099
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1100
+ languages: ['en'],
1101
+ offer: 'paid',
1102
+ certificate_offer: 'free',
1103
+ price_currency: 'EUR',
1104
+ price: 49.99,
1105
+ certificate_price: 0,
1106
+ }).one();
1107
+
1108
+ render(
1109
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1110
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1111
+ </div>,
1112
+ );
1113
+
1114
+ const content = getHeaderContainer().innerHTML;
1115
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1116
+ expect(content).toContain('<dd>The certification process is free.<br>€0.00</dd>');
1117
+ });
1118
+
1119
+ it('does not render price information on SyllabusCourseRunCompacted', async () => {
1120
+ const course = PacedCourseFactory().one();
1121
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1122
+ languages: ['en'],
1123
+ offer: undefined,
1124
+ certificate_offer: undefined,
1125
+ price: 59.99,
1126
+ certificate_price: 59.99,
1127
+ }).one();
1128
+
1129
+ render(
1130
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1131
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1132
+ </div>,
1133
+ );
1134
+
1135
+ const content = getHeaderContainer().innerHTML;
1136
+ expect(content).not.toContain('The course content is paid');
1137
+ expect(content).not.toContain('The certification process is paid.');
1138
+ });
1139
+
1140
+ it('does not render course price information on SyllabusCourseRunCompacted', async () => {
1141
+ const course = PacedCourseFactory().one();
1142
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1143
+ languages: ['en'],
1144
+ certificate_offer: 'paid',
1145
+ price_currency: 'EUR',
1146
+ offer: undefined,
1147
+ price: 59.99,
1148
+ certificate_price: 59.99,
1149
+ }).one();
1150
+
1151
+ render(
1152
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1153
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1154
+ </div>,
1155
+ );
1156
+
1157
+ const content = getHeaderContainer().innerHTML;
1158
+ expect(content).not.toContain('The course content is paid.');
1159
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1160
+ });
1161
+
1162
+ it('does not render certificate price information on SyllabusCourseRunCompacted', async () => {
1163
+ const course = PacedCourseFactory().one();
1164
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1165
+ languages: ['en'],
1166
+ price_currency: 'EUR',
1167
+ offer: 'paid',
1168
+ price: 49.99,
1169
+ certificate_offer: undefined,
1170
+ certificate_price: undefined,
1171
+ }).one();
1172
+
1173
+ render(
1174
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1175
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1176
+ </div>,
1177
+ );
1178
+
1179
+ const content = getHeaderContainer().innerHTML;
1180
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1181
+ expect(content).not.toContain('The certification process is paid.');
1182
+ });
1183
+
1184
+ it('does not render prices but only offers on SyllabusCourseRunCompacted', async () => {
1185
+ const course = PacedCourseFactory().one();
1186
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1187
+ languages: ['en'],
1188
+ offer: 'free',
1189
+ certificate_offer: 'free',
1190
+ price_currency: 'EUR',
1191
+ price: undefined,
1192
+ certificate_price: undefined,
1193
+ }).one();
1194
+
1195
+ render(
1196
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1197
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1198
+ </div>,
1199
+ );
1200
+
1201
+ const content = getHeaderContainer().innerHTML;
1202
+ expect(content).toContain('<dd>The course content is free.<br></dd>');
1203
+ expect(content).toContain('<dd>The certification process is free.<br></dd>');
1204
+ });
1205
+
1206
+ it('renders prices as zero on SyllabusCourseRunCompacted', async () => {
1207
+ const course = PacedCourseFactory().one();
1208
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1209
+ languages: ['en'],
1210
+ offer: 'free',
1211
+ certificate_offer: 'free',
1212
+ price_currency: 'EUR',
1213
+ price: 0,
1214
+ certificate_price: 0,
1215
+ }).one();
1216
+
1217
+ render(
1218
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1219
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1220
+ </div>,
1221
+ );
1222
+
1223
+ const content = getHeaderContainer().innerHTML;
1224
+ expect(content).toContain('<dd>The course content is free.<br>€0.00</dd>');
1225
+ expect(content).toContain('<dd>The certification process is free.<br>€0.00</dd>');
1226
+ });
1227
+
1228
+ it('does not render invalid offers on SyllabusCourseRunCompacted', async () => {
1229
+ const course = PacedCourseFactory().one();
1230
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1231
+ languages: ['en'],
1232
+ offer: 'invalid',
1233
+ certificate_offer: 'invalid',
1234
+ price_currency: 'EUR',
1235
+ price: 59.99,
1236
+ certificate_price: 59.99,
1237
+ }).one();
1238
+
1239
+ render(
1240
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1241
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1242
+ </div>,
1243
+ );
1244
+
1245
+ const content = getHeaderContainer().innerHTML;
1246
+ expect(content).not.toContain('The course content is');
1247
+ expect(content).not.toContain('The certification process is');
1248
+ expect(content).not.toContain('<br>€59.99');
1249
+ });
1250
+
1251
+ it('renders price information as paid and paid on SyllabusCourseRun', async () => {
1252
+ const course = PacedCourseFactory().one();
1253
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1254
+ languages: ['en'],
1255
+ offer: 'paid',
1256
+ certificate_offer: 'paid',
1257
+ price_currency: 'EUR',
1258
+ price: 49.99,
1259
+ certificate_price: 59.99,
1260
+ }).one();
1261
+
1262
+ render(
1263
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1264
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1265
+ </div>,
1266
+ );
1267
+
1268
+ const content = getHeaderContainer().innerHTML;
1269
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1270
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1271
+ });
1272
+
1273
+ it('renders price information as subscription on SyllabusCourseRun', async () => {
1274
+ const course = PacedCourseFactory().one();
1275
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1276
+ languages: ['en'],
1277
+ offer: 'Subscription',
1278
+ certificate_offer: 'Subscription',
1279
+ price_currency: 'EUR',
1280
+ price: 49.99,
1281
+ certificate_price: 59.99,
1282
+ }).one();
1283
+
1284
+ render(
1285
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1286
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1287
+ </div>,
1288
+ );
1289
+
1290
+ const content = getHeaderContainer().innerHTML;
1291
+ expect(content).toContain('<dd>Subscribe to access the course content.<br>€49.99</dd>');
1292
+ expect(content).toContain(
1293
+ '<dd>The certification process is offered through subscription.<br>€59.99</dd>',
1294
+ );
1295
+ });
1296
+
1297
+ it('renders price information as Partially free on SyllabusCourseRun', async () => {
1298
+ const course = PacedCourseFactory().one();
1299
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1300
+ languages: ['en'],
1301
+ offer: 'Partially free',
1302
+ certificate_offer: 'paid',
1303
+ price_currency: 'EUR',
1304
+ price: 0,
1305
+ certificate_price: 59.99,
1306
+ }).one();
1307
+
1308
+ render(
1309
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1310
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1311
+ </div>,
1312
+ );
1313
+
1314
+ const content = getHeaderContainer().innerHTML;
1315
+ expect(content).toContain('<dd>The course content is free.<br>€0.00</dd>');
1316
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1317
+ });
1318
+
1319
+ it('renders price information as paid and free on SyllabusCourseRun', async () => {
1320
+ const course = PacedCourseFactory().one();
1321
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1322
+ languages: ['en'],
1323
+ offer: 'paid',
1324
+ certificate_offer: 'free',
1325
+ price_currency: 'EUR',
1326
+ price: 49.99,
1327
+ certificate_price: 0,
1328
+ }).one();
1329
+
1330
+ render(
1331
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1332
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1333
+ </div>,
1334
+ );
1335
+
1336
+ const content = getHeaderContainer().innerHTML;
1337
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1338
+ expect(content).toContain('<dd>The certification process is free.<br>€0.00</dd>');
1339
+ });
1340
+
1341
+ it('does not render price information on SyllabusCourseRun', async () => {
1342
+ const course = PacedCourseFactory().one();
1343
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1344
+ languages: ['en'],
1345
+ offer: undefined,
1346
+ certificate_offer: undefined,
1347
+ price: 59.99,
1348
+ certificate_price: 59.99,
1349
+ }).one();
1350
+
1351
+ render(
1352
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1353
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1354
+ </div>,
1355
+ );
1356
+
1357
+ const content = getHeaderContainer().innerHTML;
1358
+ expect(content).not.toContain('The course content is paid');
1359
+ expect(content).not.toContain('The certification process is paid.');
1360
+ });
1361
+
1362
+ it('does not render course price information on SyllabusCourseRun', async () => {
1363
+ const course = PacedCourseFactory().one();
1364
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1365
+ languages: ['en'],
1366
+ offer: undefined,
1367
+ price: 59.99,
1368
+ price_currency: 'EUR',
1369
+ certificate_offer: 'paid',
1370
+ certificate_price: 59.99,
1371
+ }).one();
1372
+
1373
+ render(
1374
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1375
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1376
+ </div>,
1377
+ );
1378
+
1379
+ const content = getHeaderContainer().innerHTML;
1380
+ expect(content).not.toContain('The course content is paid.');
1381
+ expect(content).toContain('<dd>The certification process is paid.<br>€59.99</dd>');
1382
+ });
1383
+
1384
+ it('does not render certificate price information on SyllabusCourseRun', async () => {
1385
+ const course = PacedCourseFactory().one();
1386
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1387
+ languages: ['en'],
1388
+ price_currency: 'EUR',
1389
+ offer: 'paid',
1390
+ price: 49.99,
1391
+ certificate_offer: undefined,
1392
+ certificate_price: undefined,
1393
+ }).one();
1394
+
1395
+ render(
1396
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1397
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1398
+ </div>,
1399
+ );
1400
+
1401
+ const content = getHeaderContainer().innerHTML;
1402
+ expect(content).toContain('<dd>The course content is paid.<br>€49.99</dd>');
1403
+ expect(content).not.toContain('The certification process is paid.');
1404
+ });
1405
+
1406
+ it('does not render prices but only offers on SyllabusCourseRun', async () => {
1407
+ const course = PacedCourseFactory().one();
1408
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1409
+ languages: ['en'],
1410
+ offer: 'free',
1411
+ certificate_offer: 'free',
1412
+ price_currency: 'EUR',
1413
+ price: undefined,
1414
+ certificate_price: undefined,
1415
+ }).one();
1416
+
1417
+ render(
1418
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1419
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1420
+ </div>,
1421
+ );
1422
+
1423
+ const content = getHeaderContainer().innerHTML;
1424
+ expect(content).toContain('<dd>The course content is free.<br></dd>');
1425
+ expect(content).toContain('<dd>The certification process is free.<br></dd>');
1426
+ });
1427
+
1428
+ it('renders prices as zero on SyllabusCourseRun', async () => {
1429
+ const course = PacedCourseFactory().one();
1430
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1431
+ languages: ['en'],
1432
+ offer: 'free',
1433
+ certificate_offer: 'free',
1434
+ price_currency: 'EUR',
1435
+ price: 0,
1436
+ certificate_price: 0,
1437
+ }).one();
1438
+
1439
+ render(
1440
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1441
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1442
+ </div>,
1443
+ );
1444
+
1445
+ const content = getHeaderContainer().innerHTML;
1446
+ expect(content).toContain('<dd>The course content is free.<br>€0.00</dd>');
1447
+ expect(content).toContain('<dd>The certification process is free.<br>€0.00</dd>');
1448
+ });
1449
+
1450
+ it('does not render invalid offers on SyllabusCourseRun', async () => {
1451
+ const course = PacedCourseFactory().one();
1452
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1453
+ languages: ['en'],
1454
+ offer: 'invalid',
1455
+ certificate_offer: 'invalid',
1456
+ price_currency: 'EUR',
1457
+ price: 59.99,
1458
+ certificate_price: 59.99,
1459
+ }).one();
1460
+
1461
+ render(
1462
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1463
+ <SyllabusCourseRun courseRun={courseRun} course={course} showLanguages={false} />
1464
+ </div>,
1465
+ );
1466
+
1467
+ const content = getHeaderContainer().innerHTML;
1468
+ expect(content).not.toContain('The course content is');
1469
+ expect(content).not.toContain('The certification process is');
1470
+ expect(content).not.toContain('<br>€59.99');
1471
+ });
1027
1472
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.34.1-dev5",
3
+ "version": "2.34.1-dev9",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -47,29 +47,29 @@
47
47
  "@faker-js/faker": "9.5.0",
48
48
  "@formatjs/cli": "6.6.1",
49
49
  "@formatjs/intl-relativetimeformat": "11.4.10",
50
- "@hookform/resolvers": "4.1.0",
50
+ "@hookform/resolvers": "4.1.2",
51
51
  "@lyracom/embedded-form-glue": "1.4.2",
52
52
  "@openfun/cunningham-react": "3.0.0",
53
53
  "@openfun/cunningham-tokens": "2.2.0",
54
- "@sentry/browser": "9.1.0",
55
- "@sentry/types": "9.1.0",
56
- "@storybook/addon-actions": "8.5.6",
57
- "@storybook/addon-essentials": "8.5.6",
58
- "@storybook/addon-interactions": "8.5.6",
59
- "@storybook/addon-links": "8.5.6",
60
- "@storybook/react": "8.5.6",
61
- "@storybook/react-webpack5": "8.5.6",
62
- "@storybook/test": "8.5.6",
63
- "@tanstack/query-core": "5.66.3",
64
- "@tanstack/query-sync-storage-persister": "5.66.3",
65
- "@tanstack/react-query": "5.66.3",
66
- "@tanstack/react-query-devtools": "5.66.3",
67
- "@tanstack/react-query-persist-client": "5.66.3",
54
+ "@sentry/browser": "9.2.0",
55
+ "@sentry/types": "9.2.0",
56
+ "@storybook/addon-actions": "8.6.0",
57
+ "@storybook/addon-essentials": "8.6.0",
58
+ "@storybook/addon-interactions": "8.6.0",
59
+ "@storybook/addon-links": "8.6.0",
60
+ "@storybook/react": "8.6.0",
61
+ "@storybook/react-webpack5": "8.6.0",
62
+ "@storybook/test": "8.6.0",
63
+ "@tanstack/query-core": "5.66.4",
64
+ "@tanstack/query-sync-storage-persister": "5.66.4",
65
+ "@tanstack/react-query": "5.66.9",
66
+ "@tanstack/react-query-devtools": "5.66.9",
67
+ "@tanstack/react-query-persist-client": "5.66.9",
68
68
  "@testing-library/dom": "10.4.0",
69
69
  "@testing-library/jest-dom": "6.6.3",
70
70
  "@testing-library/react": "16.2.0",
71
71
  "@testing-library/user-event": "14.6.1",
72
- "@types/fetch-mock": "7.3.8",
72
+ "@types/fetch-mock": "9.2.2",
73
73
  "@types/iframe-resizer": "4.0.0",
74
74
  "@types/jest": "29.5.14",
75
75
  "@types/js-cookie": "3.0.6",
@@ -80,8 +80,8 @@
80
80
  "@types/react-autosuggest": "10.1.11",
81
81
  "@types/react-dom": "19.0.4",
82
82
  "@types/react-modal": "3.16.3",
83
- "@typescript-eslint/eslint-plugin": "8.24.0",
84
- "@typescript-eslint/parser": "8.24.0",
83
+ "@typescript-eslint/eslint-plugin": "8.25.0",
84
+ "@typescript-eslint/parser": "8.25.0",
85
85
  "babel-jest": "29.7.0",
86
86
  "babel-loader": "9.2.1",
87
87
  "babel-plugin-react-intl": "8.2.25",
@@ -106,7 +106,7 @@
106
106
  "fetch-mock": "<10",
107
107
  "file-loader": "6.2.0",
108
108
  "glob": "11.0.1",
109
- "i18n-iso-countries": "7.13.0",
109
+ "i18n-iso-countries": "7.14.0",
110
110
  "iframe-resizer": "<5",
111
111
  "intl-pluralrules": "2.0.1",
112
112
  "jest": "29.7.0",
@@ -114,10 +114,10 @@
114
114
  "js-cookie": "3.0.5",
115
115
  "lodash-es": "4.17.21",
116
116
  "mdn-polyfills": "5.20.0",
117
- "msw": "2.7.0",
117
+ "msw": "2.7.3",
118
118
  "node-fetch": ">2.6.6 <3",
119
119
  "nodemon": "3.1.9",
120
- "prettier": "3.5.1",
120
+ "prettier": "3.5.2",
121
121
  "query-string": "9.1.1",
122
122
  "react": "19.0.0",
123
123
  "react-autosuggest": "10.1.0",
@@ -125,13 +125,13 @@
125
125
  "react-hook-form": "7.54.2",
126
126
  "react-intl": "7.1.6",
127
127
  "react-modal": "3.16.3",
128
- "react-router": "7.1.5",
129
- "sass": "1.85.0",
128
+ "react-router": "7.2.0",
129
+ "sass": "1.85.1",
130
130
  "source-map-loader": "5.0.0",
131
- "storybook": "8.5.6",
131
+ "storybook": "8.6.0",
132
132
  "tsconfig-paths-webpack-plugin": "4.2.0",
133
133
  "typescript": "5.7.3",
134
- "uuid": "11.0.5",
134
+ "uuid": "11.1.0",
135
135
  "webpack": "5.98.0",
136
136
  "webpack-cli": "6.0.1",
137
137
  "whatwg-fetch": "3.6.20",
@@ -154,7 +154,7 @@
154
154
  "yarn": "1.22.22"
155
155
  },
156
156
  "devDependencies": {
157
- "@storybook/addon-mdx-gfm": "8.5.6",
157
+ "@storybook/addon-mdx-gfm": "8.6.0",
158
158
  "@storybook/addon-webpack5-compiler-babel": "3.0.5"
159
159
  }
160
160
  }