richie-education 2.34.0 → 2.34.1-dev4

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 (22) hide show
  1. package/js/components/AddressesManagement/index.spec.tsx +1 -1
  2. package/js/components/AddressesManagement/index.tsx +123 -129
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +2 -4
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +1 -1
  5. package/js/components/Icon/index.stories.tsx +1 -1
  6. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  7. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +2 -2
  8. package/js/hooks/useCreditCards/index.spec.tsx +3 -2
  9. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +1 -1
  10. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -2
  11. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +1 -1
  12. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +8 -5
  13. package/js/utils/test/wrappers/types.ts +2 -2
  14. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +1 -1
  15. package/js/widgets/Dashboard/components/SearchBar/index.spec.tsx +1 -1
  16. package/js/widgets/Dashboard/components/SearchResultsCount/index.spec.tsx +1 -1
  17. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -1
  18. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +6 -4
  19. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +1 -5
  20. package/package.json +64 -62
  21. package/scss/components/_styleguide.scss +2 -1
  22. package/scss/vendors/css/cunningham-tokens.css +1 -0
@@ -6,7 +6,7 @@ import fetchMock from 'fetch-mock';
6
6
  import { IntlProvider } from 'react-intl';
7
7
  import countries from 'i18n-iso-countries';
8
8
  import { QueryClientProvider } from '@tanstack/react-query';
9
- import { PropsWithChildren } from 'react';
9
+ import React, { PropsWithChildren } from 'react';
10
10
  import { CunninghamProvider } from '@openfun/cunningham-react';
11
11
  import userEvent, { UserEvent } from '@testing-library/user-event';
12
12
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
@@ -1,4 +1,4 @@
1
- import { Children, forwardRef, useEffect, useState } from 'react';
1
+ import { Children, useEffect, useState, RefAttributes } from 'react';
2
2
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
3
  import { Button } from '@openfun/cunningham-react';
4
4
  import AddressForm, { type AddressFormValues } from 'components/AddressesManagement/AddressForm';
@@ -7,7 +7,6 @@ import { Icon, IconTypeEnum } from 'components/Icon';
7
7
  import RegisteredAddress from 'components/RegisteredAddress';
8
8
  import { useAddressesManagement } from 'hooks/useAddressesManagement';
9
9
  import type * as Joanie from 'types/Joanie';
10
- import { Address } from 'types/Joanie';
11
10
  import { Maybe } from 'types/utils';
12
11
 
13
12
  // constant used as `address.id` for local address
@@ -106,147 +105,142 @@ export const messages = defineMessages({
106
105
  },
107
106
  });
108
107
 
