richie-education 2.34.1-dev5 → 2.34.1-dev51

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 (33) hide show
  1. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +1 -1
  2. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +84 -13
  3. package/js/components/CourseGlimpse/index.spec.tsx +80 -5
  4. package/js/components/CourseGlimpse/index.tsx +92 -76
  5. package/js/components/CourseGlimpse/utils.ts +31 -1
  6. package/js/components/Icon/index.tsx +7 -0
  7. package/js/components/OpenEdxFullNameForm/index.spec.tsx +17 -7
  8. package/js/components/OpenEdxFullNameForm/index.tsx +13 -16
  9. package/js/components/SaleTunnel/index.full-process.spec.tsx +1 -1
  10. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +9 -8
  11. package/js/types/Course.ts +18 -0
  12. package/js/types/index.ts +5 -0
  13. package/js/utils/test/expectAlert.ts +63 -0
  14. package/js/utils/test/factories/richie.ts +31 -1
  15. package/js/widgets/Slider/components/Slide.tsx +20 -0
  16. package/js/widgets/Slider/components/SlidePanel.tsx +83 -0
  17. package/js/widgets/Slider/components/Slideshow.tsx +58 -0
  18. package/js/widgets/Slider/index.spec.tsx +167 -0
  19. package/js/widgets/Slider/index.tsx +119 -0
  20. package/js/widgets/Slider/types/index.ts +8 -0
  21. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +107 -0
  22. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +107 -0
  23. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +450 -5
  24. package/js/widgets/index.tsx +3 -0
  25. package/package.json +45 -43
  26. package/scss/colors/_theme.scss +26 -7
  27. package/scss/components/_header.scss +108 -14
  28. package/scss/components/_subheader.scss +35 -0
  29. package/scss/components/templates/courses/cms/_program_detail.scss +71 -0
  30. package/scss/components/templates/richie/slider/_slider.scss +165 -99
  31. package/scss/objects/_course_glimpses.scss +103 -8
  32. package/scss/objects/_selector.scss +1 -0
  33. package/scss/settings/_variables.scss +4 -0
@@ -27,7 +27,7 @@ jest.mock('settings', () => ({
27
27
  CONTRACT_SETTINGS: {
28
28
  ...jest.requireActual('settings').CONTRACT_SETTINGS,
29
29
  pollInterval: 10,
30
- dummySignatureSignTimeout: 10,
30
+ dummySignatureSignTimeout: 50,
31
31
  },
32
32
  }));
33
33
 
@@ -1,8 +1,8 @@
1
- import { defineMessages, useIntl } from 'react-intl';
2
-
1
+ import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
3
2
  import { Icon, IconTypeEnum } from 'components/Icon';
4
3
  import { CommonDataProps } from 'types/commonDataProps';
5
4
  import { CourseGlimpseCourse } from 'components/CourseGlimpse/index';
5
+ import { CourseOffer } from 'types/Course';
6
6
 
