richie-education 2.25.0-b2.dev176 → 2.25.0-b2.dev183

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 (31) hide show
  1. package/js/api/joanie.spec.ts +2 -1
  2. package/js/api/joanie.ts +1 -51
  3. package/js/api/lms/dummy.ts +22 -0
  4. package/js/api/lms/openedx-fonzie.spec.ts +20 -0
  5. package/js/api/lms/openedx-fonzie.ts +35 -0
  6. package/js/api/utils.ts +51 -0
  7. package/js/components/AddressesManagement/AddressForm/index.tsx +15 -14
  8. package/js/components/Form/Form/index.tsx +23 -0
  9. package/js/components/Form/index.ts +3 -0
  10. package/js/hooks/useDashboardAddressForm.tsx +13 -12
  11. package/js/hooks/useOpenEdxProfile/index.ts +45 -0
  12. package/js/hooks/useOpenEdxProfile/utils/index.spec.ts +69 -0
  13. package/js/hooks/useOpenEdxProfile/utils/index.ts +131 -0
  14. package/js/pages/DashboardAddressesManagement/_styles.scss +1 -1
  15. package/js/pages/DashboardAddressesManagement/index.spec.tsx +34 -12
  16. package/js/pages/DashboardAddressesManagement/index.tsx +14 -9
  17. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +156 -115
  18. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +9 -8
  19. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +92 -121
  20. package/js/pages/DashboardOpenEdxProfile/index.spec.tsx +131 -0
  21. package/js/pages/DashboardOpenEdxProfile/index.tsx +237 -0
  22. package/js/pages/DashboardPreferences/index.tsx +2 -0
  23. package/js/types/api.ts +6 -1
  24. package/js/types/openEdx.ts +41 -0
  25. package/js/utils/react-query/useSessionMutation/index.spec.tsx +1 -1
  26. package/js/utils/react-query/useSessionQuery/index.spec.tsx +1 -1
  27. package/js/utils/test/factories/openEdx.tsx +23 -0
  28. package/js/widgets/Dashboard/components/DashboardBox/_styles.scss +6 -0
  29. package/js/widgets/Dashboard/components/DashboardBox/index.tsx +4 -0
  30. package/package.json +1 -1
  31. package/scss/objects/_form.scss +9 -2
@@ -1,7 +1,8 @@
1
1
  import fetchMock from 'fetch-mock';
2
2
  import { ResourcesQuery } from 'hooks/useResources';
3
3
  import { HttpStatusCode } from 'utils/errors/HttpError';
4
- import { buildApiUrl, getResponseBody } from './joanie';
4
+ import { buildApiUrl } from './joanie';
5
+ import { getResponseBody } from './utils';
5
6
 