109
- interface AddressesManagementProps {
108
+ interface AddressesManagementProps extends RefAttributes<HTMLDivElement> {
110
109
  handleClose: () => void;
111
110
  selectAddress: (address: Joanie.Address) => void;
112
111
  }
113
112
 
114
- const AddressesManagement = forwardRef<HTMLDivElement, AddressesManagementProps>(
115
- ({ handleClose, selectAddress }, ref) => {
116
- const intl = useIntl();
117
- const [editedAddress, setEditedAddress] = useState<Maybe<Joanie.Address>>();
118
- const {
119
- methods: { setError, create, update, remove, promote },
120
- states: { error },
121
- ...addresses
122
- } = useAddressesManagement();
113
+ const AddressesManagement = ({ handleClose, selectAddress, ref }: AddressesManagementProps) => {
114
+ const intl = useIntl();
115
+ const [editedAddress, setEditedAddress] = useState<Maybe<Joanie.Address>>();
116
+ const {
117
+ methods: { setError, create, update, remove, promote },
118
+ states: { error },
119
+ ...addresses
120
+ } = useAddressesManagement();
123
121
 
124
- /**
125
- * Sort addresses ascending by title according to the locale
126
- *
127
- * @param {Joanie.Address} a
128
- * @param {Joanie.Address} b
129
- * @returns {Joanie.Address[]} Sorted addresses ascending by title
130
- */
131
- const sortAddressByTitleAsc = (a: Joanie.Address, b: Joanie.Address) => {
132
- return a.title.localeCompare(b.title, [intl.locale, intl.defaultLocale]);
133
- };
122
+ /**
123
+ * Sort addresses ascending by title according to the locale
124
+ *
125
+ * @param {Joanie.Address} a
126
+ * @param {Joanie.Address} b
127
+ * @returns {Joanie.Address[]} Sorted addresses ascending by title
128
+ */
129
+ const sortAddressByTitleAsc = (a: Joanie.Address, b: Joanie.Address) => {
130
+ return a.title.localeCompare(b.title, [intl.locale, intl.defaultLocale]);
131
+ };
134
132
 
135
- /**
136
- * update `selectedAddress` state with the address provided
137
- * then close the address management form
138
- *
139
- * @param {Joanie.Address} address
140
- */
141
- const handleSelect = (address: Joanie.Address) => {
142
- setError(undefined);
143
- selectAddress(address);
144
- handleClose();
145
- };
133
+ /**
134
+ * update `selectedAddress` state with the address provided
135
+ * then close the address management form
136
+ *
137
+ * @param {Joanie.Address} address
138
+ */
139
+ const handleSelect = (address: Joanie.Address) => {
140
+ setError(undefined);
141
+ selectAddress(address);
142
+ handleClose();
143
+ };
146
144
 
147
- /**
148
- * Create a new address according to form values
149
- * then update `selectedAddress` state with this new one.
150
- * If `save` checkbox input is checked, the address is persisted
151
- * otherwise it is only stored through the `selectedAddress` state.
152
- *
153
- * @param {AddressFormValues} formValues address fields to update
154
- */
155
- const handleCreate = async ({ save, ...address }: AddressFormValues) => {
156
- if (save) {
157
- await create(address, { onSuccess: handleSelect });
158
- } else {
159
- handleSelect({
160
- id: LOCAL_BILLING_ADDRESS_ID,
161
- is_main: false,
162
- ...address,
163
- });
164
- }
165
- };
145
+ /**
146
+ * Create a new address according to form values
147
+ * then update `selectedAddress` state with this new one.
148
+ * If `save` checkbox input is checked, the address is persisted
149
+ * otherwise it is only stored through the `selectedAddress` state.
150
+ *
151
+ * @param {AddressFormValues} formValues address fields to update
152
+ */
153
+ const handleCreate = async ({ save, ...address }: AddressFormValues) => {
154
+ if (save) {
155
+ await create(address, { onSuccess: handleSelect });
156
+ } else {
157
+ handleSelect({
158
+ id: LOCAL_BILLING_ADDRESS_ID,
159
+ is_main: false,
160
+ ...address,
161
+ });
162
+ }
163
+ };
166
164
 
167
- /**
168
- * Update the `editedAddress` with new values provided as argument
169
- * then clear `editedAddress` state if request succeeded.
170
- *
171
- * @param {AddressFormValues} formValues address fields to update
172
- */
173
- const handleUpdate = async ({ save, ...newAddress }: AddressFormValues) => {
174
- update(
175
- {
176
- ...editedAddress!,
177
- ...newAddress,
178
- },
179
- {
180
- onSuccess: () => setEditedAddress(undefined),
181
- },
182
- );
183
- };
165
+ /**
166
+ * Update the `editedAddress` with new values provided as argument
167
+ * then clear `editedAddress` state if request succeeded.
168
+ *
169
+ * @param {AddressFormValues} formValues address fields to update
170
+ */
171
+ const handleUpdate = async ({ save, ...newAddress }: AddressFormValues) => {
172
+ update(
173
+ {
174
+ ...editedAddress!,
175
+ ...newAddress,
176
+ },
177
+ {
178
+ onSuccess: () => setEditedAddress(undefined),
179
+ },
180
+ );
181
+ };
184
182
 
185
- useEffect(() => {
186
- setError(undefined);
187
- if (editedAddress) {
188
- document.querySelector<HTMLElement>('[name="address-form"] input')?.focus();
189
- }
190
- }, [editedAddress]);
183
+ useEffect(() => {
184
+ setError(undefined);
185
+ if (editedAddress) {
186
+ document.querySelector<HTMLElement>('[name="address-form"] input')?.focus();
187
+ }
188
+ }, [editedAddress]);
191
189
 
192
- return (
193
- <div className="AddressesManagement" ref={ref}>
194
- <Button
195
- className="AddressesManagement__closeButton"
196
- color="tertiary"
197
- size="small"
198
- onClick={handleClose}
199
- >
200
- <Icon name={IconTypeEnum.CHEVRON_LEFT_OUTLINE} className="button__icon" />
201
- <FormattedMessage {...messages.closeButton} />
202
- </Button>
203
- {error && <Banner message={error} type={BannerType.ERROR} rounded />}
204
- {addresses.items.length > 0 ? (
205
- <section className="address-registered">
206
- <header>
207
- <h2 className="h5">
208
- <FormattedMessage {...messages.registeredAddresses} />
209
- </h2>
210
- </header>
211
- <ul className="registered-addresses-list">
212
- {Children.toArray(
213
- addresses.items
214
- .sort(sortAddressByTitleAsc)
215
- .map((address) => (
216
- <RegisteredAddress
217
- address={address}
218
- edit={setEditedAddress}
219
- promote={promote}
220
- remove={remove}
221
- select={handleSelect}
222
- />
223
- )),
224
- )}
225
- </ul>
226
- </section>
227
- ) : null}
228
- <section className={`address-form ${editedAddress ? 'address-form--highlighted' : ''}`}>
190
+ return (
191
+ <div className="AddressesManagement" ref={ref}>
192
+ <Button
193
+ className="AddressesManagement__closeButton"
194
+ color="tertiary"
195
+ size="small"
196
+ onClick={handleClose}
197
+ >
198
+ <Icon name={IconTypeEnum.CHEVRON_LEFT_OUTLINE} className="button__icon" />
199
+ <FormattedMessage {...messages.closeButton} />
200
+ </Button>
201
+ {error && <Banner message={error} type={BannerType.ERROR} rounded />}
202
+ {addresses.items.length > 0 ? (
203
+ <section className="address-registered">
229
204
  <header>
230
205
  <h2 className="h5">
231
- {editedAddress ? (
232
- <FormattedMessage
233
- {...messages.editAddress}
234
- values={{ title: editedAddress.title }}
235
- />
236
- ) : (
237
- <FormattedMessage {...messages.addAddress} />
238
- )}
206
+ <FormattedMessage {...messages.registeredAddresses} />
239
207
  </h2>
240
208
  </header>
241
- <AddressForm
242
- address={editedAddress}
243
- handleReset={() => setEditedAddress(undefined)}
244
- onSubmit={editedAddress ? handleUpdate : handleCreate}
245
- />
209
+ <ul className="registered-addresses-list">
210
+ {Children.toArray(
211
+ addresses.items
212
+ .sort(sortAddressByTitleAsc)
213
+ .map((address) => (
214
+ <RegisteredAddress
215
+ address={address}
216
+ edit={setEditedAddress}
217
+ promote={promote}
218
+ remove={remove}
219
+ select={handleSelect}
220
+ />
221
+ )),
222
+ )}
223
+ </ul>
246
224
  </section>
247
- </div>
248
- );
249
- },
250
- );
225
+ ) : null}
226
+ <section className={`address-form ${editedAddress ? 'address-form--highlighted' : ''}`}>
227
+ <header>
228
+ <h2 className="h5">
229
+ {editedAddress ? (
230
+ <FormattedMessage {...messages.editAddress} values={{ title: editedAddress.title }} />
231
+ ) : (
232
+ <FormattedMessage {...messages.addAddress} />
233
+ )}
234
+ </h2>
235
+ </header>
236
+ <AddressForm
237
+ address={editedAddress}
238
+ handleReset={() => setEditedAddress(undefined)}
239
+ onSubmit={editedAddress ? handleUpdate : handleCreate}
240
+ />
241
+ </section>
242
+ </div>
243
+ );
244
+ };
251
245
 
252
246
  export default AddressesManagement;
@@ -204,12 +204,10 @@ describe('<AbstractContractFrame />', () => {
204
204
  await user.click(button);
205
205
 
206
206
  // The dummy interface should be loading
207
- screen.getByRole('heading', { name: 'Signing the contract ...' });
207
+ await screen.findByRole('heading', { name: 'Signing the contract ...' });
208
208
 
209
209
  // Then the signature check polling should be started
210
- await waitFor(() => {
211
- expect(screen.getByRole('heading', { name: 'Verifying signature ...' })).toBeInTheDocument();
212
- });
210
+ await screen.findByRole('heading', { name: 'Verifying signature ...' });
213
211
  expect(
214
212
  screen.getByText(
215
213
  'We are waiting for the signature to be validated from our signature platform. It can take up to few minutes. Do not close this page.',
@@ -134,7 +134,7 @@ const ContractFrameContent = ({
134
134
  const [signatureType, setSignatureType] = useState<SignatureType>();
135
135
  const [invitationLink, setInvitationLink] = useState<Maybe<string>>();
136
136
  const [error, setError] = useState<Maybe<string>>();
137
- const timeoutRef = useRef<NodeJS.Timeout>();
137
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
138
138
 
139
139
  const setErrored = (e: string) => {
140
140
  setStep(ContractSteps.ERROR);
@@ -29,7 +29,7 @@ type IconContainerProps = {
29
29
  };
30
30
  const IconContainer = ({ name, enumKey }: IconContainerProps) => {
31
31
  const [showTooltip, setShowTooltip] = useState(false);
32
- const timeoutRef = useRef<NodeJS.Timeout>();
32
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
33
33
  const ENUM_NAME = 'IconTypeEnum';
34
34
 
35
35
  const styleContainer: CSSProperties = {
@@ -17,7 +17,7 @@ const PayplugLightbox = ({
17
17
  onError,
18
18
  ...props
19
19
  }: PaymentInterfaceProps<PayplugPayment>) => {
20
- const ref = useRef<ReturnType<typeof setTimeout>>();
20
+ const ref = useRef<ReturnType<typeof setTimeout>>(undefined);
21
21
 
22
22
  /** type guard to check if the payment is a payment one click */
23
23
  const isPaidPayment = (p: PayplugPayment) => p?.is_paid === true;
@@ -47,7 +47,7 @@ const messages = defineMessages({
47
47
  });
48
48
 
49
49
  const SaleTunnelSavePaymentMethod = () => {
50
- const initialCreditCards = useRef<CreditCard[]>();
50
+ const initialCreditCards = useRef<CreditCard[]>([]);
51
51
  const [shouldPoll, setShouldPoll] = useState(false);
52
52
  const [payment, setPayment] = useState<Payment>();
53
53
  const [error, setError] = useState<string>();
@@ -71,7 +71,7 @@ const SaleTunnelSavePaymentMethod = () => {
71
71
  };
72
72
 
73
73
  const waitForNewCreditCard = () => {
74
- const initialIds = initialCreditCards.current!.map((cc) => cc.id);
74
+ const initialIds = initialCreditCards.current.map((cc) => cc.id);
75
75
  const newCard = creditCardsQuery.items.find((cc) => !initialIds.includes(cc.id));
76
76
 
77
77
  if (!newCard) return;
@@ -208,8 +208,9 @@ describe('useCreditCards', () => {
208
208
  await act(async () => {
209
209
  responseDeferred.resolve({});
210
210
  });
211
-
212
- expect(result.current.states.updating).toBe(false);
211
+ await waitFor(() => {
212
+ expect(result.current.states.updating).toBe(false);
213
+ });
213
214
  expect(result.current.states.isPending).toBe(false);
214
215
  expect(result.current.states.error).toBe(undefined);
215
216
  });
@@ -181,7 +181,7 @@ describe('<DahsboardEditCreditCard/>', () => {
181
181
  await screen.findByText('Credit cards');
182
182
 
183
183
  // The title is correctly updated.
184
- screen.getByRole('heading', {
184
+ await screen.findByRole('heading', {
185
185
  level: 6,
186
186
  name: creditCardUpdated.title,
187
187
  });
@@ -98,7 +98,7 @@ describe.each([
98
98
  expect(mockCheckArchive).not.toHaveBeenCalled();
99
99
  });
100
100
 
101
- it('should check if archive exist when a id is stored', async () => {
101
+ it('should check if archive exist when an id is stored', async () => {
102
102
  storeContractArchiveId({
103
103
  ...localStorageArchiveFilters,
104
104
  contractArchiveId: faker.string.uuid(),
@@ -115,7 +115,9 @@ describe.each([
115
115
  expect(mockCheckArchive).toHaveBeenCalledTimes(1);
116
116
  });
117
117
 
118
- expect(result.current.isPolling).toBe(false);
118
+ await waitFor(() => {
119
+ expect(result.current.isPolling).toBe(false);
120
+ });
119
121
  expect(result.current.isContractArchiveExists).toBe(true);
120
122
  });
121
123
 
@@ -25,7 +25,7 @@ const useCheckContractArchiveExist = (
25
25
  // stay null until fetched
26
26
  const [isContractArchiveExists, setIsContractArchiveExists] = useState<Nullable<boolean>>(null);
27
27
 
28
- const timeoutRef = useRef<NodeJS.Timeout>();
28
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
29
29
 
30
30
  // This method will check if the archive exists on the server
31
31
  // option.polling === true will recursivly poll archive existence
@@ -41,6 +41,9 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
41
41
  beforeEach(() => {
42
42
  nbApiCalls = joanieSessionData.nbSessionApiRequest;
43
43
  });
44
+ afterEach(() => {
45
+ fetchMock.restore();
46
+ });
44
47
 
45
48
  it('should render', async () => {
46
49
  const organization = OrganizationFactory().one();
@@ -59,6 +62,10 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
59
62
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
60
63
  mockPaginatedResponse(CourseProductRelationFactory().many(15), 15, false),
61
64
  );
65
+ fetchMock.get(
66
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&page=1`,
67
+ [],
68
+ );
62
69
 
63
70
  render(<TeacherDashboardOrganizationCourseLoader />, {
64
71
  routerOptions: {
@@ -70,6 +77,7 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
70
77
 
71
78
  nbApiCalls += 1; // course api call
72
79
  nbApiCalls += 1; // course-product-relations api call
80
+ nbApiCalls += 1; // contracts api call
73
81
  const calledUrls = fetchMock.calls().map((call) => call[0]);
74
82
  expect(calledUrls).toHaveLength(nbApiCalls);
75
83
  expect(calledUrls).toContain(
@@ -78,11 +86,6 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
78
86
  expect(calledUrls).toContain(
79
87
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
80
88
  );
81
-
82
- fetchMock.get(
83
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&page=1`,
84
- [],
85
- );
86
89
  await expectNoSpinner('Loading organization...');
87
90
 
88
91
  expect(
@@ -1,4 +1,4 @@
1
- import { PropsWithChildren } from 'react';
1
+ import { PropsWithChildren, ReactElement } from 'react';
2
2
  import { QueryClient } from '@tanstack/query-core';
3
3
  import { RenderOptions as TestingLibraryRenderOptions } from '@testing-library/react';
4
4
  import { Nullable } from 'types/utils';
@@ -19,7 +19,7 @@ interface QueryOptions {
19
19
  * @property queryOptions options to configure a custom client used by react-query for a test
20
20
  */
21
21
  export interface AppWrapperProps {
22
- wrapper?: Nullable<(props: PropsWithChildren<{ options?: AppWrapperProps }>) => JSX.Element>;
22
+ wrapper?: Nullable<(props: PropsWithChildren<{ options?: AppWrapperProps }>) => ReactElement>;
23
23
  intlOptions?: IntlWrapperProps;
24
24
  queryOptions?: QueryOptions;
25
25
  historyOptions?: History;
@@ -83,7 +83,7 @@ enum ComponentStates {
83
83
  export const OrderPaymentRetryModal = ({ installment, order, ...props }: Props) => {
84
84
  const intl = useIntl();
85
85
  const API = useJoanieApi();
86
- const timeoutRef = useRef<NodeJS.Timeout>();
86
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
87
87
  const { methods: orderMethods } = useOrders(undefined, { enabled: false });
88
88
  const [payment, setPayment] = useState<Payment>();
89
89
  const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
@@ -4,7 +4,7 @@ import { render } from 'utils/test/render';
4
4
  import { PresentationalAppWrapper } from 'utils/test/wrappers/PresentationalAppWrapper';
5
5
  import SearchBar from '.';
6
6
 
7
- describe('Dashbaord/components/SearchBar', () => {
7
+ describe('Dashboard/components/SearchBar', () => {
8
8
  it('should render', () => {
9
9
  render(<SearchBar onSubmit={jest.fn()} />, { wrapper: PresentationalAppWrapper });
10
10
  expect(screen.getByRole('textbox', { name: /Search/ })).toBeInTheDocument();
@@ -3,7 +3,7 @@ import { render } from 'utils/test/render';
3
3
  import { PresentationalAppWrapper } from 'utils/test/wrappers/PresentationalAppWrapper';
4
4
  import SearchResultsCount from '.';
5
5
 
6
- describe('Dashbaord/components/SearchResultsCount', () => {
6
+ describe('Dashboard/components/SearchResultsCount', () => {
7
7
  it('should render singular message', () => {
8
8
  render(<SearchResultsCount nbResults={1} />, {
9
9
  wrapper: PresentationalAppWrapper,
@@ -139,11 +139,12 @@ const EnrollableCourseRunList = ({ courseRuns, order }: Props) => {
139
139
  <ol className="course-runs-list">
140
140
  {Children.toArray(
141
141
  courseRuns.map((courseRun) => (
142
- <li className="course-runs-item form-field">
142
+ <li key={`${order.id}|${courseRun.id}`} className="course-runs-item form-field">
143
143
  <input
144
144
  className="form-field__radio-input"
145
145
  type="radio"
146
146
  id={`${order.id}|${courseRun.id}`}
147
+ data-testid={`radio-input-${order.id}-${courseRun.id}`}
147
148
  name={order.id}
148
149
  disabled={needsSignature}
149
150
  aria-label={intl.formatMessage(messages.ariaSelectCourseRun, {
@@ -166,11 +166,13 @@ describe('CourseProductCourseRuns', () => {
166
166
  );
167
167
 
168
168
  // - A radio input
169
- screen.getByRole('radio', {
170
- name: `Select course run from ${dateFormatter.format(
169
+ const $input = screen.getByTestId(`radio-input-${order.id}-${courseRun.id}`);
170
+ expect($input).toHaveAttribute('type', 'radio');
171
+ expect($input).toHaveAccessibleName(
172
+ `Select course run from ${dateFormatter.format(
171
173
  new Date(courseRun.start),
172
174
  )} to ${dateFormatter.format(new Date(courseRun.end))}.`,
173
- });
175
+ );
174
176
  });
175
177
 
176
178
  // A call to action should be displayed
@@ -215,7 +217,7 @@ describe('CourseProductCourseRuns', () => {
215
217
  });
216
218
 
217
219
  // A spinner should be displayed
218
- screen.getByRole('status', { name: 'Enrolling...' });
220
+ await screen.findByRole('status', { name: 'Enrolling...' });
219
221
 
220
222
  await act(async () => {
221
223
  enrollmentDeferred.resolve(HttpStatusCode.OK);
@@ -195,11 +195,7 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
195
195
  step,
196
196
  },
197
197
  dispatch,
198
- ] = useReducer<React.Reducer<ReducerState, ReducerAction>, ReducerState>(
199
- reducer,
200
- initialState(user, props.courseRun, enrollmentIsActive),
201
- (s) => s,
202
- );
198
+ ] = useReducer(reducer, initialState(user, props.courseRun, enrollmentIsActive));
203
199
 
204
200
  const setEnroll = useCallback(
205
201
  async (isActive: boolean = true) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.34.0",
3
+ "version": "2.34.1-dev4",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -38,74 +38,74 @@
38
38
  "not dead"
39
39
  ],
40
40
  "dependencies": {
41
- "@babel/core": "7.26.0",
41
+ "@babel/core": "7.26.9",
42
42
  "@babel/plugin-syntax-dynamic-import": "7.8.3",
43
- "@babel/plugin-transform-modules-commonjs": "7.25.9",
44
- "@babel/preset-env": "7.26.0",
45
- "@babel/preset-react": "7.25.9",
43
+ "@babel/plugin-transform-modules-commonjs": "7.26.3",
44
+ "@babel/preset-env": "7.26.9",
45
+ "@babel/preset-react": "7.26.3",
46
46
  "@babel/preset-typescript": "7.26.0",
47
- "@faker-js/faker": "9.2.0",
48
- "@formatjs/cli": "6.3.11",
49
- "@formatjs/intl-relativetimeformat": "11.4.5",
50
- "@hookform/resolvers": "3.9.1",
47
+ "@faker-js/faker": "9.5.0",
48
+ "@formatjs/cli": "6.6.1",
49
+ "@formatjs/intl-relativetimeformat": "11.4.10",
50
+ "@hookform/resolvers": "4.1.0",
51
51
  "@lyracom/embedded-form-glue": "1.4.2",
52
- "@openfun/cunningham-react": "2.9.4",
53
- "@openfun/cunningham-tokens": "2.1.1",
54
- "@sentry/browser": "8.42.0",
55
- "@sentry/types": "8.42.0",
56
- "@storybook/addon-actions": "8.4.6",
57
- "@storybook/addon-essentials": "8.4.6",
58
- "@storybook/addon-interactions": "8.4.6",
59
- "@storybook/addon-links": "8.4.6",
60
- "@storybook/react": "8.4.6",
61
- "@storybook/react-webpack5": "8.4.6",
62
- "@storybook/test": "8.4.6",
63
- "@tanstack/query-core": "5.62.1",
64
- "@tanstack/query-sync-storage-persister": "5.62.1",
65
- "@tanstack/react-query": "5.62.1",
66
- "@tanstack/react-query-devtools": "5.62.1",
67
- "@tanstack/react-query-persist-client": "5.62.1",
52
+ "@openfun/cunningham-react": "3.0.0",
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",
68
68
  "@testing-library/dom": "10.4.0",
69
69
  "@testing-library/jest-dom": "6.6.3",
70
- "@testing-library/react": "16.0.1",
71
- "@testing-library/user-event": "14.5.2",
70
+ "@testing-library/react": "16.2.0",
71
+ "@testing-library/user-event": "14.6.1",
72
72
  "@types/fetch-mock": "7.3.8",
73
- "@types/iframe-resizer": "3.5.13",
73
+ "@types/iframe-resizer": "4.0.0",
74
74
  "@types/jest": "29.5.14",
75
75
  "@types/js-cookie": "3.0.6",
76
76
  "@types/lodash-es": "4.17.12",
77
77
  "@types/node-fetch": "2.6.12",
78
78
  "@types/query-string": "6.3.0",
79
- "@types/react": "18.3.12",
79
+ "@types/react": "19.0.10",
80
80
  "@types/react-autosuggest": "10.1.11",
81
- "@types/react-dom": "18.3.1",
81
+ "@types/react-dom": "19.0.4",
82
82
  "@types/react-modal": "3.16.3",
83
- "@typescript-eslint/eslint-plugin": "8.17.0",
84
- "@typescript-eslint/parser": "8.17.0",
83
+ "@typescript-eslint/eslint-plugin": "8.24.0",
84
+ "@typescript-eslint/parser": "8.24.0",
85
85
  "babel-jest": "29.7.0",
86
86
  "babel-loader": "9.2.1",
87
87
  "babel-plugin-react-intl": "8.2.25",
88
88
  "bootstrap": ">=4.6.0 <5",
89
89
  "classnames": "2.5.1",
90
90
  "cljs-merge": "1.1.1",
91
- "core-js": "3.39.0",
91
+ "core-js": "3.40.0",
92
92
  "downshift": "9.0.8",
93
93
  "eslint": ">=8.57.0 <9",
94
94
  "eslint-config-airbnb": "19.0.4",
95
95
  "eslint-config-airbnb-typescript": "18.0.0",
96
- "eslint-config-prettier": "9.1.0",
97
- "eslint-import-resolver-webpack": "0.13.9",
98
- "eslint-plugin-compat": "6.0.1",
99
- "eslint-plugin-formatjs": "5.2.5",
96
+ "eslint-config-prettier": "10.0.1",
97
+ "eslint-import-resolver-webpack": "0.13.10",
98
+ "eslint-plugin-compat": "6.0.2",
99
+ "eslint-plugin-formatjs": "5.2.14",
100
100
  "eslint-plugin-import": "2.31.0",
101
101
  "eslint-plugin-jsx-a11y": "6.10.2",
102
- "eslint-plugin-prettier": "5.2.1",
103
- "eslint-plugin-react": "7.37.2",
104
- "eslint-plugin-react-hooks": "5.0.0",
105
- "eslint-plugin-storybook": "0.11.1",
102
+ "eslint-plugin-prettier": "5.2.3",
103
+ "eslint-plugin-react": "7.37.4",
104
+ "eslint-plugin-react-hooks": "5.1.0",
105
+ "eslint-plugin-storybook": "0.11.3",
106
106
  "fetch-mock": "<10",
107
107
  "file-loader": "6.2.0",
108
- "glob": "11.0.0",
108
+ "glob": "11.0.1",
109
109
  "i18n-iso-countries": "7.13.0",
110
110
  "iframe-resizer": "<5",
111
111
  "intl-pluralrules": "2.0.1",
@@ -114,35 +114,37 @@
114
114
  "js-cookie": "3.0.5",
115
115
  "lodash-es": "4.17.21",
116
116
  "mdn-polyfills": "5.20.0",
117
- "msw": "2.6.6",
117
+ "msw": "2.7.0",
118
118
  "node-fetch": ">2.6.6 <3",
119
- "nodemon": "3.1.7",
120
- "prettier": "3.4.1",
119
+ "nodemon": "3.1.9",
120
+ "prettier": "3.5.1",
121
121
  "query-string": "9.1.1",
122
- "react": "18.3.1",
122
+ "react": "19.0.0",
123
123
  "react-autosuggest": "10.1.0",
124
- "react-dom": "18.3.1",
125
- "react-hook-form": "7.53.2",
126
- "react-intl": "7.0.1",
127
- "react-modal": "3.16.1",
128
- "react-router": "7.0.1",
129
- "sass": "1.81.0",
124
+ "react-dom": "19.0.0",
125
+ "react-hook-form": "7.54.2",
126
+ "react-intl": "7.1.6",
127
+ "react-modal": "3.16.3",
128
+ "react-router": "7.1.5",
129
+ "sass": "1.85.0",
130
130
  "source-map-loader": "5.0.0",
131
- "storybook": "8.4.6",
131
+ "storybook": "8.5.6",
132
132
  "tsconfig-paths-webpack-plugin": "4.2.0",
133
- "typescript": "5.7.2",
134
- "uuid": "11.0.3",
135
- "webpack": "5.96.1",
136
- "webpack-cli": "5.1.4",
133
+ "typescript": "5.7.3",
134
+ "uuid": "11.0.5",
135
+ "webpack": "5.98.0",
136
+ "webpack-cli": "6.0.1",
137
137
  "whatwg-fetch": "3.6.20",
138
138
  "xhr-mock": "2.5.1",
139
139
  "yargs": "17.7.2",
140
- "yup": "1.4.0"
140
+ "yup": "1.6.1"
141
141
  },
142
142
  "resolutions": {
143
143
  "@testing-library/dom": "10.4.0",
144
- "@types/react": "18.3.12",
145
- "@types/react-dom": "18.3.1"
144
+ "@types/react": "19.0.10",
145
+ "@types/react-dom": "19.0.4",
146
+ "react": "19.0.0",
147
+ "react-dom": "19.0.0"
146
148
  },
147
149
  "msw": {
148
150
  "workerDirectory": "../richie/static/richie/js"
@@ -152,7 +154,7 @@
152
154
  "yarn": "1.22.22"
153
155
  },
154
156
  "devDependencies": {
155
- "@storybook/addon-mdx-gfm": "8.4.6",
156
- "@storybook/addon-webpack5-compiler-babel": "3.0.3"
157
+ "@storybook/addon-mdx-gfm": "8.5.6",
158
+ "@storybook/addon-webpack5-compiler-babel": "3.0.5"
157
159
  }
158
160
  }
@@ -5,7 +5,8 @@
5
5
  // Draw a background grid in pure CSS
6
6
  @mixin draw-grid($line-color: null, $border-color: null) {
7
7
  @if $line-color {
8
- background: linear-gradient(-90deg, $line-color $onepixel, transparent $onepixel),
8
+ background:
9
+ linear-gradient(-90deg, $line-color $onepixel, transparent $onepixel),
9
10
  linear-gradient($line-color $onepixel, transparent $onepixel),
10
11
  linear-gradient(-90deg, $line-color $onepixel, transparent $onepixel),
11
12
  linear-gradient($line-color $onepixel, transparent $onepixel),
@@ -132,6 +132,7 @@
132
132
  --c--theme--breakpoints--lg: 992px;
133
133
  --c--theme--breakpoints--xl: 1200px;
134
134
  --c--theme--breakpoints--xxl: 1400px;
135
+ --c--components--tabs--border-bottom-color: var(--c--theme--colors--greyscale-300);
135
136
  --c--components--button--font-family: Montserrat;
136
137
  --c--components--dashboardlistavatar--saturation: 30;
137
138
  --c--components--dashboardlistavatar--lightness: 55;