7
7
  const messages = defineMessages({
8
8
  dateIconAlt: {
@@ -10,8 +10,46 @@ const messages = defineMessages({
10
10
  description: 'Course date logo alternative text for screen reader users',
11
11
  id: 'components.CourseGlimpseFooter.dateIconAlt',
12
12
  },
13
+ enrollmentOfferIconFreeAlt: {
14
+ defaultMessage: 'The entire course can be completed for free.',
15
+ description: 'Course offers free alternative text',
16
+ id: 'components.CourseGlimpseFooter.enrollmentOfferIconFreeAlt',
17
+ },
18
+ enrollmentOfferIconPartiallyFreeAlt: {
19
+ defaultMessage: 'More than half of the course is for free.',
20
+ description: 'Course offers partially free alternative text',
21
+ id: 'components.CourseGlimpseFooter.enrollmentOfferIconPartiallyFreeAlt',
22
+ },
23
+ enrollmentOfferIconPaidAlt: {
24
+ defaultMessage: 'Course requires a payment.',
25
+ description: 'Course offers paid alternative text',
26
+ id: 'components.CourseGlimpseFooter.enrollmentOfferIconPaidAlt',
27
+ },
28
+ enrollmentOfferIconSubscriptionAlt: {
29
+ defaultMessage: 'Course requires to be a subscriber or a paid member.',
30
+ description: 'Course offers subscription alternative text',
31
+ id: 'components.CourseGlimpseFooter.enrollmentOfferIconSubscriptionAlt',
32
+ },
33
+ certificateOfferIconAlt: {
34
+ defaultMessage: 'The course offers a certification.',
35
+ description: 'Course certificate offer alternative text',
36
+ id: 'components.CourseGlimpseFooter.certificateOfferIconAlt',
37
+ },
13
38
  });
14
39
 
40
+ const courseOfferMessages = {
41
+ [CourseOffer.FREE]: messages.enrollmentOfferIconFreeAlt,
42
+ [CourseOffer.PARTIALLY_FREE]: messages.enrollmentOfferIconPartiallyFreeAlt,
43
+ [CourseOffer.PAID]: messages.enrollmentOfferIconPaidAlt,
44
+ [CourseOffer.SUBSCRIPTION]: messages.enrollmentOfferIconSubscriptionAlt,
45
+ };
46
+
47
+ type OfferIconType =
48
+ | IconTypeEnum.OFFER_SUBSCRIPTION
49
+ | IconTypeEnum.OFFER_PAID
50
+ | IconTypeEnum.OFFER_PARTIALLY_FREE
51
+ | IconTypeEnum.OFFER_FREE;
52
+
15
53
  /**
16
54
  * <CourseGlimpseFooter />.
17
55
  * This is spun off from <CourseGlimpse /> to allow easier override through webpack.
@@ -20,19 +58,52 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
20
58
  course,
21
59
  }) => {
22
60
  const intl = useIntl();
61
+ const offer = course.offer ?? CourseOffer.FREE;
62
+ const certificateOffer = course.certificate_offer ?? null;
63
+ const hasCertificateOffer = certificateOffer !== null;
64
+ const hasEnrollmentOffer = offer !== CourseOffer.FREE;
65
+ const offerIcon = `icon-offer-${offer}` as OfferIconType;
66
+ const offerCertificateIcon = hasCertificateOffer && IconTypeEnum.SCHOOL;
67
+ const offerPrice = hasEnrollmentOffer && course.price;
68
+
23
69
  return (
24
70
  <div className="course-glimpse-footer">
25
- <div className="course-glimpse-footer__date">
26
- <Icon name={IconTypeEnum.CALENDAR} title={intl.formatMessage(messages.dateIconAlt)} />
27
- {course.state.text.charAt(0).toUpperCase() +
28
- course.state.text.substring(1) +
29
- (course.state.datetime
30
- ? ` ${intl.formatDate(new Date(course.state.datetime!), {
31
- year: 'numeric',
32
- month: 'short',
33
- day: 'numeric',
34
- })}`
35
- : '')}
71
+ <div className="course-glimpse-footer__column course-glimpse-footer__date">
72
+ <Icon
73
+ name={IconTypeEnum.CALENDAR}
74
+ title={intl.formatMessage(messages.dateIconAlt)}
75
+ size="small"
76
+ />
77
+ <span>
78
+ {course.state.text.charAt(0).toUpperCase() +
79
+ course.state.text.substring(1) +
80
+ (course.state.datetime
81
+ ? ` ${intl.formatDate(new Date(course.state.datetime!), {
82
+ year: 'numeric',
83
+ month: 'long',
84
+ day: 'numeric',
85
+ })}`
86
+ : '')}
87
+ </span>
88
+ </div>
89
+ <div className="course-glimpse-footer__column course-glimpse-footer__price">
90
+ {offerCertificateIcon && (
91
+ <Icon
92
+ className="offer-certificate__icon"
93
+ name={offerCertificateIcon}
94
+ title={intl.formatMessage(messages.certificateOfferIconAlt)}
95
+ />
96
+ )}
97
+ <Icon
98
+ className="offer__icon"
99
+ name={offerIcon}
100
+ title={intl.formatMessage(courseOfferMessages[offer])}
101
+ />
102
+ {offerPrice && (
103
+ <span className="offer__price">
104
+ <FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
105
+ </span>
106
+ )}
36
107
  </div>
37
108
  </div>
38
109
  );
@@ -1,9 +1,10 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { render, screen, within } from '@testing-library/react';
2
2
  import { IntlProvider } from 'react-intl';
3
3
  import { MemoryRouter } from 'react-router';
4
4
  import { CommonDataProps } from 'types/commonDataProps';
5
5
  import { RichieContextFactory } from 'utils/test/factories/richie';
6
6
  import { CourseStateTextEnum } from 'types';
7
+ import { CourseCertificateOffer, CourseOffer } from 'types/Course';
7
8
  import { CourseGlimpse, CourseGlimpseCourse } from '.';
8
9
 
9
10
  const renderCourseGlimpse = ({
@@ -53,6 +54,11 @@ describe('widgets/Search/components/CourseGlimpse', () => {
53
54
  text: CourseStateTextEnum.STARTING_ON,
54
55
  },
55
56
  title: 'Course 42',
57
+ offer: CourseOffer.PAID,
58
+ price: 42.0,
59
+ certificate_offer: CourseCertificateOffer.FREE,
60
+ certificate_price: null,
61
+ price_currency: 'EUR',
56
62
  };
57
63
 
58
64
  const contextProps: CommonDataProps['context'] = RichieContextFactory().one();
@@ -63,6 +69,11 @@ describe('widgets/Search/components/CourseGlimpse', () => {
63
69
  // first text we encounter should be the title, so that screen reader users get it first
64
70
  expect(container.textContent?.indexOf('Course 42')).toBe(0);
65
71
 
72
+ // The course glimpse container should have the a variant class according to their offers
73
+ const containerElement = container.querySelector('.course-glimpse');
74
+ expect(containerElement).toHaveClass('course-glimpse--offer-paid');
75
+ expect(containerElement).toHaveClass('course-glimpse--offer-certificate');
76
+
66
77
  // The link that wraps the course glimpse should have no title as its content is explicit enough
67
78
  const link = container.querySelector('.course-glimpse__link');
68
79
  expect(link).not.toHaveAttribute('title');
@@ -73,9 +84,9 @@ describe('widgets/Search/components/CourseGlimpse', () => {
73
84
  screen.getByLabelText('Organization');
74
85
  screen.getByText('Some Organization');
75
86
  screen.getByText('Category');
76
- // Matches on 'Starting on Mar 14, 2019', date is wrapped with intl <span>
87
+ // Matches on 'Starting on March 14, 2019', date is wrapped with intl <span>
77
88
  screen.getByLabelText('Course date');
78
- screen.getByText('Starting on Mar 14, 2019');
89
+ screen.getByText('Starting on March 14, 2019');
79
90
 
80
91
  // Check course logo
81
92
  const courseGlipseMedia = container.getElementsByClassName('course-glimpse__media');
@@ -97,6 +108,17 @@ describe('widgets/Search/components/CourseGlimpse', () => {
97
108
  // The logo is rendered along with alt text "" as it is decorative and included in a link block
98
109
  expect(orgImg).toHaveAttribute('alt', '');
99
110
  expect(orgImg).toHaveAttribute('src', '/thumbs/org_small.png');
111
+
112
+ // Check certificate offer
113
+ within(container).getByRole('img', { name: 'The course offers a certification.' });
114
+
115
+ // Check offer information
116
+ const offerIcon = within(container).getByRole('img', { name: 'Course requires a payment.' });
117
+ const useElement = offerIcon.lastChild;
118
+ expect(useElement).toHaveAttribute('href', '#icon-offer-paid');
119
+
120
+ const offerPrice = offerIcon.nextSibling;
121
+ expect(offerPrice).toHaveTextContent('€42.00');
100
122
  });
101
123
 
102
124
  it('works when there is no call to action or datetime on the state (eg. an archived course)', () => {
@@ -115,8 +137,8 @@ describe('widgets/Search/components/CourseGlimpse', () => {
115
137
  // Make sure the component renders and shows the state
116
138
  screen.getByRole('heading', { name: 'Course 42', level: 3 });
117
139
  const dateFormatter = Intl.DateTimeFormat('en', {
118
- day: '2-digit',
119
- month: 'short',
140
+ day: 'numeric',
141
+ month: 'long',
120
142
  year: 'numeric',
121
143
  });
122
144
  const formatedDatetime = dateFormatter.format(new Date(course.state.datetime!));
@@ -145,4 +167,57 @@ describe('widgets/Search/components/CourseGlimpse', () => {
145
167
  'course-glimpse__metadata--code',
146
168
  );
147
169
  });
170
+
171
+ it('does not show certificate offer if the course does not offer a certificate', () => {
172
+ const { container } = renderCourseGlimpse({
173
+ contextProps,
174
+ course: { ...course, certificate_offer: null },
175
+ });
176
+
177
+ const containerElement = container.querySelector('.course-glimpse');
178
+ expect(containerElement).not.toHaveClass('course-glimpse--offer-certificate');
179
+
180
+ const certificicateOfferIcon = within(container).queryByRole('img', {
181
+ name: 'The course offers a certification.',
182
+ });
183
+ expect(certificicateOfferIcon).not.toBeInTheDocument();
184
+ });
185
+
186
+ it('does show free course offer if the course has no offer', () => {
187
+ const { container } = renderCourseGlimpse({
188
+ contextProps,
189
+ course: { ...course, offer: null },
190
+ });
191
+
192
+ const containerElement = container.querySelector('.course-glimpse');
193
+ expect(containerElement).toHaveClass('course-glimpse--offer-free');
194
+
195
+ const offerIcon = within(container).getByRole('img', {
196
+ name: 'The entire course can be completed for free.',
197
+ });
198
+ const useElement = offerIcon.lastChild;
199
+ expect(useElement).toHaveAttribute('href', '#icon-offer-free');
200
+
201
+ // And no price is shown
202
+ expect(offerIcon.nextSibling).not.toBeInTheDocument();
203
+ });
204
+
205
+ it.each([
206
+ [CourseOffer.FREE, 'The entire course can be completed for free.'],
207
+ [CourseOffer.PARTIALLY_FREE, 'More than half of the course is for free.'],
208
+ [CourseOffer.PAID, 'Course requires a payment.'],
209
+ [CourseOffer.SUBSCRIPTION, 'Course requires to be a subscriber or a paid member.'],
210
+ ])('does show a specific course offer icon', (offer, altText) => {
211
+ const { container } = renderCourseGlimpse({
212
+ contextProps,
213
+ course: { ...course, offer },
214
+ });
215
+
216
+ const containerElement = container.querySelector('.course-glimpse');
217
+ expect(containerElement).toHaveClass(`course-glimpse--offer-${offer}`);
218
+
219
+ const offerIcon = within(container).getByRole('img', { name: altText });
220
+ const useElement = offerIcon.lastChild;
221
+ expect(useElement).toHaveAttribute('href', `#icon-offer-${offer}`);
222
+ });
148
223
  });
@@ -1,10 +1,12 @@
1
1
  import React, { memo } from 'react';
2
2
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
+ import c from 'classnames';
3
4
 
4
5
  import { Nullable } from 'types/utils';
5
6
  import { CommonDataProps } from 'types/commonDataProps';
6
7
  import { Icon, IconTypeEnum } from 'components/Icon';
7
8
  import { CourseState } from 'types';
9
+ import { CourseCertificateOffer, CourseOffer } from 'types/Course';
8
10
  import { CourseGlimpseFooter } from './CourseGlimpseFooter';
9
11
  import CourseLink from './CourseLink';
10
12
 
@@ -40,6 +42,11 @@ export interface CourseGlimpseCourse {
40
42
  duration?: string;
41
43
  effort?: string;
42
44
  categories?: string[];
45
+ certificate_offer: Nullable<CourseCertificateOffer>;
46
+ offer: Nullable<CourseOffer>;
47
+ certificate_price: Nullable<number>;
48
+ price: Nullable<number>;
49
+ price_currency: string;
43
50
  }
44
51
 
45
52
  export interface CourseGlimpseProps {
@@ -71,91 +78,100 @@ const messages = defineMessages({
71
78
 
72
79
  const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataProps) => {
73
80
  const intl = useIntl();
81
+ const offer = course.offer ?? CourseOffer.FREE;
82
+ const hasCertificateOffer = course.certificate_offer !== null;
74
83
  return (
75
- <div className="course-glimpse" data-testid="course-glimpse">
76
- {/* the media link is only here for mouse users, so hide it for keyboard/screen reader users.
84
+ <div
85
+ className={c('course-glimpse', `course-glimpse--offer-${offer}`, {
86
+ 'course-glimpse--offer-certificate': hasCertificateOffer,
87
+ })}
88
+ data-testid="course-glimpse"
89
+ >
90
+ <div className="course-glimpse__body">
91
+ {/* the media link is only here for mouse users, so hide it for keyboard/screen reader users.
77
92
  Keyboard/sr will focus the link on the title */}
78
- <div aria-hidden="true" className="course-glimpse__media">
79
- <CourseLink
80
- tabIndex={-1}
81
- className="course-glimpse__link"
82
- href={course.course_url}
83
- to={course.course_route}
84
- >
85
- {/* alt forced to empty string because it's a decorative image */}
86
- {course.cover_image ? (
87
- <img
88
- alt=""
89
- sizes={course.cover_image.sizes}
90
- src={course.cover_image.src}
91
- srcSet={course.cover_image.srcset}
92
- />
93
- ) : (
94
- <div className="course-glimpse__media__empty">
95
- <FormattedMessage {...messages.cover} />
96
- </div>
97
- )}
98
- </CourseLink>
99
- </div>
100
- <div className="course-glimpse__content">
101
- <div className="course-glimpse__wrapper">
102
- <h3 className="course-glimpse__title">
103
- <CourseLink
104
- className="course-glimpse__link"
105
- href={course.course_url}
106
- to={course.course_route}
107
- >
108
- <span className="course-glimpse__title-text">{course.title}</span>
109
- </CourseLink>
110
- </h3>
111
- {course.organization.image ? (
112
- <div className="course-glimpse__organization-logo">
113
- {/* alt forced to empty string because the organization name is rendered after */}
93
+ <div aria-hidden="true" className="course-glimpse__media">
94
+ <CourseLink
95
+ tabIndex={-1}
96
+ className="course-glimpse__link"
97
+ href={course.course_url}
98
+ to={course.course_route}
99
+ >
100
+ {/* alt forced to empty string because it's a decorative image */}
101
+ {course.cover_image ? (
114
102
  <img
115
103
  alt=""
116
- sizes={course.organization.image.sizes}
117
- src={course.organization.image.src}
118
- srcSet={course.organization.image.srcset}
104
+ sizes={course.cover_image.sizes}
105
+ src={course.cover_image.src}
106
+ srcSet={course.cover_image.srcset}
119
107
  />
120
- </div>
121
- ) : null}
122
- <div className="course-glimpse__metadata course-glimpse__metadata--organization">
123
- <Icon
124
- name={IconTypeEnum.ORG}
125
- title={intl.formatMessage(messages.organizationIconAlt)}
126
- size="small"
127
- />
128
- <span className="title">{course.organization.title}</span>
129
- </div>
130
- <div className="course-glimpse__metadata course-glimpse__metadata--code">
131
- <Icon
132
- name={IconTypeEnum.BARCODE}
133
- title={intl.formatMessage(messages.codeIconAlt)}
134
- size="small"
135
- />
136
- <span>{course.code || '-'}</span>
137
- </div>
108
+ ) : (
109
+ <div className="course-glimpse__media__empty">
110
+ <FormattedMessage {...messages.cover} />
111
+ </div>
112
+ )}
113
+ </CourseLink>
138
114
  </div>
139
- {course.icon ? (
140
- <div className="course-glimpse__icon">
141
- <span className="category-badge">
142
- {/* alt forced to empty string because it's a decorative image */}
143
- <img
144
- alt=""
145
- className="category-badge__icon"
146
- sizes={course.icon.sizes}
147
- src={course.icon.src}
148
- srcSet={course.icon.srcset}
115
+ <div className="course-glimpse__content">
116
+ <div className="course-glimpse__wrapper">
117
+ <h3 className="course-glimpse__title">
118
+ <CourseLink
119
+ className="course-glimpse__link"
120
+ href={course.course_url}
121
+ to={course.course_route}
122
+ >
123
+ <span className="course-glimpse__title-text">{course.title}</span>
124
+ </CourseLink>
125
+ </h3>
126
+ {course.organization.image ? (
127
+ <div className="course-glimpse__organization-logo">
128
+ {/* alt forced to empty string because the organization name is rendered after */}
129
+ <img
130
+ alt=""
131
+ sizes={course.organization.image.sizes}
132
+ src={course.organization.image.src}
133
+ srcSet={course.organization.image.srcset}
134
+ />
135
+ </div>
136
+ ) : null}
137
+ <div className="course-glimpse__metadata course-glimpse__metadata--organization">
138
+ <Icon
139
+ name={IconTypeEnum.ORG}
140
+ title={intl.formatMessage(messages.organizationIconAlt)}
141
+ size="small"
149
142
  />
150
- <span className="offscreen">
151
- <FormattedMessage {...messages.categoryLabel} />
152
- </span>
153
- <span className="category-badge__title">{course.icon.title}</span>
154
- </span>
143
+ <span className="title">{course.organization.title}</span>
144
+ </div>
145
+ <div className="course-glimpse__metadata course-glimpse__metadata--code">
146
+ <Icon
147
+ name={IconTypeEnum.BARCODE}
148
+ title={intl.formatMessage(messages.codeIconAlt)}
149
+ size="small"
150
+ />
151
+ <span>{course.code || '-'}</span>
152
+ </div>
155
153
  </div>
156
- ) : null}
157
- <CourseGlimpseFooter context={context} course={course} />
154
+ {course.icon ? (
155
+ <div className="course-glimpse__icon">
156
+ <span className="category-badge">
157
+ {/* alt forced to empty string because it's a decorative image */}
158
+ <img
159
+ alt=""
160
+ className="category-badge__icon"
161
+ sizes={course.icon.sizes}
162
+ src={course.icon.src}
163
+ srcSet={course.icon.srcset}
164
+ />
165
+ <span className="offscreen">
166
+ <FormattedMessage {...messages.categoryLabel} />
167
+ </span>
168
+ <span className="category-badge__title">{course.icon.title}</span>
169
+ </span>
170
+ </div>
171
+ ) : null}
172
+ </div>
158
173
  </div>
174
+ <CourseGlimpseFooter context={context} course={course} />
159
175
  </div>
160
176
  );
161
177
  };
@@ -1,10 +1,16 @@
1
1
  import { IntlShape } from 'react-intl';
2
2
  import { generatePath } from 'react-router';
3
- import { Course as RichieCourse, isRichieCourse } from 'types/Course';
3
+ import {
4
+ CourseCertificateOffer,
5
+ CourseOffer,
6
+ Course as RichieCourse,
7
+ isRichieCourse,
8
+ } from 'types/Course';
4
9
  import {
5
10
  CourseListItem as JoanieCourse,
6
11
  CourseProductRelationLight,
7
12
  isCourseProductRelation,
13
+ ProductType,
8
14
  } from 'types/Joanie';
9
15
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
10
16
  import { CourseGlimpseCourse } from '.';
@@ -40,6 +46,20 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
40
46
  product_id: courseProductRelation.product.id,
41
47
  course_route: courseRoute,
42
48
  state: courseProductRelation.product.state,
49
+ certificate_offer:
50
+ courseProductRelation.product.type === ProductType.CERTIFICATE
51
+ ? CourseCertificateOffer.PAID
52
+ : null,
53
+ offer: courseProductRelation.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
54
+ certificate_price:
55
+ courseProductRelation.product.type === ProductType.CERTIFICATE
56
+ ? courseProductRelation.product.price
57
+ : null,
58
+ price:
59
+ courseProductRelation.product.type === ProductType.CREDENTIAL
60
+ ? courseProductRelation.product.price
61
+ : null,
62
+ price_currency: courseProductRelation.product.price_currency,
43
63
  };
