richie-education 2.25.0-b2.dev58 → 2.25.0-b2.dev63

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.
@@ -3,7 +3,7 @@ import { useState } from 'react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
4
  import { PaymentErrorMessageId } from 'components/PaymentButton/index';
5
5
  import { Product } from 'types/Joanie';
6
- import { useJoanieApi } from 'contexts/JoanieApiContext';
6
+ import context from 'utils/context';
7
7
 
8
8
  const messages = defineMessages({
9
9
  termsMessage: {
@@ -33,7 +33,6 @@ export const useTerms = ({
33
33
  error?: PaymentErrorMessageId;
34
34
  }) => {
35
35
  const intl = useIntl();
36
- const api = useJoanieApi();
37
36
  const [termsAccepted, setTermsAccepted] = useState(false);
38
37
  const validateTerms = () => {
39
38
  if (!product.contract_definition) {
@@ -44,18 +43,6 @@ export const useTerms = ({
44
43
  }
45
44
  };
46
45
 
47
- const openContract = async (e: React.MouseEvent) => {
48
- if (!product.contract_definition) {
49
- return;
50
- }
51
- e.stopPropagation();
52
- e.preventDefault();
53
- const blob = await api.contractDefinitions.previewTemplate(product.contract_definition.id);
54
- // eslint-disable-next-line compat/compat
55
- const file = window.URL.createObjectURL(blob);
56
- window.open(file);
57
- };
58
-
59
46
  return {
60
47
  termsAccepted,
61
48
  validateTerms,
@@ -66,12 +53,14 @@ export const useTerms = ({
66
53
  label={
67
54
  <>
68
55
  {intl.formatMessage(messages.termsMessage)}{' '}
69
- <button
70
- onClick={openContract}
56
+ <a
57
+ href={context.site_urls.terms_and_conditions ?? '#'}
58
+ target="_blank"
59
+ rel="noopener noreferrer"
71
60
  title={intl.formatMessage(messages.termsMessageLinkTitle)}
72
61
  >
73
62
  {intl.formatMessage(messages.termsMessageLink)}
74
- </button>
63
+ </a>
75
64
  </>
76
65
  }
77
66
  onChange={(e) => setTermsAccepted(e.target.checked)}
@@ -54,6 +54,9 @@ jest.mock('utils/context', () => ({
54
54
  joanie_backend: {
55
55
  endpoint: 'https://joanie.test',
56
56
  },
57
+ site_urls: {
58
+ terms_and_conditions: '/en/about/terms-and-conditions/',
59
+ },
57
60
  }).one(),
58
61
  }));
59
62
 
@@ -177,7 +180,9 @@ describe.each([
177
180
  </Wrapper>,
178
181
  );
179
182
 
180
- const $terms = screen.getByLabelText('By checking this box, you accept the');
183
+ const $terms = screen.getByLabelText(
184
+ 'By checking this box, you accept the General Terms of Sale',
185
+ );
181
186
  await act(async () => {
182
187
  fireEvent.click($terms);
183
188
  });
@@ -330,7 +335,9 @@ describe.each([
330
335
  nbApiCalls += 1; // fetch order for useProductOrder
331
336
  expect(fetchMock.calls()).toHaveLength(nbApiCalls);
332
337
 
333
- const $terms = screen.getByLabelText('By checking this box, you accept the');
338
+ const $terms = screen.getByLabelText(
339
+ 'By checking this box, you accept the General Terms of Sale',
340
+ );
334
341
  await act(async () => {
335
342
  fireEvent.click($terms);
336
343
  });
@@ -455,7 +462,9 @@ describe.each([
455
462
  expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
456
463
  });
457
464
 
458
- const $terms = screen.getByLabelText('By checking this box, you accept the');
465
+ const $terms = screen.getByLabelText(
466
+ 'By checking this box, you accept the General Terms of Sale',
467
+ );
459
468
  await act(async () => {
460
469
  fireEvent.click($terms);
461
470
  });
@@ -590,7 +599,9 @@ describe.each([
590
599
  `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
591
600
  );
592
601
 
593
- const $terms = screen.getByLabelText('By checking this box, you accept the');
602
+ const $terms = screen.getByLabelText(
603
+ 'By checking this box, you accept the General Terms of Sale',
604
+ );
594
605
  await act(async () => {
595
606
  fireEvent.click($terms);
596
607
  });
@@ -703,7 +714,9 @@ describe.each([
703
714
  nbApiCalls += 1; // useProductOrder get order with filters
704
715
  expect(fetchMock.calls()).toHaveLength(nbApiCalls);
705
716
 
706
- const $terms = screen.getByLabelText('By checking this box, you accept the');
717
+ const $terms = screen.getByLabelText(
718
+ 'By checking this box, you accept the General Terms of Sale',
719
+ );
707
720
  await act(async () => {
708
721
  fireEvent.click($terms);
709
722
  });
@@ -802,17 +815,9 @@ describe.each([
802
815
  });
803
816
 
804
817
  it('should be able to preview the contract if product has a contract definition', async () => {
805
- // eslint-disable-next-line compat/compat
806
- URL.createObjectURL = jest.fn((blob) => blob) as any;
807
- window.open = jest.fn();
808
-
809
818
  const product: Joanie.Product = ProductFactory().one();
810
819
  const billingAddress: Joanie.Address = AddressFactory().one();
811
820
 
812
- const PREVIEW_URL = `https://joanie.test/api/v1.0/contract_definitions/${
813
- product.contract_definition!.id
814
- }/preview_template/`;
815
-
816
821
  const fetchOrderQueryParams =
817
822
  product.type === ProductType.CREDENTIAL
818
823
  ? {
@@ -826,12 +831,10 @@ describe.each([
826
831
  state: ['pending', 'validated', 'submitted'],
827
832
  };
828
833
 
829
- fetchMock
830
- .get(
831
- `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
832
- [],
833
- )
834
- .get(PREVIEW_URL, 'preview content');
834
+ fetchMock.get(
835
+ `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
836
+ [],
837
+ );
835
838
 
836
839
  render(
837
840
  <Wrapper client={createTestQueryClient({ user: true })} product={product}>
@@ -839,25 +842,8 @@ describe.each([
839
842
  </Wrapper>,
840
843
  );
841
844
 
842
- const $terms = screen.getByRole('button', { name: 'General Terms of Sale' });
843
-
844
- // eslint-disable-next-line compat/compat
845
- expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
846
- expect(window.open).toHaveBeenCalledTimes(0);
847
- expect(fetchMock.called(PREVIEW_URL)).toBe(false);
848
-
849
- // console.log($terms);
850
- await act(async () => {
851
- fireEvent.click($terms);
852
- });
853
-
854
- expect(fetchMock.called(PREVIEW_URL)).toBe(true);
855
- // eslint-disable-next-line compat/compat
856
- expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
857
- // eslint-disable-next-line compat/compat
858
- expect(URL.createObjectURL).toHaveBeenCalledWith('preview content');
859
- expect(window.open).toHaveBeenCalledTimes(1);
860
- expect(window.open).toHaveBeenCalledWith('preview content');
845
+ const $terms = screen.getByRole('link', { name: 'General Terms of Sale' });
846
+ expect($terms).toHaveAttribute('href', '/en/about/terms-and-conditions/');
861
847
  });
862
848
 
863
849
  it('should not show terms checkbox if the product does not have a contract definition', async () => {
@@ -959,7 +945,9 @@ describe.each([
959
945
  name: `Pay ${formatPrice(product.price, product.price_currency)}`,
960
946
  }) as HTMLButtonElement;
961
947
 
962
- const $terms = screen.getByLabelText('By checking this box, you accept the');
948
+ const $terms = screen.getByLabelText(
949
+ 'By checking this box, you accept the General Terms of Sale',
950
+ );
963
951
  await act(async () => {
964
952
  fireEvent.click($terms);
965
953
  });
@@ -31,6 +31,9 @@ export interface RichieContext {
31
31
  release: string;
32
32
  sentry_dsn: Nullable<string>;
33
33
  web_analytics_providers?: Nullable<string[]>;
34
+ site_urls: {
35
+ terms_and_conditions: Nullable<string>;
36
+ };
34
37
  }
35
38
 
36
39
  export interface CommonDataProps {
@@ -27,9 +27,10 @@ describe('CreditCardHelper', () => {
27
27
  });
28
28
 
29
29
  it('is soon expired', () => {
30
- const expirationDate = faker.date.future({
31
- refDate: new Date(),
32
- years: 2.99 / 12,
30
+ const now = new Date();
31
+ const expirationDate = faker.date.between({
32
+ from: now,
33
+ to: new Date(now.getFullYear(), now.getMonth() + 4, 0, 23, 59, 59),
33
34
  });
34
35
 
35
36
  expect(
@@ -43,7 +44,10 @@ describe('CreditCardHelper', () => {
43
44
  });
44
45
 
45
46
  it('is expired', () => {
46
- const date = faker.date.past();
47
+ const now = new Date();
48
+ const date = faker.date.past({
49
+ refDate: new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59),
50
+ });
47
51
  expect(
48
52
  CreditCardHelper.getExpirationState(
49
53
  CreditCardFactory({
@@ -15,16 +15,29 @@ export class CreditCardHelper {
15
15
  if (!creditCard.expiration_month || !creditCard.expiration_year) {
16
16
  return false;
17
17
  }
18
- const expirationDate = new Date(creditCard.expiration_year, creditCard.expiration_month - 1, 1);
19
- const limitDate = new Date();
20
- limitDate.setMonth(limitDate.getMonth() + 3);
21
- return limitDate.getTime() > expirationDate.getTime();
18
+ const expirationDate = new Date(
19
+ creditCard.expiration_year,
20
+ creditCard.expiration_month,
21
+ 0,
22
+ 23,
23
+ 59,
24
+ 59,
25
+ );
26
+ const limitDate = new Date(new Date().getFullYear(), new Date().getMonth() + 4, 0, 23, 59, 59);
27
+ return limitDate.getTime() >= expirationDate.getTime();
22
28
  }
23
29
 
24
30
  static isExpired(creditCard: CreditCard): boolean {
25
- const expirationDate = new Date(creditCard.expiration_year, creditCard.expiration_month - 1, 1);
31
+ const expirationDate = new Date(
32
+ creditCard.expiration_year,
33
+ creditCard.expiration_month,
34
+ 0,
35
+ 23,
36
+ 59,
37
+ 59,
38
+ );
26
39
  const now = new Date();
27
- return expirationDate.getTime() < now.getTime();
40
+ return expirationDate.getTime() <= now.getTime();
28
41
  }
29
42
 
30
43
  static getExpirationState(creditCard: CreditCard): CreditCardExpirationStatus {
@@ -181,6 +181,9 @@ export const RichieContextFactory = factory<CommonDataProps['context']>(() => ({
181
181
  release: faker.system.semver(),
182
182
  sentry_dsn: null,
183
183
  web_analytics_providers: null,
184
+ site_urls: {
185
+ terms_and_conditions: null,
186
+ },
184
187
  }));
185
188
 
186
189
  export const CourseLightFactory = factory<Course>(() => {
@@ -1,11 +1,14 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { IntlProvider } from 'react-intl';
3
3
  import { PropsWithChildren } from 'react';
4
- import { EnrollmentFactory } from 'utils/test/factories/joanie';
4
+ import { CredentialOrderFactory, EnrollmentFactory } from 'utils/test/factories/joanie';
5
5
  import { Priority } from 'types';
6
- import { Enrollment } from 'types/Joanie';
7
- import { DATETIME_FORMAT } from 'hooks/useDateFormat';
8
- import { Enrolled } from './DashboardItemCourseEnrolling';
6
+ import { CourseRun, Enrollment } from 'types/Joanie';
7
+ import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
8
+ import { CourseRunFactoryFromPriority } from 'utils/test/factories/richie';
9
+ import { noop } from 'utils';
10
+ import { computeState } from 'utils/CourseRuns';
11
+ import { DashboardItemCourseEnrollingRun, Enrolled } from './DashboardItemCourseEnrolling';
9
12
 
10
13
  /**
11
14
  * Most of the component of this file are tested from DashboardItemEnrollment.spec.tsx and
@@ -16,49 +19,109 @@ describe('<Enrolled/>', () => {
16
19
  return <IntlProvider locale="en">{children}</IntlProvider>;
17
20
  };
18
21
 
19
- const runTest = async (priority: Priority, expectButton: boolean) => {
20
- const enrollment: Enrollment = EnrollmentFactory().one();
21
- enrollment.course_run.state.priority = priority;
22
- render(<Enrolled enrollment={enrollment} />, { wrapper });
23
- await screen.findByText(
24
- 'You are enrolled for the session from ' +
25
- new Intl.DateTimeFormat('en', DATETIME_FORMAT).format(
26
- new Date(enrollment.course_run.start),
27
- ) +
28
- ' to ' +
29
- new Intl.DateTimeFormat('en', DATETIME_FORMAT).format(new Date(enrollment.course_run.end)),
30
- );
31
- if (expectButton) {
32
- const link = screen.getByRole('link', { name: 'Access course' });
33
- expect(link).toBeEnabled();
34
- expect(link).toHaveAttribute('href', enrollment.course_run.resource_link);
35
- } else {
36
- expect(screen.queryByRole('link', { name: 'Access course' })).toBeNull();
37
- }
38
- };
22
+ it.each([
23
+ {
24
+ buttonTestLabel: 'and access course button',
25
+ priority: Priority.ONGOING_OPEN,
26
+ expectButton: true,
27
+ },
28
+ {
29
+ buttonTestLabel: 'and no access course button',
30
+ priority: Priority.FUTURE_OPEN,
31
+ expectButton: false,
32
+ },
33
+ {
34
+ buttonTestLabel: 'and access course button',
35
+ priority: Priority.ARCHIVED_OPEN,
36
+ expectButton: true,
37
+ },
38
+ {
39
+ buttonTestLabel: 'and no access course button',
40
+ priority: Priority.FUTURE_NOT_YET_OPEN,
41
+ expectButton: false,
42
+ },
43
+ {
44
+ buttonTestLabel: 'and no access course button',
45
+ priority: Priority.FUTURE_CLOSED,
46
+ expectButton: false,
47
+ },
48
+ {
49
+ buttonTestLabel: 'and access course button',
50
+ priority: Priority.ONGOING_CLOSED,
51
+ expectButton: true,
52
+ },
53
+ {
54
+ buttonTestLabel: 'and access course button',
55
+ priority: Priority.ARCHIVED_CLOSED,
56
+ expectButton: true,
57
+ },
58
+ {
59
+ buttonTestLabel: 'and no access course button',
60
+ priority: Priority.TO_BE_SCHEDULED,
61
+ expectButton: false,
62
+ },
63
+ ])(
64
+ 'handles enrollments with priority=$priority $buttonTestLabel',
65
+ async ({ priority, expectButton }) => {
66
+ const enrollment: Enrollment = EnrollmentFactory().one();
67
+ enrollment.course_run.state.priority = priority;
68
+ render(<Enrolled enrollment={enrollment} />, { wrapper });
69
+ await screen.findByText(
70
+ 'You are enrolled for the session from ' +
71
+ new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
72
+ new Date(enrollment.course_run.start),
73
+ ) +
74
+ ' to ' +
75
+ new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
76
+ new Date(enrollment.course_run.end),
77
+ ),
78
+ );
79
+ if (expectButton) {
80
+ const link = screen.getByRole('link', { name: 'Access to course' });
81
+ expect(link).toBeEnabled();
82
+ expect(link).toHaveAttribute('href', enrollment.course_run.resource_link);
83
+ } else {
84
+ expect(screen.queryByRole('link', { name: 'Access to course' })).toBeNull();
85
+ }
86
+ },
87
+ );
88
+ });
89
+
90
+ describe('<DashboardItemCourseEnrollingRun/>', () => {
91
+ it.each([
92
+ [Priority.ONGOING_OPEN, false],
93
+ [Priority.FUTURE_OPEN, false],
94
+ [Priority.ARCHIVED_OPEN, false],
95
+ [Priority.FUTURE_NOT_YET_OPEN, true],
96
+ [Priority.FUTURE_CLOSED, false],
97
+ [Priority.ONGOING_CLOSED, false],
98
+ [Priority.ARCHIVED_CLOSED, false],
99
+ [Priority.TO_BE_SCHEDULED, false],
100
+ ])(
101
+ `handles correctly enrollment_start date displaying with priority=%s`,
102
+ async (priority, expectEnrollmentNotYetOpened) => {
103
+ const order = CredentialOrderFactory().one();
104
+ const courseRun = CourseRunFactoryFromPriority(priority)().one();
105
+ courseRun.state = computeState(courseRun);
106
+ const joanieCourseRun = courseRun as unknown as CourseRun;
107
+ joanieCourseRun.course = order.course;
108
+
109
+ render(
110
+ <IntlProvider locale="en">
111
+ <DashboardItemCourseEnrollingRun
112
+ order={order}
113
+ courseRun={joanieCourseRun}
114
+ selected={false}
115
+ enroll={noop}
116
+ />
117
+ </IntlProvider>,
118
+ );
39
119
 
40
- it('handles enrollments with priority=ONGOING_OPEN', () => {
41
- runTest(Priority.ONGOING_OPEN, true);
42
- });
43
- it('handles enrollments with priority=FUTURE_OPEN', () => {
44
- runTest(Priority.FUTURE_OPEN, false);
45
- });
46
- it('handles enrollments with priority=ARCHIVED_OPEN', () => {
47
- runTest(Priority.ARCHIVED_OPEN, true);
48
- });
49
- it('handles enrollments with priority=FUTURE_NOT_YET_OPEN', () => {
50
- runTest(Priority.FUTURE_NOT_YET_OPEN, false);
51
- });
52
- it('handles enrollments with priority=FUTURE_CLOSED', () => {
53
- runTest(Priority.FUTURE_CLOSED, false);
54
- });
55
- it('handles enrollments with priority=ONGOING_CLOSED', () => {
56
- runTest(Priority.ONGOING_CLOSED, true);
57
- });
58
- it('handles enrollments with priority=ARCHIVED_CLOSED', () => {
59
- runTest(Priority.ARCHIVED_CLOSED, true);
60
- });
61
- it('handles enrollments with priority=TO_BE_SCHEDULED', () => {
62
- runTest(Priority.TO_BE_SCHEDULED, false);
63
- });
120
+ if (expectEnrollmentNotYetOpened) {
121
+ screen.getByText(/Enrollment will open on/);
122
+ } else {
123
+ expect(screen.queryByText(/Enrollment will open on/)).not.toBeInTheDocument();
124
+ }
125
+ },
126
+ );
64
127
  });
@@ -1,15 +1,15 @@
1
- import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
1
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
2
2
  import { useMemo } from 'react';
3
3
  import { Button } from '@openfun/cunningham-react';
4
4
  import { CoursesHelper } from 'utils/CoursesHelper';
5
5
  import { Priority } from 'types';
6
6
  import {
7
+ AbstractCourse,
8
+ CertificateOrder,
7
9
  CourseRun,
8
- Enrollment,
9
10
  CredentialOrder,
10
- AbstractCourse,
11
+ Enrollment,
11
12
  Product,
12
- CertificateOrder,
13
13
  } from 'types/Joanie';
14
14
  import { Spinner } from 'components/Spinner';
15
15
  import Banner, { BannerType } from 'components/Banner';
@@ -208,7 +208,7 @@ interface DashboardItemCourseEnrollingRunProps {
208
208
  product?: Product;
209
209
  }
210
210
 
211
- const DashboardItemCourseEnrollingRun = ({
211
+ export const DashboardItemCourseEnrollingRun = ({
212
212
  courseRun,
213
213
  selected,
214
214
  enroll,
@@ -247,7 +247,7 @@ const DashboardItemCourseEnrollingRun = ({
247
247
  }}
248
248
  />
249
249
  </div>
250
- {!isOpenedForEnrollment && (
250
+ {courseRun.state.priority === Priority.FUTURE_NOT_YET_OPEN && (
251
251
  <div className="dashboard-item__course-enrolling__run__not-opened">
252
252
  <FormattedMessage
253
253
  {...messages.enrollmentNotYetOpened}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev58",
3
+ "version": "2.25.0-b2.dev63",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {