richie-education 2.28.2-dev39 → 2.28.2-dev53
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 +12 -16
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
- package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
- package/js/components/ContractFrame/_styles.scss +6 -14
- package/js/components/CreditCardSelector/index.spec.tsx +7 -7
- package/js/components/CreditCardSelector/index.tsx +2 -2
- package/js/components/DownloadContractButton/index.spec.tsx +1 -1
- package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
- package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
- package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
- package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
- package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
- package/js/components/PaymentInterfaces/types.ts +5 -2
- package/js/components/PurchaseButton/index.spec.tsx +69 -37
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
- package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
- package/js/components/SaleTunnel/_styles.scss +10 -1
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
- package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
- package/js/components/SaleTunnel/index.spec.tsx +330 -779
- package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
- package/js/components/SignContractButton/index.spec.tsx +16 -20
- package/js/components/SignContractButton/index.tsx +3 -1
- package/js/hooks/useCreditCards/index.spec.tsx +70 -6
- package/js/hooks/useCreditCards/index.ts +49 -11
- package/js/hooks/useOrders/index.spec.tsx +322 -0
- package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
- package/js/hooks/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +1 -0
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
- package/js/settings/settings.test.ts +11 -2
- package/js/types/Joanie.ts +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- package/js/utils/test/factories/joanie.ts +36 -51
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
- package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
- package/package.json +1 -1
- package/scss/components/_index.scss +2 -1
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
Enrollment,
|
|
19
19
|
CredentialOrder,
|
|
20
20
|
OrderState,
|
|
21
|
-
|
|
21
|
+
PURCHASABLE_ORDER_STATES,
|
|
22
|
+
ENROLLABLE_ORDER_STATES,
|
|
23
|
+
NOT_CANCELED_ORDER_STATES,
|
|
22
24
|
} from 'types/Joanie';
|
|
23
25
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
24
26
|
import { Deferred } from 'utils/test/deferred';
|
|
@@ -176,7 +178,7 @@ describe('CourseProductItem', () => {
|
|
|
176
178
|
expect(screen.queryByTestId('CertificateItem')).toBeNull();
|
|
177
179
|
});
|
|
178
180
|
|
|
179
|
-
it('renders product
|
|
181
|
+
it('renders product information in compact mode', async () => {
|
|
180
182
|
const relation = CourseProductRelationFactory().one();
|
|
181
183
|
fetchMock.get(
|
|
182
184
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
|
|
@@ -231,13 +233,130 @@ describe('CourseProductItem', () => {
|
|
|
231
233
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
232
234
|
});
|
|
233
235
|
|
|
234
|
-
it(
|
|
236
|
+
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
237
|
+
'renders product informations for %s order',
|
|
238
|
+
async (state) => {
|
|
239
|
+
const relation = CourseProductRelationFactory().one();
|
|
240
|
+
const { product } = relation;
|
|
241
|
+
const order = CredentialOrderFactory({
|
|
242
|
+
product_id: product.id,
|
|
243
|
+
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
244
|
+
target_courses: product.target_courses,
|
|
245
|
+
state,
|
|
246
|
+
}).one();
|
|
247
|
+
|
|
248
|
+
fetchMock.get(
|
|
249
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
250
|
+
relation,
|
|
251
|
+
);
|
|
252
|
+
const orderQueryParameters = {
|
|
253
|
+
course_code: order.course.code,
|
|
254
|
+
product_id: order.product_id,
|
|
255
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
256
|
+
};
|
|
257
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
258
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
259
|
+
fetchMock.get(url, [order]);
|
|
260
|
+
|
|
261
|
+
render(
|
|
262
|
+
<CourseProductItem
|
|
263
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
264
|
+
productId={relation.product.id}
|
|
265
|
+
/>,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// In the header, we should display the product title, the product price
|
|
269
|
+
// and product date range and languages
|
|
270
|
+
await screen.findByRole('heading', { level: 3, name: relation.product.title });
|
|
271
|
+
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
272
|
+
// but we want to it to visually look like a h6
|
|
273
|
+
|
|
274
|
+
// - In place of product price, a label should be displayed
|
|
275
|
+
const $enrolledInfo = await screen.findByText('Purchased');
|
|
276
|
+
expect($enrolledInfo.tagName).toBe('STRONG');
|
|
277
|
+
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
278
|
+
|
|
279
|
+
// - Render all order's target courses information with CourseRunList component
|
|
280
|
+
await waitFor(() => {
|
|
281
|
+
order.target_courses.forEach((course) => {
|
|
282
|
+
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
283
|
+
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
284
|
+
// but we want to it to visually look like a h5
|
|
285
|
+
const $courseTitle = getByText($item, course.title);
|
|
286
|
+
expect($courseTitle.tagName).toBe('STRONG');
|
|
287
|
+
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
288
|
+
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// - Does not render PurchaseButton cta
|
|
293
|
+
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
298
|
+
'renders product informations for %s order in compact mode',
|
|
299
|
+
async (state) => {
|
|
300
|
+
const relation = CourseProductRelationFactory().one();
|
|
301
|
+
const { product } = relation;
|
|
302
|
+
const order = CredentialOrderFactory({
|
|
303
|
+
product_id: product.id,
|
|
304
|
+
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
305
|
+
target_courses: product.target_courses,
|
|
306
|
+
state,
|
|
307
|
+
}).one();
|
|
308
|
+
|
|
309
|
+
fetchMock.get(
|
|
310
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
311
|
+
relation,
|
|
312
|
+
);
|
|
313
|
+
const orderQueryParameters = {
|
|
314
|
+
course_code: order.course.code,
|
|
315
|
+
product_id: order.product_id,
|
|
316
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
317
|
+
};
|
|
318
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
319
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
320
|
+
fetchMock.get(url, [order]);
|
|
321
|
+
|
|
322
|
+
render(
|
|
323
|
+
<CourseProductItem
|
|
324
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
325
|
+
productId={relation.product.id}
|
|
326
|
+
compact
|
|
327
|
+
/>,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// In the header, we should display the product title, the product price
|
|
331
|
+
// and product date range and languages
|
|
332
|
+
await screen.findByRole('heading', { level: 3, name: relation.product.title });
|
|
333
|
+
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
334
|
+
// but we want to it to visually look like a h6
|
|
335
|
+
|
|
336
|
+
// - In place of product price, a label should be displayed
|
|
337
|
+
const $enrolledInfo = await screen.findByText('Purchased');
|
|
338
|
+
expect($enrolledInfo.tagName).toBe('STRONG');
|
|
339
|
+
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
340
|
+
|
|
341
|
+
// - Any target courses information should be displayed
|
|
342
|
+
relation.product.target_courses.forEach((course) => {
|
|
343
|
+
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
344
|
+
expect($item).not.toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// - Does not render PurchaseButton cta
|
|
348
|
+
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
235
353
|
const relation = CourseProductRelationFactory().one();
|
|
236
354
|
const { product } = relation;
|
|
237
355
|
const order = CredentialOrderFactory({
|
|
238
356
|
product_id: product.id,
|
|
239
357
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
240
358
|
target_courses: product.target_courses,
|
|
359
|
+
state,
|
|
241
360
|
}).one();
|
|
242
361
|
|
|
243
362
|
fetchMock.get(
|
|
@@ -245,14 +364,13 @@ describe('CourseProductItem', () => {
|
|
|
245
364
|
relation,
|
|
246
365
|
);
|
|
247
366
|
const orderQueryParameters = {
|
|
248
|
-
product_id: order.product_id,
|
|
249
367
|
course_code: order.course.code,
|
|
250
|
-
|
|
368
|
+
product_id: order.product_id,
|
|
369
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
251
370
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
371
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
372
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
373
|
+
fetchMock.get(url, [order]);
|
|
256
374
|
|
|
257
375
|
render(
|
|
258
376
|
<CourseProductItem
|
|
@@ -291,71 +409,75 @@ describe('CourseProductItem', () => {
|
|
|
291
409
|
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
292
410
|
});
|
|
293
411
|
|
|
294
|
-
it(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
412
|
+
it.each(ENROLLABLE_ORDER_STATES)(
|
|
413
|
+
'renders product informations for a %s order in compact mode',
|
|
414
|
+
async (state) => {
|
|
415
|
+
const relation = CourseProductRelationFactory().one();
|
|
416
|
+
const order: CredentialOrder = CredentialOrderFactory({
|
|
417
|
+
product_id: relation.product.id,
|
|
418
|
+
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
419
|
+
target_courses: relation.product.target_courses,
|
|
420
|
+
state,
|
|
421
|
+
}).one();
|
|
301
422
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
423
|
+
fetchMock.get(
|
|
424
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
|
|
425
|
+
relation,
|
|
426
|
+
);
|
|
427
|
+
const orderQueryParameters = {
|
|
428
|
+
product_id: order.product_id,
|
|
429
|
+
course_code: order.course?.code,
|
|
430
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
431
|
+
};
|
|
432
|
+
fetchMock.get(
|
|
433
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
434
|
+
[order],
|
|
435
|
+
);
|
|
315
436
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
437
|
+
render(
|
|
438
|
+
<CourseProductItem
|
|
439
|
+
productId={relation.product.id}
|
|
440
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
441
|
+
compact
|
|
442
|
+
/>,
|
|
443
|
+
);
|
|
323
444
|
|
|
324
|
-
|
|
325
|
-
|
|
445
|
+
// Wait for product information to be fetched
|
|
446
|
+
await screen.findByRole('heading', { level: 3, name: relation.product.title });
|
|
326
447
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
448
|
+
// - In place of product price, a label should be displayed
|
|
449
|
+
const $enrolledInfo = await screen.findByText('Purchased');
|
|
450
|
+
expect($enrolledInfo.tagName).toBe('STRONG');
|
|
451
|
+
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
331
452
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
453
|
+
// - Product date range and languages should not be displayed anymore
|
|
454
|
+
expect(screen.queryByTestId('product-widget__header-metadata-dates')).not.toBeInTheDocument();
|
|
455
|
+
expect(
|
|
456
|
+
screen.queryByTestId('product-widget__header-metadata-languages'),
|
|
457
|
+
).not.toBeInTheDocument();
|
|
337
458
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
459
|
+
// - Render all order's target courses information with EnrollableCourseRunList component
|
|
460
|
+
await waitFor(() => {
|
|
461
|
+
order.target_courses.forEach((course) => {
|
|
462
|
+
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
463
|
+
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
464
|
+
// but we want to it to visually look like a h5
|
|
465
|
+
const $courseTitle = getByText($item, course.title);
|
|
466
|
+
expect($courseTitle.tagName).toBe('STRONG');
|
|
467
|
+
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
468
|
+
screen.getByTestId(
|
|
469
|
+
`EnrollableCourseRunList-${course.course_runs.map(({ id }) => id).join('-')}-${order.id}`,
|
|
470
|
+
);
|
|
471
|
+
});
|
|
350
472
|
});
|
|
351
|
-
});
|
|
352
473
|
|
|
353
|
-
|
|
354
|
-
|
|
474
|
+
// - Render <CertificateItem />
|
|
475
|
+
screen.getByTestId('CertificateItem');
|
|
355
476
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
477
|
+
// - Does not Render PurchaseButton cta
|
|
478
|
+
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
479
|
+
},
|
|
480
|
+
);
|
|
359
481
|
|
|
360
482
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
361
483
|
const relation = CourseProductRelationFactory().one();
|
|
@@ -378,7 +500,7 @@ describe('CourseProductItem', () => {
|
|
|
378
500
|
const orderQueryParameters = {
|
|
379
501
|
product_id: order.product_id,
|
|
380
502
|
course_code: order.course?.code,
|
|
381
|
-
state:
|
|
503
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
382
504
|
};
|
|
383
505
|
fetchMock.get(
|
|
384
506
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
@@ -422,188 +544,16 @@ describe('CourseProductItem', () => {
|
|
|
422
544
|
});
|
|
423
545
|
});
|
|
424
546
|
|
|
425
|
-
it(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const order = CredentialOrderFactory({
|
|
429
|
-
product_id: product.id,
|
|
430
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
431
|
-
target_courses: product.target_courses,
|
|
432
|
-
state: OrderState.PENDING,
|
|
433
|
-
}).one();
|
|
434
|
-
fetchMock.get(
|
|
435
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
436
|
-
relation,
|
|
437
|
-
);
|
|
438
|
-
const orderQueryParameters = {
|
|
439
|
-
product_id: order.product_id,
|
|
440
|
-
course_code: order.course?.code,
|
|
441
|
-
state: ACTIVE_ORDER_STATES,
|
|
442
|
-
};
|
|
443
|
-
fetchMock.get(
|
|
444
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
445
|
-
[order],
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
render(
|
|
449
|
-
<CourseProductItem
|
|
450
|
-
productId={product.id}
|
|
451
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
452
|
-
/>,
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
// Wait for product information to be fetched
|
|
456
|
-
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
457
|
-
|
|
458
|
-
const $price = screen.getByText(
|
|
459
|
-
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
460
|
-
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
461
|
-
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
462
|
-
);
|
|
463
|
-
expect($price.tagName).toBe('STRONG');
|
|
464
|
-
expect($price.classList.contains('h6')).toBe(true);
|
|
465
|
-
|
|
466
|
-
// - Render all target courses information
|
|
467
|
-
relation.product.target_courses.forEach((course) => {
|
|
468
|
-
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
469
|
-
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
470
|
-
// but we want to it to visually look like a h5
|
|
471
|
-
const $courseTitle = getByText($item, course.title);
|
|
472
|
-
expect($courseTitle.tagName).toBe('STRONG');
|
|
473
|
-
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
474
|
-
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
screen.getByRole('button', { name: product.call_to_action });
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
481
|
-
const relation = CourseProductRelationFactory().one();
|
|
482
|
-
const { product } = relation;
|
|
483
|
-
fetchMock.get(
|
|
484
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
485
|
-
relation,
|
|
486
|
-
);
|
|
487
|
-
const orderQueryParameters = {
|
|
488
|
-
product_id: product.id,
|
|
489
|
-
course_code: '00000',
|
|
490
|
-
state: ACTIVE_ORDER_STATES,
|
|
491
|
-
};
|
|
492
|
-
fetchMock.get(
|
|
493
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
494
|
-
[],
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
render(
|
|
498
|
-
<CourseProductItem
|
|
499
|
-
productId={product.id}
|
|
500
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
501
|
-
/>,
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
// Wait for product information to be fetched
|
|
505
|
-
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
506
|
-
|
|
507
|
-
const $price = screen.getByText(
|
|
508
|
-
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
509
|
-
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
510
|
-
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
511
|
-
);
|
|
512
|
-
expect($price.tagName).toBe('STRONG');
|
|
513
|
-
expect($price.classList.contains('h6')).toBe(true);
|
|
514
|
-
|
|
515
|
-
// - Render all target courses information
|
|
516
|
-
relation.product.target_courses.forEach((course) => {
|
|
517
|
-
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
518
|
-
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
519
|
-
// but we want to it to visually look like a h5
|
|
520
|
-
const $courseTitle = getByText($item, course.title);
|
|
521
|
-
expect($courseTitle.tagName).toBe('STRONG');
|
|
522
|
-
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
523
|
-
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
screen.getByRole('button', { name: product.call_to_action });
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it('does not render sale tunnel button if user already has a submitted order', async () => {
|
|
530
|
-
const relation = CourseProductRelationFactory().one();
|
|
531
|
-
const { product } = relation;
|
|
532
|
-
const order = CredentialOrderFactory({
|
|
533
|
-
product_id: product.id,
|
|
534
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
535
|
-
target_courses: product.target_courses,
|
|
536
|
-
state: OrderState.SUBMITTED,
|
|
537
|
-
}).one();
|
|
538
|
-
fetchMock.get(
|
|
539
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
540
|
-
relation,
|
|
541
|
-
);
|
|
542
|
-
const orderQueryParameters = {
|
|
543
|
-
product_id: order.product_id,
|
|
544
|
-
course_code: order.course?.code,
|
|
545
|
-
state: ACTIVE_ORDER_STATES,
|
|
546
|
-
};
|
|
547
|
-
fetchMock.get(
|
|
548
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
549
|
-
[order],
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
render(
|
|
553
|
-
<CourseProductItem
|
|
554
|
-
productId={product.id}
|
|
555
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
556
|
-
/>,
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
// Wait for product information to be fetched
|
|
560
|
-
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
561
|
-
|
|
562
|
-
// - In place of product price, a label "Pending" should be displayed
|
|
563
|
-
const $enrolledInfo = await screen.findByText('Pending');
|
|
564
|
-
expect($enrolledInfo.tagName).toBe('STRONG');
|
|
565
|
-
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
566
|
-
|
|
567
|
-
// - As order is pending, the user should not be able to enroll to course runs.
|
|
568
|
-
await waitFor(() => {
|
|
569
|
-
order.target_courses.forEach((course) => {
|
|
570
|
-
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
571
|
-
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
572
|
-
// but we want to it to visually look like a h5
|
|
573
|
-
const $courseTitle = getByText($item, course.title);
|
|
574
|
-
expect($courseTitle.tagName).toBe('STRONG');
|
|
575
|
-
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
576
|
-
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
// - Render <CertificateItem />
|
|
581
|
-
screen.getByTestId('CertificateItem');
|
|
582
|
-
|
|
583
|
-
// - Does not Render PurchaseButton cta
|
|
584
|
-
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it.each([
|
|
588
|
-
{
|
|
589
|
-
orderState: OrderState.PENDING,
|
|
590
|
-
},
|
|
591
|
-
{
|
|
592
|
-
orderState: OrderState.SUBMITTED,
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
orderState: OrderState.DRAFT,
|
|
596
|
-
},
|
|
597
|
-
])(
|
|
598
|
-
"should not render sign button and banner for order's state $orderState",
|
|
599
|
-
async ({ orderState }) => {
|
|
547
|
+
it.each(PURCHASABLE_ORDER_STATES)(
|
|
548
|
+
'renders sale tunnel button if user already has a %s order',
|
|
549
|
+
async (state) => {
|
|
600
550
|
const relation = CourseProductRelationFactory().one();
|
|
601
551
|
const { product } = relation;
|
|
602
552
|
const order = CredentialOrderFactory({
|
|
603
553
|
product_id: product.id,
|
|
604
|
-
target_courses: product.target_courses,
|
|
605
554
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
606
|
-
|
|
555
|
+
target_courses: product.target_courses,
|
|
556
|
+
state,
|
|
607
557
|
}).one();
|
|
608
558
|
fetchMock.get(
|
|
609
559
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -611,10 +561,9 @@ describe('CourseProductItem', () => {
|
|
|
611
561
|
);
|
|
612
562
|
const orderQueryParameters = {
|
|
613
563
|
product_id: order.product_id,
|
|
614
|
-
course_code: order.course
|
|
615
|
-
state:
|
|
564
|
+
course_code: order.course?.code,
|
|
565
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
616
566
|
};
|
|
617
|
-
|
|
618
567
|
fetchMock.get(
|
|
619
568
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
620
569
|
[order],
|
|
@@ -628,45 +577,46 @@ describe('CourseProductItem', () => {
|
|
|
628
577
|
);
|
|
629
578
|
|
|
630
579
|
// Wait for product information to be fetched
|
|
631
|
-
|
|
632
|
-
await screen.findByRole('heading', { level: 3, name: product.title }),
|
|
633
|
-
).toBeInTheDocument();
|
|
580
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
634
581
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
).
|
|
582
|
+
const $price = screen.getByText(
|
|
583
|
+
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
584
|
+
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
585
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
586
|
+
);
|
|
587
|
+
expect($price.tagName).toBe('STRONG');
|
|
588
|
+
expect($price.classList.contains('h6')).toBe(true);
|
|
641
589
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
590
|
+
// - Render all target courses information
|
|
591
|
+
relation.product.target_courses.forEach((course) => {
|
|
592
|
+
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
593
|
+
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
594
|
+
// but we want to it to visually look like a h5
|
|
595
|
+
const $courseTitle = getByText($item, course.title);
|
|
596
|
+
expect($courseTitle.tagName).toBe('STRONG');
|
|
597
|
+
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
598
|
+
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
screen.getByRole('button', { name: product.call_to_action });
|
|
645
602
|
},
|
|
646
603
|
);
|
|
647
604
|
|
|
648
|
-
it('renders
|
|
605
|
+
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
649
606
|
const relation = CourseProductRelationFactory().one();
|
|
650
607
|
const { product } = relation;
|
|
651
|
-
const order = CredentialOrderFactory({
|
|
652
|
-
product_id: product.id,
|
|
653
|
-
target_courses: product.target_courses,
|
|
654
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
655
|
-
state: OrderState.VALIDATED,
|
|
656
|
-
}).one();
|
|
657
608
|
fetchMock.get(
|
|
658
609
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
659
610
|
relation,
|
|
660
611
|
);
|
|
661
612
|
const orderQueryParameters = {
|
|
662
|
-
product_id:
|
|
663
|
-
course_code:
|
|
664
|
-
state:
|
|
613
|
+
product_id: product.id,
|
|
614
|
+
course_code: '00000',
|
|
615
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
665
616
|
};
|
|
666
|
-
|
|
667
617
|
fetchMock.get(
|
|
668
618
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
669
|
-
[
|
|
619
|
+
[],
|
|
670
620
|
);
|
|
671
621
|
|
|
672
622
|
render(
|
|
@@ -679,66 +629,26 @@ describe('CourseProductItem', () => {
|
|
|
679
629
|
// Wait for product information to be fetched
|
|
680
630
|
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
681
631
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
it('adapts layout when user has a pending order and compact prop is set', async () => {
|
|
688
|
-
const relation = CourseProductRelationFactory().one();
|
|
689
|
-
const order: CredentialOrder = CredentialOrderFactory({
|
|
690
|
-
product_id: relation.product.id,
|
|
691
|
-
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
692
|
-
target_courses: relation.product.target_courses,
|
|
693
|
-
state: OrderState.SUBMITTED,
|
|
694
|
-
}).one();
|
|
695
|
-
fetchMock.get(
|
|
696
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
|
|
697
|
-
relation,
|
|
698
|
-
);
|
|
699
|
-
const orderQueryParameters = {
|
|
700
|
-
product_id: order.product_id,
|
|
701
|
-
course_code: order.course?.code,
|
|
702
|
-
state: ACTIVE_ORDER_STATES,
|
|
703
|
-
};
|
|
704
|
-
fetchMock.get(
|
|
705
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
706
|
-
[order],
|
|
707
|
-
);
|
|
708
|
-
|
|
709
|
-
render(
|
|
710
|
-
<CourseProductItem
|
|
711
|
-
productId={relation.product.id}
|
|
712
|
-
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
713
|
-
compact={true}
|
|
714
|
-
/>,
|
|
632
|
+
const $price = screen.getByText(
|
|
633
|
+
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
634
|
+
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
635
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
715
636
|
);
|
|
637
|
+
expect($price.tagName).toBe('STRONG');
|
|
638
|
+
expect($price.classList.contains('h6')).toBe(true);
|
|
716
639
|
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
screen.getByTestId('product-widget__header-metadata-dates');
|
|
727
|
-
screen.getByTestId('product-widget__header-metadata-languages');
|
|
728
|
-
|
|
729
|
-
// - Target courses should not be rendered
|
|
730
|
-
await waitFor(() => {
|
|
731
|
-
order.target_courses.forEach((course) => {
|
|
732
|
-
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
733
|
-
expect($item).not.toBeInTheDocument();
|
|
734
|
-
});
|
|
640
|
+
// - Render all target courses information
|
|
641
|
+
relation.product.target_courses.forEach((course) => {
|
|
642
|
+
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
643
|
+
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
644
|
+
// but we want to it to visually look like a h5
|
|
645
|
+
const $courseTitle = getByText($item, course.title);
|
|
646
|
+
expect($courseTitle.tagName).toBe('STRONG');
|
|
647
|
+
expect($courseTitle.classList.contains('h5')).toBe(true);
|
|
648
|
+
screen.getByTestId(`CourseRunList-${course.course_runs.map(({ id }) => id).join('-')}`);
|
|
735
649
|
});
|
|
736
650
|
|
|
737
|
-
|
|
738
|
-
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
739
|
-
|
|
740
|
-
// - Does not Render PurchaseButton cta
|
|
741
|
-
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
651
|
+
screen.getByRole('button', { name: product.call_to_action });
|
|
742
652
|
});
|
|
743
653
|
|
|
744
654
|
it('renders error message when product fetching has failed', async () => {
|
|
@@ -771,7 +681,7 @@ describe('CourseProductItem', () => {
|
|
|
771
681
|
product_id: product.id,
|
|
772
682
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
773
683
|
target_courses: product.target_courses,
|
|
774
|
-
state: OrderState.
|
|
684
|
+
state: OrderState.DRAFT,
|
|
775
685
|
}).one();
|
|
776
686
|
fetchMock.get(
|
|
777
687
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -780,7 +690,7 @@ describe('CourseProductItem', () => {
|
|
|
780
690
|
const orderQueryParameters = {
|
|
781
691
|
product_id: order.product_id,
|
|
782
692
|
course_code: order.course?.code,
|
|
783
|
-
state:
|
|
693
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
784
694
|
};
|
|
785
695
|
fetchMock.get(
|
|
786
696
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
@@ -810,7 +720,7 @@ describe('CourseProductItem', () => {
|
|
|
810
720
|
product_id: product.id,
|
|
811
721
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
812
722
|
target_courses: product.target_courses,
|
|
813
|
-
state: OrderState.
|
|
723
|
+
state: OrderState.DRAFT,
|
|
814
724
|
}).one();
|
|
815
725
|
fetchMock.get(
|
|
816
726
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -819,7 +729,7 @@ describe('CourseProductItem', () => {
|
|
|
819
729
|
const orderQueryParameters = {
|
|
820
730
|
product_id: order.product_id,
|
|
821
731
|
course_code: order.course?.code,
|
|
822
|
-
state:
|
|
732
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
823
733
|
};
|
|
824
734
|
fetchMock.get(
|
|
825
735
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
@@ -850,7 +760,7 @@ describe('CourseProductItem', () => {
|
|
|
850
760
|
product_id: product.id,
|
|
851
761
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
852
762
|
target_courses: product.target_courses,
|
|
853
|
-
state: OrderState.
|
|
763
|
+
state: OrderState.DRAFT,
|
|
854
764
|
}).one();
|
|
855
765
|
fetchMock.get(
|
|
856
766
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -859,7 +769,7 @@ describe('CourseProductItem', () => {
|
|
|
859
769
|
const orderQueryParameters = {
|
|
860
770
|
product_id: order.product_id,
|
|
861
771
|
course_code: order.course?.code,
|
|
862
|
-
state:
|
|
772
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
863
773
|
};
|
|
864
774
|
fetchMock.get(
|
|
865
775
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|