6
7
  describe('api/joanie', () => {
7
8
  it('getResponse should handle empty response body', async () => {
package/js/api/joanie.ts CHANGED
@@ -12,61 +12,11 @@ import queryString from 'query-string';
12
12
  import type * as Joanie from 'types/Joanie';
13
13
  import { AuthenticationApi } from 'api/authentication';
14
14
  import context from 'utils/context';
15
- import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
16
15
  import { JOANIE_API_VERSION } from 'settings';
17
16
  import { ResourcesQuery } from 'hooks/useResources';
18
17
  import { ObjectHelper } from 'utils/ObjectHelper';
19
18
  import { Maybe } from 'types/utils';
20
-
21
- interface CheckStatusOptions {
22
- fallbackValue: any;
23
- ignoredErrorStatus: (number | HttpStatusCode)[];
24
- }
25
-
26
- export async function getFileFromResponse(response: Response): Promise<File> {
27
- const filenameRegex = /filename="(.*)";/;
28
- const dispositionHeader = response.headers.get('Content-Disposition');
29
- const matches = dispositionHeader?.match(filenameRegex);
30
-
31
- return new File([await response.blob()], matches ? matches[1] : '', {
32
- type: response.headers.get('Content-Type') || '',
33
- });
34
- }
35
-
36
- export function getResponseBody(response: Response) {
37
- if (response.headers.get('Content-Type') === 'application/json') {
38
- return response.json();
39
- }
40
- const fileType = ['application/pdf', 'application/zip'];
41
- if (fileType.includes(response.headers.get('Content-Type') || '')) {
42
- return new Promise((resolve) => resolve(response));
43
- }
44
- return response.text();
45
- }
46
-
47
- /*
48
- A util to manage Joanie API responses.
49
- It parses properly the response according to its `Content-Type`
50
- otherwise it throws an `HttpError`.
51
-
52
- `options` arguments accept an array of ignoredErrorStatus. If the response
53
- fails with one of this status code, the `fallbackValue` will return and no error will
54
- be raised.
55
- */
56
- export function checkStatus(
57
- response: Response,
58
- options: CheckStatusOptions = { fallbackValue: null, ignoredErrorStatus: [] },
59
- ): Promise<any> {
60
- if (response.ok) {
61
- return getResponseBody(response);
62
- }
63
-
64
- if (options.ignoredErrorStatus.includes(response.status)) {
65
- return Promise.resolve(options.fallbackValue);
66
- }
67
-
68
- throw new HttpError(response.status, response.statusText);
69
- }
19
+ import { checkStatus, getFileFromResponse } from './utils';
70
20
 
71
21
  /*
72
22
  Generate default headers used for most of Joanie requests. It defined `Content-Type`
@@ -6,6 +6,12 @@ import { UnknownEnrollment, OpenEdXEnrollment } from 'types';
6
6
  import { location } from 'utils/indirection/window';
7
7
  import { CURRENT_JOANIE_DEV_DEMO_USER, RICHIE_USER_TOKEN } from 'settings';
8
8
  import { base64Decode } from 'utils/base64Parser';
9
+ import {
10
+ OpenEdxGender,
11
+ OpenEdxLanguageIsoCode,
12
+ OpenEdxLevelOfEducation,
13
+ OpenEdxApiProfile,
14
+ } from 'types/openEdx';
9
15
 
10
16
  type JWTPayload = {
11
17
  email: string;
@@ -89,6 +95,22 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
89
95
  localStorage.removeItem(RICHIE_DUMMY_IS_LOGGED_IN);
90
96
  },
91
97
  accessToken: () => sessionStorage.getItem(RICHIE_USER_TOKEN),
98
+ account: {
99
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
100
+ get: (username: string): Promise<OpenEdxApiProfile> => {
101
+ return Promise.resolve({
102
+ username: 'j_do',
103
+ name: 'John Do',
104
+ email: 'j.do@whois.net',
105
+ country: 'fr',
106
+ level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
107
+ gender: OpenEdxGender.MALE,
108
+ year_of_birth: '1971',
109
+ 'pref-lang': OpenEdxLanguageIsoCode.ENGLISH,
110
+ language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
111
+ } as OpenEdxApiProfile);
112
+ },
113
+ },
92
114
  },
93
115
  enrollment: {
94
116
  get: async (url: string, user: Nullable<User>) =>
@@ -1,6 +1,7 @@
1
1
  import { faker } from '@faker-js/faker';
2
2
  import fetchMock from 'fetch-mock';
3
3
  import { RICHIE_USER_TOKEN } from 'settings';
4
+ import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
4
5
  import FonzieAPIInterface from './openedx-fonzie';
5
6
 
6
7
  jest.mock('utils/context', () => ({
@@ -30,6 +31,25 @@ describe('Fonzie API', () => {
30
31
  await expect(api.user.me()).resolves.toEqual(user);
31
32
  });
32
33
 
34
+ it('uses its own route to get user profile', async () => {
35
+ const openEdxApiProfile = OpenEdxApiProfileFactory().one();
36
+ const { 'pref-lang': language, ...account } = openEdxApiProfile;
37
+ fetchMock.get(
38
+ `https://demo.endpoint.api/api/user/v1/accounts/${openEdxApiProfile.username}`,
39
+ account,
40
+ );
41
+ fetchMock.get(
42
+ `https://demo.endpoint.api/api/user/v1/preferences/${openEdxApiProfile.username}`,
43
+ {
44
+ 'pref-lang': language,
45
+ },
46
+ );
47
+
48
+ const api = FonzieAPIInterface(configuration);
49
+ expect(await api.user.account!.get(openEdxApiProfile.username)).toEqual(openEdxApiProfile);
50
+ expect(fetchMock.calls()).toHaveLength(2);
51
+ });
52
+
33
53
  it('is able to retrieve access token within the session storage', () => {
34
54
  const accessToken = faker.string.uuid();
35
55
  sessionStorage.setItem(RICHIE_USER_TOKEN, accessToken);
@@ -1,6 +1,10 @@
1
1
  import { AuthenticationBackend, LMSBackend } from 'types/commonDataProps';
2
2
  import { APILms } from 'types/api';
3
3
  import { RICHIE_USER_TOKEN } from 'settings';
4
+ import { isHttpError } from 'utils/errors/HttpError';
5
+ import { handle } from 'utils/errors/handle';
6
+ import { OpenEdxApiProfile } from 'types/openEdx';
7
+ import { checkStatus } from 'api/utils';
4
8
  import OpenEdxHawthornApiInterface from './openedx-hawthorn';
5
9
 
6
10
  /**
@@ -24,6 +28,8 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
24
28
  routes: {
25
29
  user: {
26
30
  me: `${APIConf.endpoint}/api/v1.0/user/me`,
31
+ account: `${APIConf.endpoint}/api/user/v1/accounts/:username`,
32
+ preferences: `${APIConf.endpoint}/api/user/v1/preferences/:username`,
27
33
  },
28
34
  },
29
35
  };
@@ -36,6 +42,35 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
36
42
  accessToken: () => {
37
43
  return sessionStorage.getItem(RICHIE_USER_TOKEN);
38
44
  },
45
+ account: {
46
+ get: async (username: string) => {
47
+ const options: RequestInit = {
48
+ credentials: 'include',
49
+ };
50
+
51
+ try {
52
+ const account = await fetch(
53
+ APIOptions.routes.user.account.replace(':username', username),
54
+ options,
55
+ ).then(checkStatus);
56
+ const preferences = await fetch(
57
+ APIOptions.routes.user.preferences.replace(':username', username),
58
+ options,
59
+ ).then(checkStatus);
60
+
61
+ return {
62
+ ...account,
63
+ ...preferences,
64
+ } as OpenEdxApiProfile;
65
+ } catch (e) {
66
+ if (isHttpError(e)) {
67
+ handle(new Error(`[GET - Account] > ${e.code} - ${e.message}`));
68
+ }
69
+
70
+ throw e;
71
+ }
72
+ },
73
+ },
39
74
  },
40
75
  };
41
76
  };
@@ -0,0 +1,51 @@
1
+ import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
2
+
3
+ interface CheckStatusOptions {
4
+ fallbackValue: any;
5
+ ignoredErrorStatus: (number | HttpStatusCode)[];
6
+ }
7
+
8
+ export async function getFileFromResponse(response: Response): Promise<File> {
9
+ const filenameRegex = /filename="(.*)";/;
10
+ const dispositionHeader = response.headers.get('Content-Disposition');
11
+ const matches = dispositionHeader?.match(filenameRegex);
12
+
13
+ return new File([await response.blob()], matches ? matches[1] : '', {
14
+ type: response.headers.get('Content-Type') || '',
15
+ });
16
+ }
17
+
18
+ export function getResponseBody(response: Response) {
19
+ if (response.headers.get('Content-Type') === 'application/json') {
20
+ return response.json();
21
+ }
22
+ const fileType = ['application/pdf', 'application/zip'];
23
+ if (fileType.includes(response.headers.get('Content-Type') || '')) {
24
+ return new Promise((resolve) => resolve(response));
25
+ }
26
+ return response.text();
27
+ }
28
+
29
+ /*
30
+ A util to manage API responses.
31
+ It parses properly the response according to its `Content-Type`
32
+ otherwise it throws an `HttpError`.
33
+
34
+ `options` arguments accept an array of ignoredErrorStatus. If the response
35
+ fails with one of this status code, the `fallbackValue` will return and no error will
36
+ be raised.
37
+ */
38
+ export function checkStatus(
39
+ response: Response,
40
+ options: CheckStatusOptions = { fallbackValue: null, ignoredErrorStatus: [] },
41
+ ): Promise<any> {
42
+ if (response.ok) {
43
+ return getResponseBody(response);
44
+ }
45
+
46
+ if (options.ignoredErrorStatus.includes(response.status)) {
47
+ return Promise.resolve(options.fallbackValue);
48
+ }
49
+
50
+ throw new HttpError(response.status, response.statusText);
51
+ }
@@ -10,6 +10,7 @@ import Input from 'components/Form/Input';
10
10
  import { useAddresses } from 'hooks/useAddresses';
11
11
  import type { Address } from 'types/Joanie';
12
12
  import { messages as formMessages } from 'components/Form/messages';
13
+ import Form from 'components/Form';
13
14
  import validationSchema from './validationSchema';
14
15
 
15
16
  export interface AddressFormValues extends Omit<Address, 'id' | 'is_main'> {
@@ -60,11 +61,11 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
60
61
 
61
62
  return (
62
63
  <FormProvider {...form}>
63
- <form className="form" name="address-form" onSubmit={handleSubmit(onSubmit)} noValidate>
64
+ <Form name="address-form" onSubmit={handleSubmit(onSubmit)} noValidate>
64
65
  <p className="form__required-fields-note">
65
66
  <FormattedMessage {...formMessages.formOptionalFieldsText} />
66
67
  </p>
67
- <div className="form-row">
68
+ <Form.Row>
68
69
  <Input
69
70
  className="form-field"
70
71
  required
@@ -72,8 +73,8 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
72
73
  name="title"
73
74
  label={intl.formatMessage(messages.titleInputLabel)}
74
75
  />
75
- </div>
76
- <div className="form-row">
76
+ </Form.Row>
77
+ <Form.Row>
77
78
  <Input
78
79
  className="form-field"
79
80
  required
@@ -86,16 +87,16 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
86
87
  name="last_name"
87
88
  label={intl.formatMessage(messages.last_nameInputLabel)}
88
89
  />
89
- </div>
90
- <div className="form-row">
90
+ </Form.Row>
91
+ <Form.Row>
91
92
  <Input
92
93
  required
93
94
  fullWidth
94
95
  name="address"
95
96
  label={intl.formatMessage(messages.addressInputLabel)}
96
97
  />
97
- </div>
98
- <div className="form-row">
98
+ </Form.Row>
99
+ <Form.Row>
99
100
  <Input
100
101
  className="form-field"
101
102
  required
@@ -109,17 +110,17 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
109
110
  name="city"
110
111
  label={intl.formatMessage(messages.cityInputLabel)}
111
112
  />
112
- </div>
113
- <div className="form-row">
113
+ </Form.Row>
114
+ <Form.Row>
114
115
  <CountrySelectField
115
116
  name="country"
116
117
  label={intl.formatMessage(messages.countryInputLabel)}
117
118
  state={formState.errors.country ? 'error' : 'default'}
118
119
  />
119
- </div>
120
+ </Form.Row>
120
121
 
121
122
  {!address ? (
122
- <div className="form-row">
123
+ <Form.Row>
123
124
  <Checkbox
124
125
  aria-invalid={!!formState.errors?.save}
125
126
  id="save"
@@ -132,7 +133,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
132
133
  )}
133
134
  {...register('save')}
134
135
  />
135
- </div>
136
+ </Form.Row>
136
137
  ) : null}
137
138
 
138
139
  <footer className="form__footer">
@@ -155,7 +156,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
155
156
  </Button>
156
157
  )}
157
158
  </footer>
158
- </form>
159
+ </Form>
159
160
  </FormProvider>
160
161
  );
161
162
  };
@@ -0,0 +1,23 @@
1
+ import c from 'classnames';
2
+ import { PropsWithChildren } from 'react';
3
+
4
+ interface FormProps
5
+ extends React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> {}
6
+
7
+ const Form = ({ children, onSubmit, className, name, noValidate = true }: FormProps) => {
8
+ return (
9
+ <form className={c('form', className)} name={name} onSubmit={onSubmit} noValidate={noValidate}>
10
+ {children}
11
+ </form>
12
+ );
13
+ };
14
+
15
+ Form.Row = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
16
+ return <div className={c('form-row', className)}>{children}</div>;
17
+ };
18
+
19
+ Form.Footer = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
20
+ return <div className={c('form-footer', className)}>{children}</div>;
21
+ };
22
+
23
+ export default Form;
@@ -1,5 +1,8 @@
1
+ import Form from './Form';
2
+
1
3
  export { default as Select } from './Select';
2
4
  export { CountrySelectField } from './CountrySelectField';
3
5
  export { messages } from './messages';
4
6
  export * from './utils';
5
7
  export * from './ValidationErrors';
8
+ export default Form;
@@ -11,6 +11,7 @@ import { messages as formMessages } from 'components/Form/messages';
11
11
  import { CountrySelectField } from 'components/Form/CountrySelectField';
