richie-education 3.1.2 → 3.1.3-dev12
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/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -20
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +22 -35
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
- package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +75 -0
- package/js/components/SaleTunnel/index.stories.tsx +0 -1
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffer/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -17
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
- package/js/types/Joanie.ts +26 -30
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +12 -25
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +23 -38
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +27 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +174 -158
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +9 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +108 -28
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
|
@@ -6,12 +6,11 @@ import {
|
|
|
6
6
|
PacedCourseFactory,
|
|
7
7
|
} from 'utils/test/factories/richie';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
OfferFactory,
|
|
10
10
|
EnrollmentFactory,
|
|
11
11
|
CredentialOrderFactory,
|
|
12
12
|
ProductFactory,
|
|
13
|
-
|
|
14
|
-
OrderGroupFactory,
|
|
13
|
+
CredentialProductFactory,
|
|
15
14
|
} from 'utils/test/factories/joanie';
|
|
16
15
|
import {
|
|
17
16
|
CourseRun,
|
|
@@ -75,8 +74,8 @@ describe('CourseProductItem', () => {
|
|
|
75
74
|
}).format(price);
|
|
76
75
|
|
|
77
76
|
it('should display a loader until product is loaded', async () => {
|
|
78
|
-
const
|
|
79
|
-
const { product } =
|
|
77
|
+
const offer = OfferFactory().one();
|
|
78
|
+
const { product } = offer;
|
|
80
79
|
const productDeferred = new Deferred();
|
|
81
80
|
fetchMock.get(
|
|
82
81
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -93,17 +92,14 @@ describe('CourseProductItem', () => {
|
|
|
93
92
|
|
|
94
93
|
// - A loader should be displayed while product information are fetching
|
|
95
94
|
await expectSpinner('Loading product information...');
|
|
96
|
-
productDeferred.resolve(
|
|
95
|
+
productDeferred.resolve(offer);
|
|
97
96
|
await expectNoSpinner('Loading product information...');
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
it('renders product information for anonymous user', async () => {
|
|
101
|
-
const
|
|
102
|
-
const { product } =
|
|
103
|
-
fetchMock.get(
|
|
104
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
105
|
-
relation,
|
|
106
|
-
);
|
|
100
|
+
const offer = OfferFactory().one();
|
|
101
|
+
const { product } = offer;
|
|
102
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
107
103
|
|
|
108
104
|
render(
|
|
109
105
|
<CourseProductItem
|
|
@@ -132,7 +128,7 @@ describe('CourseProductItem', () => {
|
|
|
132
128
|
).not.toBeInTheDocument();
|
|
133
129
|
|
|
134
130
|
// - Render all target courses information
|
|
135
|
-
|
|
131
|
+
offer.product.target_courses.forEach((course) => {
|
|
136
132
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
137
133
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
138
134
|
// but we want to it to visually look like a h5
|
|
@@ -151,17 +147,130 @@ describe('CourseProductItem', () => {
|
|
|
151
147
|
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
152
148
|
});
|
|
153
149
|
|
|
150
|
+
it('renders discount rate for anonymous user', async () => {
|
|
151
|
+
const offer = OfferFactory({
|
|
152
|
+
product: CredentialProductFactory({
|
|
153
|
+
price: 840,
|
|
154
|
+
price_currency: 'EUR',
|
|
155
|
+
}).one(),
|
|
156
|
+
discounted_price: 800,
|
|
157
|
+
discount_rate: 0.3,
|
|
158
|
+
description: 'Year 2023 discount',
|
|
159
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
160
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
161
|
+
}).one();
|
|
162
|
+
const { product } = offer;
|
|
163
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
164
|
+
|
|
165
|
+
render(
|
|
166
|
+
<CourseProductItem
|
|
167
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
168
|
+
productId={product.id}
|
|
169
|
+
/>,
|
|
170
|
+
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
174
|
+
|
|
175
|
+
// - Render discount information
|
|
176
|
+
// Original price should be displayed as a del element
|
|
177
|
+
const originalPriceLabel = screen.getByText('Original price:');
|
|
178
|
+
expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
179
|
+
const originalPrice = screen.getByText(
|
|
180
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
181
|
+
);
|
|
182
|
+
expect(originalPrice.tagName).toBe('DEL');
|
|
183
|
+
expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
|
|
184
|
+
|
|
185
|
+
// Discounted price should be displayed as an ins element
|
|
186
|
+
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
187
|
+
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
188
|
+
const discountedPrice = screen.getByText(
|
|
189
|
+
priceFormatter(product.price_currency, offer.discounted_price!).replace(
|
|
190
|
+
/(\u202F|\u00a0)/g,
|
|
191
|
+
' ',
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
expect(discountedPrice.tagName).toBe('INS');
|
|
195
|
+
expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
|
|
196
|
+
|
|
197
|
+
// Discount description should be displayed
|
|
198
|
+
screen.getByText('Year 2023 discount');
|
|
199
|
+
|
|
200
|
+
// Discount rate should be displayed
|
|
201
|
+
screen.getByText('-30%');
|
|
202
|
+
|
|
203
|
+
// Discount date range should be displayed
|
|
204
|
+
screen.getByText('from Jan 01, 2023');
|
|
205
|
+
screen.getByText('to Dec 31, 2023');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('renders discount amount for anonymous user', async () => {
|
|
209
|
+
const offer = OfferFactory({
|
|
210
|
+
product: CredentialProductFactory({
|
|
211
|
+
price: 840,
|
|
212
|
+
price_currency: 'EUR',
|
|
213
|
+
}).one(),
|
|
214
|
+
discounted_price: 800,
|
|
215
|
+
discount_amount: 40,
|
|
216
|
+
description: 'Year 2023 discount',
|
|
217
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
218
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
219
|
+
}).one();
|
|
220
|
+
const { product } = offer;
|
|
221
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
222
|
+
|
|
223
|
+
render(
|
|
224
|
+
<CourseProductItem
|
|
225
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
226
|
+
productId={product.id}
|
|
227
|
+
/>,
|
|
228
|
+
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
232
|
+
|
|
233
|
+
// - Render discount information
|
|
234
|
+
// Original price should be displayed as a del element
|
|
235
|
+
const originalPriceLabel = screen.getByText('Original price:');
|
|
236
|
+
expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
237
|
+
const originalPrice = screen.getByText(
|
|
238
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
239
|
+
);
|
|
240
|
+
expect(originalPrice.tagName).toBe('DEL');
|
|
241
|
+
expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
|
|
242
|
+
|
|
243
|
+
// Discounted price should be displayed as an ins element
|
|
244
|
+
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
245
|
+
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
246
|
+
const discountedPrice = screen.getByText(
|
|
247
|
+
priceFormatter(product.price_currency, offer.discounted_price!).replace(
|
|
248
|
+
/(\u202F|\u00a0)/g,
|
|
249
|
+
' ',
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
expect(discountedPrice.tagName).toBe('INS');
|
|
253
|
+
expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
|
|
254
|
+
|
|
255
|
+
// Discount description should be displayed
|
|
256
|
+
screen.getByText('Year 2023 discount');
|
|
257
|
+
|
|
258
|
+
// Discount rate should be displayed
|
|
259
|
+
screen.getByText(priceFormatter(product.price_currency, -40).replace(/(\u202F|\u00a0)/g, ' '));
|
|
260
|
+
|
|
261
|
+
// Discount date range should be displayed
|
|
262
|
+
screen.getByText('from Jan 01, 2023');
|
|
263
|
+
screen.getByText('to Dec 31, 2023');
|
|
264
|
+
});
|
|
265
|
+
|
|
154
266
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
155
|
-
const
|
|
267
|
+
const offer = OfferFactory({
|
|
156
268
|
product: ProductFactory({
|
|
157
269
|
certificate_definition: undefined,
|
|
158
270
|
}).one(),
|
|
159
271
|
}).one();
|
|
160
|
-
const { product } =
|
|
161
|
-
fetchMock.get(
|
|
162
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
163
|
-
relation,
|
|
164
|
-
);
|
|
272
|
+
const { product } = offer;
|
|
273
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
165
274
|
|
|
166
275
|
render(
|
|
167
276
|
<CourseProductItem
|
|
@@ -179,16 +288,16 @@ describe('CourseProductItem', () => {
|
|
|
179
288
|
});
|
|
180
289
|
|
|
181
290
|
it('renders product information in compact mode', async () => {
|
|
182
|
-
const
|
|
291
|
+
const offer = OfferFactory().one();
|
|
183
292
|
fetchMock.get(
|
|
184
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
185
|
-
|
|
293
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
|
|
294
|
+
offer,
|
|
186
295
|
);
|
|
187
296
|
|
|
188
297
|
const { container } = render(
|
|
189
298
|
<CourseProductItem
|
|
190
299
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
191
|
-
productId={
|
|
300
|
+
productId={offer.product.id}
|
|
192
301
|
compact
|
|
193
302
|
/>,
|
|
194
303
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -196,14 +305,14 @@ describe('CourseProductItem', () => {
|
|
|
196
305
|
|
|
197
306
|
// In the header, we should display the product title, the product price
|
|
198
307
|
// and product date range and languages
|
|
199
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
308
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
200
309
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
201
310
|
// but we want to it to visually look like a h6
|
|
202
311
|
|
|
203
312
|
const $price = screen.getByText(
|
|
204
313
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
205
314
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
206
|
-
priceFormatter(
|
|
315
|
+
priceFormatter(offer.product.price_currency, offer.product.price).replace(
|
|
207
316
|
/(\u202F|\u00a0)/g,
|
|
208
317
|
' ',
|
|
209
318
|
),
|
|
@@ -219,7 +328,7 @@ describe('CourseProductItem', () => {
|
|
|
219
328
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
220
329
|
|
|
221
330
|
// - Any target courses information should be displayed
|
|
222
|
-
|
|
331
|
+
offer.product.target_courses.forEach((course) => {
|
|
223
332
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
224
333
|
expect($item).not.toBeInTheDocument();
|
|
225
334
|
});
|
|
@@ -228,7 +337,7 @@ describe('CourseProductItem', () => {
|
|
|
228
337
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
229
338
|
|
|
230
339
|
// - Render a login button
|
|
231
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
340
|
+
screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
|
|
232
341
|
// - Does not render PurchaseButton cta
|
|
233
342
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
234
343
|
});
|
|
@@ -236,8 +345,8 @@ describe('CourseProductItem', () => {
|
|
|
236
345
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
237
346
|
'renders product informations for %s order',
|
|
238
347
|
async (state) => {
|
|
239
|
-
const
|
|
240
|
-
const { product } =
|
|
348
|
+
const offer = OfferFactory().one();
|
|
349
|
+
const { product } = offer;
|
|
241
350
|
const order = CredentialOrderFactory({
|
|
242
351
|
product_id: product.id,
|
|
243
352
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -247,7 +356,7 @@ describe('CourseProductItem', () => {
|
|
|
247
356
|
|
|
248
357
|
fetchMock.get(
|
|
249
358
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
250
|
-
|
|
359
|
+
offer,
|
|
251
360
|
);
|
|
252
361
|
const orderQueryParameters = {
|
|
253
362
|
course_code: order.course.code,
|
|
@@ -261,13 +370,13 @@ describe('CourseProductItem', () => {
|
|
|
261
370
|
render(
|
|
262
371
|
<CourseProductItem
|
|
263
372
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
264
|
-
productId={
|
|
373
|
+
productId={offer.product.id}
|
|
265
374
|
/>,
|
|
266
375
|
);
|
|
267
376
|
|
|
268
377
|
// In the header, we should display the product title, the product price
|
|
269
378
|
// and product date range and languages
|
|
270
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
379
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
271
380
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
272
381
|
// but we want to it to visually look like a h6
|
|
273
382
|
|
|
@@ -297,8 +406,8 @@ describe('CourseProductItem', () => {
|
|
|
297
406
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
298
407
|
'renders product informations for %s order in compact mode',
|
|
299
408
|
async (state) => {
|
|
300
|
-
const
|
|
301
|
-
const { product } =
|
|
409
|
+
const offer = OfferFactory().one();
|
|
410
|
+
const { product } = offer;
|
|
302
411
|
const order = CredentialOrderFactory({
|
|
303
412
|
product_id: product.id,
|
|
304
413
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -308,7 +417,7 @@ describe('CourseProductItem', () => {
|
|
|
308
417
|
|
|
309
418
|
fetchMock.get(
|
|
310
419
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
311
|
-
|
|
420
|
+
offer,
|
|
312
421
|
);
|
|
313
422
|
const orderQueryParameters = {
|
|
314
423
|
course_code: order.course.code,
|
|
@@ -322,14 +431,14 @@ describe('CourseProductItem', () => {
|
|
|
322
431
|
render(
|
|
323
432
|
<CourseProductItem
|
|
324
433
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
325
|
-
productId={
|
|
434
|
+
productId={offer.product.id}
|
|
326
435
|
compact
|
|
327
436
|
/>,
|
|
328
437
|
);
|
|
329
438
|
|
|
330
439
|
// In the header, we should display the product title, the product price
|
|
331
440
|
// and product date range and languages
|
|
332
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
441
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
333
442
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
334
443
|
// but we want to it to visually look like a h6
|
|
335
444
|
|
|
@@ -339,7 +448,7 @@ describe('CourseProductItem', () => {
|
|
|
339
448
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
340
449
|
|
|
341
450
|
// - Any target courses information should be displayed
|
|
342
|
-
|
|
451
|
+
offer.product.target_courses.forEach((course) => {
|
|
343
452
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
344
453
|
expect($item).not.toBeInTheDocument();
|
|
345
454
|
});
|
|
@@ -350,8 +459,8 @@ describe('CourseProductItem', () => {
|
|
|
350
459
|
);
|
|
351
460
|
|
|
352
461
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
353
|
-
const
|
|
354
|
-
const { product } =
|
|
462
|
+
const offer = OfferFactory().one();
|
|
463
|
+
const { product } = offer;
|
|
355
464
|
const order = CredentialOrderFactory({
|
|
356
465
|
product_id: product.id,
|
|
357
466
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -359,10 +468,7 @@ describe('CourseProductItem', () => {
|
|
|
359
468
|
state,
|
|
360
469
|
}).one();
|
|
361
470
|
|
|
362
|
-
fetchMock.get(
|
|
363
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
364
|
-
relation,
|
|
365
|
-
);
|
|
471
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
366
472
|
const orderQueryParameters = {
|
|
367
473
|
course_code: order.course.code,
|
|
368
474
|
product_id: order.product_id,
|
|
@@ -412,17 +518,17 @@ describe('CourseProductItem', () => {
|
|
|
412
518
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
413
519
|
'renders product informations for a %s order in compact mode',
|
|
414
520
|
async (state) => {
|
|
415
|
-
const
|
|
521
|
+
const offer = OfferFactory().one();
|
|
416
522
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
417
|
-
product_id:
|
|
523
|
+
product_id: offer.product.id,
|
|
418
524
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
419
|
-
target_courses:
|
|
525
|
+
target_courses: offer.product.target_courses,
|
|
420
526
|
state,
|
|
421
527
|
}).one();
|
|
422
528
|
|
|
423
529
|
fetchMock.get(
|
|
424
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
425
|
-
|
|
530
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
|
|
531
|
+
offer,
|
|
426
532
|
);
|
|
427
533
|
const orderQueryParameters = {
|
|
428
534
|
product_id: order.product_id,
|
|
@@ -436,14 +542,14 @@ describe('CourseProductItem', () => {
|
|
|
436
542
|
|
|
437
543
|
render(
|
|
438
544
|
<CourseProductItem
|
|
439
|
-
productId={
|
|
545
|
+
productId={offer.product.id}
|
|
440
546
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
441
547
|
compact
|
|
442
548
|
/>,
|
|
443
549
|
);
|
|
444
550
|
|
|
445
551
|
// Wait for product information to be fetched
|
|
446
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
552
|
+
await screen.findByRole('heading', { level: 3, name: offer.product.title });
|
|
447
553
|
|
|
448
554
|
// - In place of product price, a label should be displayed
|
|
449
555
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -480,8 +586,8 @@ describe('CourseProductItem', () => {
|
|
|
480
586
|
);
|
|
481
587
|
|
|
482
588
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
483
|
-
const
|
|
484
|
-
const { product } =
|
|
589
|
+
const offer = OfferFactory().one();
|
|
590
|
+
const { product } = offer;
|
|
485
591
|
// - Create an order with an active enrollment
|
|
486
592
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
487
593
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -493,10 +599,7 @@ describe('CourseProductItem', () => {
|
|
|
493
599
|
target_enrollments: [enrollment],
|
|
494
600
|
}).one();
|
|
495
601
|
|
|
496
|
-
fetchMock.get(
|
|
497
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
498
|
-
relation,
|
|
499
|
-
);
|
|
602
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
500
603
|
const orderQueryParameters = {
|
|
501
604
|
product_id: order.product_id,
|
|
502
605
|
course_code: order.course?.code,
|
|
@@ -547,8 +650,8 @@ describe('CourseProductItem', () => {
|
|
|
547
650
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
548
651
|
'renders sale tunnel button if user already has a %s order',
|
|
549
652
|
async (state) => {
|
|
550
|
-
const
|
|
551
|
-
const { product } =
|
|
653
|
+
const offer = OfferFactory().one();
|
|
654
|
+
const { product } = offer;
|
|
552
655
|
const order = CredentialOrderFactory({
|
|
553
656
|
product_id: product.id,
|
|
554
657
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -557,7 +660,7 @@ describe('CourseProductItem', () => {
|
|
|
557
660
|
}).one();
|
|
558
661
|
fetchMock.get(
|
|
559
662
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
560
|
-
|
|
663
|
+
offer,
|
|
561
664
|
);
|
|
562
665
|
const orderQueryParameters = {
|
|
563
666
|
product_id: order.product_id,
|
|
@@ -588,7 +691,7 @@ describe('CourseProductItem', () => {
|
|
|
588
691
|
expect($price.classList.contains('h6')).toBe(true);
|
|
589
692
|
|
|
590
693
|
// - Render all target courses information
|
|
591
|
-
|
|
694
|
+
offer.product.target_courses.forEach((course) => {
|
|
592
695
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
593
696
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
594
697
|
// but we want to it to visually look like a h5
|
|
@@ -603,12 +706,9 @@ describe('CourseProductItem', () => {
|
|
|
603
706
|
);
|
|
604
707
|
|
|
605
708
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
606
|
-
const
|
|
607
|
-
const { product } =
|
|
608
|
-
fetchMock.get(
|
|
609
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
610
|
-
relation,
|
|
611
|
-
);
|
|
709
|
+
const offer = OfferFactory().one();
|
|
710
|
+
const { product } = offer;
|
|
711
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
612
712
|
const orderQueryParameters = {
|
|
613
713
|
product_id: product.id,
|
|
614
714
|
course_code: '00000',
|
|
@@ -638,7 +738,7 @@ describe('CourseProductItem', () => {
|
|
|
638
738
|
expect($price.classList.contains('h6')).toBe(true);
|
|
639
739
|
|
|
640
740
|
// - Render all target courses information
|
|
641
|
-
|
|
741
|
+
offer.product.target_courses.forEach((course) => {
|
|
642
742
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
643
743
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
644
744
|
// but we want to it to visually look like a h5
|
|
@@ -652,7 +752,7 @@ describe('CourseProductItem', () => {
|
|
|
652
752
|
});
|
|
653
753
|
|
|
654
754
|
it('renders error message when product fetching has failed', async () => {
|
|
655
|
-
const { product } =
|
|
755
|
+
const { product } = OfferFactory().one();
|
|
656
756
|
|
|
657
757
|
fetchMock.get(
|
|
658
758
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -673,20 +773,18 @@ describe('CourseProductItem', () => {
|
|
|
673
773
|
});
|
|
674
774
|
|
|
675
775
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
676
|
-
const
|
|
677
|
-
|
|
776
|
+
const offer = OfferFactory({
|
|
777
|
+
seats: 2,
|
|
778
|
+
nb_seats_available: 0,
|
|
678
779
|
}).one();
|
|
679
|
-
const { product } =
|
|
780
|
+
const { product } = offer;
|
|
680
781
|
const order = CredentialOrderFactory({
|
|
681
782
|
product_id: product.id,
|
|
682
783
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
683
784
|
target_courses: product.target_courses,
|
|
684
785
|
state: OrderState.DRAFT,
|
|
685
786
|
}).one();
|
|
686
|
-
fetchMock.get(
|
|
687
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
688
|
-
relation,
|
|
689
|
-
);
|
|
787
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
|
|
690
788
|
const orderQueryParameters = {
|
|
691
789
|
product_id: order.product_id,
|
|
692
790
|
course_code: order.course?.code,
|
|
@@ -710,86 +808,4 @@ describe('CourseProductItem', () => {
|
|
|
710
808
|
expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
|
|
711
809
|
screen.getByText('Sorry, no seats available for now');
|
|
712
810
|
});
|
|
713
|
-
|
|
714
|
-
it('renders one payment button when one of two order groups is full', async () => {
|
|
715
|
-
const relation = CourseProductRelationFactory({
|
|
716
|
-
order_groups: [OrderGroupFullFactory().one(), OrderGroupFactory().one()],
|
|
717
|
-
}).one();
|
|
718
|
-
const { product } = relation;
|
|
719
|
-
const order = CredentialOrderFactory({
|
|
720
|
-
product_id: product.id,
|
|
721
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
722
|
-
target_courses: product.target_courses,
|
|
723
|
-
state: OrderState.DRAFT,
|
|
724
|
-
}).one();
|
|
725
|
-
fetchMock.get(
|
|
726
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
727
|
-
relation,
|
|
728
|
-
);
|
|
729
|
-
const orderQueryParameters = {
|
|
730
|
-
product_id: order.product_id,
|
|
731
|
-
course_code: order.course?.code,
|
|
732
|
-
state: NOT_CANCELED_ORDER_STATES,
|
|
733
|
-
};
|
|
734
|
-
fetchMock.get(
|
|
735
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
736
|
-
[order],
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
render(
|
|
740
|
-
<CourseProductItem
|
|
741
|
-
productId={product.id}
|
|
742
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
743
|
-
/>,
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
// wait for component to be fully loaded
|
|
747
|
-
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
748
|
-
|
|
749
|
-
expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
|
|
750
|
-
screen.getByRole('button', { name: product.call_to_action });
|
|
751
|
-
screen.getByText(relation.order_groups[1].nb_available_seats + ' remaining seats');
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
it('renders mutliple payment button when there are multiple order groups', async () => {
|
|
755
|
-
const relation = CourseProductRelationFactory({
|
|
756
|
-
order_groups: [OrderGroupFactory().one(), OrderGroupFactory({ nb_available_seats: 1 }).one()],
|
|
757
|
-
}).one();
|
|
758
|
-
const { product } = relation;
|
|
759
|
-
const order = CredentialOrderFactory({
|
|
760
|
-
product_id: product.id,
|
|
761
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
762
|
-
target_courses: product.target_courses,
|
|
763
|
-
state: OrderState.DRAFT,
|
|
764
|
-
}).one();
|
|
765
|
-
fetchMock.get(
|
|
766
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
767
|
-
relation,
|
|
768
|
-
);
|
|
769
|
-
const orderQueryParameters = {
|
|
770
|
-
product_id: order.product_id,
|
|
771
|
-
course_code: order.course?.code,
|
|
772
|
-
state: NOT_CANCELED_ORDER_STATES,
|
|
773
|
-
};
|
|
774
|
-
fetchMock.get(
|
|
775
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
776
|
-
[order],
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
render(
|
|
780
|
-
<CourseProductItem
|
|
781
|
-
productId={product.id}
|
|
782
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
783
|
-
/>,
|
|
784
|
-
);
|
|
785
|
-
|
|
786
|
-
// wait for component to be fully loaded
|
|
787
|
-
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
788
|
-
|
|
789
|
-
expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
|
|
790
|
-
expect(screen.getAllByTestId('PurchaseButton__cta')).toHaveLength(2);
|
|
791
|
-
expect(screen.getAllByRole('button', { name: product.call_to_action })).toHaveLength(2);
|
|
792
|
-
screen.getByText(relation.order_groups[0].nb_available_seats + ' remaining seats');
|
|
793
|
-
screen.getByText('Last remaining seat!');
|
|
794
|
-
});
|
|
795
811
|
});
|
|
@@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { StorybookHelper } from 'utils/StorybookHelper';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
OfferFactory,
|
|
7
7
|
CourseRunFactory,
|
|
8
8
|
CredentialOrderFactory,
|
|
9
9
|
CredentialProductFactory,
|
|
@@ -22,7 +22,14 @@ const render = (args: CourseProductItemProps, options?: Maybe<{ order: Credentia
|
|
|
22
22
|
fetchMock.get(`http://localhost:8071/api/v1.0/addresses/`, [], { overwriteRoutes: true });
|
|
23
23
|
fetchMock.get(
|
|
24
24
|
`http://localhost:8071/api/v1.0/courses/${args.course.code}/products/${args.productId}/`,
|
|
25
|
-
|
|
25
|
+
OfferFactory({
|
|
26
|
+
product: CredentialProductFactory({
|
|
27
|
+
price: 840,
|
|
28
|
+
price_currency: 'EUR',
|
|
29
|
+
}).one(),
|
|
30
|
+
discounted_price: 800,
|
|
31
|
+
discount_rate: 0.3,
|
|
32
|
+
}).one(),
|
|
26
33
|
{ overwriteRoutes: true },
|
|
27
34
|
);
|
|
28
35
|
fetchMock.get(
|