44
64
  };
45
65
 
@@ -59,6 +79,11 @@ const getCourseGlimpsePropsFromRichieCourse = (course: RichieCourse): CourseGlim
59
79
  effort: course.effort,
60
80
  categories: course.categories,
61
81
  organizations: course.organizations,
82
+ price: course.price,
83
+ price_currency: course.price_currency,
84
+ certificate_offer: course.certificate_offer,
85
+ offer: course.offer,
86
+ certificate_price: course.certificate_price,
62
87
  });
63
88
 
64
89
  const getCourseGlimpsePropsFromJoanieCourse = (
@@ -91,6 +116,11 @@ const getCourseGlimpsePropsFromJoanieCourse = (
91
116
  },
92
117
  state: course.state,
93
118
  nb_course_runs: course.course_run_ids.length,
119
+ price: null,
120
+ price_currency: 'EUR',
121
+ certificate_offer: null,
122
+ offer: null,
123
+ certificate_price: null,
94
124
  };
95
125
  };
96
126
 
@@ -35,6 +35,7 @@ export enum IconTypeEnum {
35
35
  CHEVRON_RIGHT_OUTLINE = 'icon-chevron-right-outline',
36
36
  CHEVRON_UP_OUTLINE = 'icon-chevron-up-outline',
37
37
  CLOCK = 'icon-clock',
38
+ COURSES = 'icon-courses',
38
39
  CREDIT_CARD = 'icon-creditCard',
39
40
  CROSS = 'icon-cross',
40
41
  DURATION = 'icon-duration',
@@ -49,6 +50,7 @@ export enum IconTypeEnum {
49
50
  LOGOUT_SQUARE = 'icon-logout-square',
50
51
  MAGNIFYING_GLASS = 'icon-magnifying-glass',
51
52
  MENU = 'icon-menu',
53
+ MONEY = 'icon-money',
52
54
  MORE = 'icon-more',
53
55
  ORG = 'icon-org',
54
56
  PACE = 'icon-pace',
@@ -62,6 +64,11 @@ export enum IconTypeEnum {
62
64
  TWITTER = 'icon-twitter',
63
65
  UNIVERSITY = 'icon-univerity',
64
66
  WARNING = 'icon-warning',
67
+ VIDEO_PLAY = 'icon-video-play',
68
+ OFFER_PAID = 'icon-offer-paid',
69
+ OFFER_FREE = 'icon-offer-free',
70
+ OFFER_PARTIALLY_FREE = 'icon-offer-partially_free',
71
+ OFFER_SUBSCRIPTION = 'icon-offer-subscription',
65
72
  }
66
73
 
67
74
  export const Icon = ({ name, title, className = '', size = 'medium', ...props }: Props) => {
@@ -15,7 +15,7 @@ import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
15
15
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
16
16
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
17
17
  import { AppWrapperProps } from 'utils/test/wrappers/types';
18
- import { expectBannerError } from 'utils/test/expectBanner';
18
+ import { expectAlertError, expectAlertWarning, expectNoAlertWarning } from 'utils/test/expectAlert';
19
19
 
20
20
  jest.mock('utils/context', () => ({
21
21
  __esModule: true,
@@ -71,7 +71,7 @@ describe('OpenEdxFullNameForm', () => {
71
71
  wrapper: Wrapper,
72
72
  });
73
73
 
74
- const $input = await screen.findByRole('textbox', { name: 'Full name' });
74
+ const $input = await screen.findByRole('textbox', { name: 'First name and last name' });
75
75
  expect($input).toHaveValue('');
76
76
  });
77
77
 
@@ -93,7 +93,7 @@ describe('OpenEdxFullNameForm', () => {
93
93
  wrapper: Wrapper,
94
94
  });
95
95
 
96
- const $input = await screen.findByRole('textbox', { name: 'Full name' });
96
+ const $input = await screen.findByRole('textbox', { name: 'First name and last name' });
97
97
  expect($input).toHaveValue(user.full_name);
98
98
  });
99
99
 
@@ -119,15 +119,25 @@ describe('OpenEdxFullNameForm', () => {
119
119
 
120
120
  expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
121
121
 
122
- const $input = await screen.findByRole('textbox', { name: 'Full name' });
122
+ const $input = await screen.findByRole('textbox', { name: 'First name and last name' });
123
123
  expect($input).toHaveValue('');
124
124
 
125
+ await expectAlertWarning(
126
+ 'Please check that your first name and last name are correct. They will be used on official document (e.g: certificate, contract, etc.)',
127
+ );
128
+
125
129
  // Submit the form
126
130
  await act(async () => {
127
131
  await expect(submitCallbacks.openEdxFullNameForm()).rejects.not.toBeUndefined();
128
132
  });
129
133
 
130
134
  screen.getByText('This field is required.');
135
+ await expectNoAlertWarning(
136
+ 'Please check that your first name and last name are correct. They will be used on official document (e.g: certificate, contract, etc.)',
137
+ );
138
+ await expectAlertError(
139
+ 'Please check that your first name and last name are correct. They will be used on official document (e.g: certificate, contract, etc.)',
140
+ );
131
141
  });
132
142
 
133
143
  it('should require a value with at least 3 chars to submit the form', async () => {
@@ -152,7 +162,7 @@ describe('OpenEdxFullNameForm', () => {
152
162
 
153
163
  expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
154
164
 
155
- const $input = await screen.findByRole('textbox', { name: 'Full name' });
165
+ const $input = await screen.findByRole('textbox', { name: 'First name and last name' });
156
166
  expect($input).toHaveValue('');
157
167
 
158
168
  const eventHandler = userEvent.setup();
@@ -189,7 +199,7 @@ describe('OpenEdxFullNameForm', () => {
189
199
 
190
200
  expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
191
201
 
192
- const $input = await screen.findByRole('textbox', { name: 'Full name' });
202
+ const $input = await screen.findByRole('textbox', { name: 'First name and last name' });
193
203
  expect($input).toHaveValue('');
194
204
 
195
205
  const eventHandler = userEvent.setup();
@@ -224,6 +234,6 @@ describe('OpenEdxFullNameForm', () => {
224
234
  wrapper: Wrapper,
225
235
  });
226
236
 
227
- await expectBannerError('An error occurred while fetching your profile. Please retry later.');
237
+ await expectAlertError('An error occurred while fetching your profile. Please retry later.');
228
238
  });
229
239
  });