12
12
  import Input from 'components/Form/Input';
13
13
  import { Address } from 'types/Joanie';
14
+ import Form from 'components/Form';
14
15
 
15
16
  const messages = defineMessages({
16
17
  isMainInputLabel: {
@@ -64,19 +65,19 @@ export const useDashboardAddressForm = (address?: Address) => {
64
65
 
65
66
  const FormView = (
66
67
  <FormProvider {...form}>
67
- <form className="form" noValidate>
68
+ <Form noValidate>
68
69
  <p className="form__required-fields-note">
69
70
  <FormattedMessage {...formMessages.formOptionalFieldsText} />
70
71
  </p>
71
- <div className="form-row">
72
+ <Form.Row>
72
73
  <Input
73
74
  required
74
75
  fullWidth
75
76
  name="title"
76
77
  label={intl.formatMessage(managementMessages.titleInputLabel)}
77
78
  />
78
- </div>
79
- <div className="form-row">
79
+ </Form.Row>
80
+ <Form.Row>
80
81
  <Input
81
82
  className="form-field"
82
83
  required
@@ -90,17 +91,17 @@ export const useDashboardAddressForm = (address?: Address) => {
90
91
  name="last_name"
91
92
  label={intl.formatMessage(managementMessages.last_nameInputLabel)}
92
93
  />
93
- </div>
94
- <div className="form-row">
94
+ </Form.Row>
95
+ <Form.Row>
95
96
  <Input
96
97
  required
97
98
  fullWidth
98
99
  name="address"
99
100
  label={intl.formatMessage(managementMessages.addressInputLabel)}
100
101
  />
101
- </div>
102
+ </Form.Row>
102
103
 
103
- <div className="form-row">
104
+ <Form.Row>
104
105
  <Input
105
106
  className="form-field"
106
107
  required
@@ -114,13 +115,13 @@ export const useDashboardAddressForm = (address?: Address) => {
114
115
  name="city"
115
116
  label={intl.formatMessage(managementMessages.cityInputLabel)}
116
117
  />
117
- </div>
118
- <div className="form-row">
118
+ </Form.Row>
119
+ <Form.Row>
119
120
  <CountrySelectField
120
121
  name="country"
121
122
  label={intl.formatMessage(managementMessages.countryInputLabel)}
122
123
  />
123
- </div>
124
+ </Form.Row>
124
125
  {!(address && address.is_main) && (
125
126
  <Checkbox
126
127
  aria-invalid={!!formState.errors?.is_main}
@@ -135,7 +136,7 @@ export const useDashboardAddressForm = (address?: Address) => {
135
136
  {...register('is_main')}
136
137
  />
137
138
  )}
138
- </form>
139
+ </Form>
139
140
  </FormProvider>
140
141
  );
141
142
 
@@ -0,0 +1,45 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { AuthenticationApi } from 'api/authentication';
4
+ import { useSessionQuery } from 'utils/react-query/useSessionQuery';
5
+ import { OpenEdxProfile, parseOpenEdxApiProfile } from './utils';
6
+
7
+ const messages = defineMessages({
8
+ errorGet: {
9
+ id: 'hooks.useOpenEdxProfile.errorGet',
10
+ description: 'Error message shown to the user when openEdx profile fetch request fails.',
11
+ defaultMessage: 'An error occurred while fetching your profile. Please retry later.',
12
+ },
13
+ });
14
+
15
+ interface UseOpenEdxProfileProps {
16
+ username: string;
17
+ }
18
+
19
+ const useOpenEdxProfile = ({ username }: UseOpenEdxProfileProps) => {
20
+ if (!AuthenticationApi) {
21
+ throw new Error('AuthenticationApi is not defined');
22
+ }
23
+
24
+ if (!AuthenticationApi!.account) {
25
+ throw new Error('Current AuthenticationApi does not support account request');
26
+ }
27
+
28
+ const intl = useIntl();
29
+ const [error, setError] = useState<string>();
30
+
31
+ const queryFn: () => Promise<OpenEdxProfile> = useCallback(async () => {
32
+ try {
33
+ const openEdxApiProfile = await AuthenticationApi!.account!.get(username);
34
+ return parseOpenEdxApiProfile(intl, openEdxApiProfile);
35
+ } catch {
36
+ setError(intl.formatMessage(messages.errorGet));
37
+ }
38
+ return Promise.reject();
39
+ }, [username, AuthenticationApi]);
40
+
41
+ const [queryHandler] = useSessionQuery<OpenEdxProfile>(['open-edx-profile'], queryFn);
42
+ return { data: queryHandler.data, error };
43
+ };
44
+
45
+ export default useOpenEdxProfile;
@@ -0,0 +1,69 @@
1
+ import { createIntl } from 'react-intl';
2
+ import { faker } from '@faker-js/faker';
3
+ import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
4
+ import { OpenEdxGender, OpenEdxLanguageIsoCode, OpenEdxLevelOfEducation } from 'types/openEdx';
5
+ import { parseOpenEdxApiProfile } from '.';
6
+
7
+ describe('useOpenEdxProfile > utils', () => {
8
+ it('parseOpenEdxApiProfile should format values', () => {
9
+ const profile = parseOpenEdxApiProfile(
10
+ createIntl({ locale: 'en' }),
11
+ OpenEdxApiProfileFactory({
12
+ username: 'John',
13
+ name: 'Do',
14
+ email: 'john.do@whereis.net',
15
+ 'pref-lang': OpenEdxLanguageIsoCode.FRENCH,
16
+ country: 'fr',
17
+ level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
18
+ gender: OpenEdxGender.MALE,
19
+ year_of_birth: '01/01/1970',
20
+ language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
21
+ date_joined: '01/01/1970',
22
+ }).one(),
23
+ );
24
+
25
+ expect(profile).toStrictEqual({
26
+ username: 'John',
27
+ name: 'Do',
28
+ email: 'john.do@whereis.net',
29
+ language: 'French',
30
+ country: 'France',
31
+ levelOfEducation: 'Master',
32
+ gender: 'Male',
33
+ yearOfBirth: '01/01/1970',
34
+ favoriteLanguage: 'English',
35
+ dateJoined: '01/01/1970',
36
+ });
37
+ });
38
+
39
+ it('parseOpenEdxApiProfile should format default values', () => {
40
+ const profile = parseOpenEdxApiProfile(
41
+ createIntl({ locale: 'en' }),
42
+ OpenEdxApiProfileFactory({
43
+ username: faker.internet.userName(),
44
+ name: '',
45
+ email: faker.internet.email(),
46
+ 'pref-lang': undefined,
47
+ country: null,
48
+ level_of_education: null,
49
+ gender: null,
50
+ year_of_birth: null,
51
+ language_proficiencies: [],
52
+ date_joined: Date.toString(),
53
+ }).one(),
54
+ );
55
+
56
+ expect(profile).toStrictEqual({
57
+ username: profile.username,
58
+ name: ' - ',
59
+ email: profile.email,
60
+ language: ' - ',
61
+ country: ' - ',
62
+ levelOfEducation: ' - ',
63
+ gender: ' - ',
64
+ yearOfBirth: ' - ',
65
+ favoriteLanguage: ' - ',
66
+ dateJoined: profile.dateJoined,
67
+ });
68
+ });
69
+ });