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.
- package/js/components/AddressesManagement/index.spec.tsx +1 -1
- package/js/components/AddressesManagement/index.tsx +123 -129
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +2 -4
- package/js/components/ContractFrame/AbstractContractFrame.tsx +1 -1
- package/js/components/Icon/index.stories.tsx +1 -1
- package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +2 -2
- package/js/hooks/useCreditCards/index.spec.tsx +3 -2
- package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +1 -1
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +1 -1
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +8 -5
- package/js/utils/test/wrappers/types.ts +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +1 -1
- package/js/widgets/Dashboard/components/SearchBar/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/SearchResultsCount/index.spec.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +6 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +1 -5
- package/package.json +64 -62
- package/scss/components/_styleguide.scss +2 -1
- 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,
|
|
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 =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
setError(undefined);
|
|
185
|
+
if (editedAddress) {
|
|
186
|
+
document.querySelector<HTMLElement>('[name="address-form"] input')?.focus();
|
|
187
|
+
}
|
|
188
|
+
}, [editedAddress]);
|
|
191
189
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
{
|
|
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
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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.
|
|
207
|
+
await screen.findByRole('heading', { name: 'Signing the contract ...' });
|
|
208
208
|
|
|
209
209
|
// Then the signature check polling should be started
|
|
210
|
-
await
|
|
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
|
|
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
|
-
|
|
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.
|
|
184
|
+
await screen.findByRole('heading', {
|
|
185
185
|
level: 6,
|
|
186
186
|
name: creditCardUpdated.title,
|
|
187
187
|
});
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx
CHANGED
|
@@ -98,7 +98,7 @@ describe.each([
|
|
|
98
98
|
expect(mockCheckArchive).not.toHaveBeenCalled();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
it('should check if archive exist when
|
|
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
|
-
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(result.current.isPolling).toBe(false);
|
|
120
|
+
});
|
|
119
121
|
expect(result.current.isContractArchiveExists).toBe(true);
|
|
120
122
|
});
|
|
121
123
|
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx
CHANGED
|
@@ -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 }>) =>
|
|
22
|
+
wrapper?: Nullable<(props: PropsWithChildren<{ options?: AppWrapperProps }>) => ReactElement>;
|
|
23
23
|
intlOptions?: IntlWrapperProps;
|
|
24
24
|
queryOptions?: QueryOptions;
|
|
25
25
|
historyOptions?: History;
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx
CHANGED
|
@@ -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('
|
|
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('
|
|
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.
|
|
170
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
41
|
+
"@babel/core": "7.26.9",
|
|
42
42
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
|
43
|
-
"@babel/plugin-transform-modules-commonjs": "7.
|
|
44
|
-
"@babel/preset-env": "7.26.
|
|
45
|
-
"@babel/preset-react": "7.
|
|
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.
|
|
48
|
-
"@formatjs/cli": "6.
|
|
49
|
-
"@formatjs/intl-relativetimeformat": "11.4.
|
|
50
|
-
"@hookform/resolvers": "
|
|
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": "
|
|
53
|
-
"@openfun/cunningham-tokens": "2.
|
|
54
|
-
"@sentry/browser": "
|
|
55
|
-
"@sentry/types": "
|
|
56
|
-
"@storybook/addon-actions": "8.
|
|
57
|
-
"@storybook/addon-essentials": "8.
|
|
58
|
-
"@storybook/addon-interactions": "8.
|
|
59
|
-
"@storybook/addon-links": "8.
|
|
60
|
-
"@storybook/react": "8.
|
|
61
|
-
"@storybook/react-webpack5": "8.
|
|
62
|
-
"@storybook/test": "8.
|
|
63
|
-
"@tanstack/query-core": "5.
|
|
64
|
-
"@tanstack/query-sync-storage-persister": "5.
|
|
65
|
-
"@tanstack/react-query": "5.
|
|
66
|
-
"@tanstack/react-query-devtools": "5.
|
|
67
|
-
"@tanstack/react-query-persist-client": "5.
|
|
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
|
|
71
|
-
"@testing-library/user-event": "14.
|
|
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": "
|
|
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": "
|
|
79
|
+
"@types/react": "19.0.10",
|
|
80
80
|
"@types/react-autosuggest": "10.1.11",
|
|
81
|
-
"@types/react-dom": "
|
|
81
|
+
"@types/react-dom": "19.0.4",
|
|
82
82
|
"@types/react-modal": "3.16.3",
|
|
83
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
84
|
-
"@typescript-eslint/parser": "8.
|
|
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.
|
|
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": "
|
|
97
|
-
"eslint-import-resolver-webpack": "0.13.
|
|
98
|
-
"eslint-plugin-compat": "6.0.
|
|
99
|
-
"eslint-plugin-formatjs": "5.2.
|
|
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.
|
|
103
|
-
"eslint-plugin-react": "7.37.
|
|
104
|
-
"eslint-plugin-react-hooks": "5.
|
|
105
|
-
"eslint-plugin-storybook": "0.11.
|
|
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.
|
|
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.
|
|
117
|
+
"msw": "2.7.0",
|
|
118
118
|
"node-fetch": ">2.6.6 <3",
|
|
119
|
-
"nodemon": "3.1.
|
|
120
|
-
"prettier": "3.
|
|
119
|
+
"nodemon": "3.1.9",
|
|
120
|
+
"prettier": "3.5.1",
|
|
121
121
|
"query-string": "9.1.1",
|
|
122
|
-
"react": "
|
|
122
|
+
"react": "19.0.0",
|
|
123
123
|
"react-autosuggest": "10.1.0",
|
|
124
|
-
"react-dom": "
|
|
125
|
-
"react-hook-form": "7.
|
|
126
|
-
"react-intl": "7.
|
|
127
|
-
"react-modal": "3.16.
|
|
128
|
-
"react-router": "7.
|
|
129
|
-
"sass": "1.
|
|
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.
|
|
131
|
+
"storybook": "8.5.6",
|
|
132
132
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
|
133
|
-
"typescript": "5.7.
|
|
134
|
-
"uuid": "11.0.
|
|
135
|
-
"webpack": "5.
|
|
136
|
-
"webpack-cli": "
|
|
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.
|
|
140
|
+
"yup": "1.6.1"
|
|
141
141
|
},
|
|
142
142
|
"resolutions": {
|
|
143
143
|
"@testing-library/dom": "10.4.0",
|
|
144
|
-
"@types/react": "
|
|
145
|
-
"@types/react-dom": "
|
|
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.
|
|
156
|
-
"@storybook/addon-webpack5-compiler-babel": "3.0.
|
|
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:
|
|
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;
|