richie-education 3.1.3-dev3 → 3.1.3-dev30
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/.storybook/__mocks__/utils/context.ts +4 -0
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +18 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +4 -0
- package/js/components/CourseGlimpse/utils.ts +35 -30
- 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 +13 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- 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 +171 -29
- package/js/components/SaleTunnel/index.stories.tsx +17 -3
- 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/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- 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 -17
- 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 -3
- 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 -4
- 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 +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/types/Course.ts +4 -0
- package/js/types/Joanie.ts +36 -29
- package/js/types/index.ts +6 -2
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +19 -25
- package/js/utils/test/factories/richie.ts +10 -2
- 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 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- 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 +19 -34
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +35 -8
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -8
- package/package.json +1 -1
- package/scss/objects/_course_glimpses.scss +16 -0
- 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
|
+
OfferingFactory,
|
|
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 offering = OfferingFactory().one();
|
|
78
|
+
const { product } = offering;
|
|
80
79
|
const productDeferred = new Deferred();
|
|
81
80
|
fetchMock.get(
|
|
82
81
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -93,16 +92,16 @@ 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(offering);
|
|
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 } =
|
|
100
|
+
const offering = OfferingFactory().one();
|
|
101
|
+
const { product } = offering;
|
|
103
102
|
fetchMock.get(
|
|
104
103
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
105
|
-
|
|
104
|
+
offering,
|
|
106
105
|
);
|
|
107
106
|
|
|
108
107
|
render(
|
|
@@ -132,7 +131,7 @@ describe('CourseProductItem', () => {
|
|
|
132
131
|
).not.toBeInTheDocument();
|
|
133
132
|
|
|
134
133
|
// - Render all target courses information
|
|
135
|
-
|
|
134
|
+
offering.product.target_courses.forEach((course) => {
|
|
136
135
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
137
136
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
138
137
|
// but we want to it to visually look like a h5
|
|
@@ -151,16 +150,142 @@ describe('CourseProductItem', () => {
|
|
|
151
150
|
expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
|
|
152
151
|
});
|
|
153
152
|
|
|
153
|
+
it('renders discount rate for anonymous user', async () => {
|
|
154
|
+
const offering = OfferingFactory({
|
|
155
|
+
product: CredentialProductFactory({
|
|
156
|
+
price: 840,
|
|
157
|
+
price_currency: 'EUR',
|
|
158
|
+
}).one(),
|
|
159
|
+
rules: {
|
|
160
|
+
discounted_price: 800,
|
|
161
|
+
discount_rate: 0.3,
|
|
162
|
+
description: 'Year 2023 discount',
|
|
163
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
164
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
165
|
+
},
|
|
166
|
+
}).one();
|
|
167
|
+
const { product } = offering;
|
|
168
|
+
fetchMock.get(
|
|
169
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
170
|
+
offering,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
render(
|
|
174
|
+
<CourseProductItem
|
|
175
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
176
|
+
productId={product.id}
|
|
177
|
+
/>,
|
|
178
|
+
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
182
|
+
|
|
183
|
+
// - Render discount information
|
|
184
|
+
// Original price should be displayed as a del element
|
|
185
|
+
const originalPriceLabel = screen.getByText('Original price:');
|
|
186
|
+
expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
187
|
+
const originalPrice = screen.getByText(
|
|
188
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
189
|
+
);
|
|
190
|
+
expect(originalPrice.tagName).toBe('DEL');
|
|
191
|
+
expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
|
|
192
|
+
|
|
193
|
+
// Discounted price should be displayed as an ins element
|
|
194
|
+
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
195
|
+
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
196
|
+
const discountedPrice = screen.getByText(
|
|
197
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
198
|
+
/(\u202F|\u00a0)/g,
|
|
199
|
+
' ',
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
expect(discountedPrice.tagName).toBe('INS');
|
|
203
|
+
expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
|
|
204
|
+
|
|
205
|
+
// Discount description should be displayed
|
|
206
|
+
screen.getByText('Year 2023 discount');
|
|
207
|
+
|
|
208
|
+
// Discount rate should be displayed
|
|
209
|
+
screen.getByText('-30%');
|
|
210
|
+
|
|
211
|
+
// Discount date range should be displayed
|
|
212
|
+
screen.getByText('from Jan 01, 2023');
|
|
213
|
+
screen.getByText('to Dec 31, 2023');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('renders discount amount for anonymous user', async () => {
|
|
217
|
+
const offering = OfferingFactory({
|
|
218
|
+
product: CredentialProductFactory({
|
|
219
|
+
price: 840,
|
|
220
|
+
price_currency: 'EUR',
|
|
221
|
+
}).one(),
|
|
222
|
+
rules: {
|
|
223
|
+
discounted_price: 800,
|
|
224
|
+
discount_amount: 40,
|
|
225
|
+
description: 'Year 2023 discount',
|
|
226
|
+
discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
|
|
227
|
+
discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
|
|
228
|
+
},
|
|
229
|
+
}).one();
|
|
230
|
+
const { product } = offering;
|
|
231
|
+
fetchMock.get(
|
|
232
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
233
|
+
offering,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
render(
|
|
237
|
+
<CourseProductItem
|
|
238
|
+
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
239
|
+
productId={product.id}
|
|
240
|
+
/>,
|
|
241
|
+
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
245
|
+
|
|
246
|
+
// - Render discount information
|
|
247
|
+
// Original price should be displayed as a del element
|
|
248
|
+
const originalPriceLabel = screen.getByText('Original price:');
|
|
249
|
+
expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
250
|
+
const originalPrice = screen.getByText(
|
|
251
|
+
priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
|
|
252
|
+
);
|
|
253
|
+
expect(originalPrice.tagName).toBe('DEL');
|
|
254
|
+
expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
|
|
255
|
+
|
|
256
|
+
// Discounted price should be displayed as an ins element
|
|
257
|
+
const discountedPriceLabel = screen.getByText('Discounted price:');
|
|
258
|
+
expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
|
|
259
|
+
const discountedPrice = screen.getByText(
|
|
260
|
+
priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
|
|
261
|
+
/(\u202F|\u00a0)/g,
|
|
262
|
+
' ',
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
expect(discountedPrice.tagName).toBe('INS');
|
|
266
|
+
expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
|
|
267
|
+
|
|
268
|
+
// Discount description should be displayed
|
|
269
|
+
screen.getByText('Year 2023 discount');
|
|
270
|
+
|
|
271
|
+
// Discount rate should be displayed
|
|
272
|
+
screen.getByText(priceFormatter(product.price_currency, -40).replace(/(\u202F|\u00a0)/g, ' '));
|
|
273
|
+
|
|
274
|
+
// Discount date range should be displayed
|
|
275
|
+
screen.getByText('from Jan 01, 2023');
|
|
276
|
+
screen.getByText('to Dec 31, 2023');
|
|
277
|
+
});
|
|
278
|
+
|
|
154
279
|
it('does not render <CertificateItem /> if product do not have a certificate', async () => {
|
|
155
|
-
const
|
|
280
|
+
const offering = OfferingFactory({
|
|
156
281
|
product: ProductFactory({
|
|
157
282
|
certificate_definition: undefined,
|
|
158
283
|
}).one(),
|
|
159
284
|
}).one();
|
|
160
|
-
const { product } =
|
|
285
|
+
const { product } = offering;
|
|
161
286
|
fetchMock.get(
|
|
162
287
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
163
|
-
|
|
288
|
+
offering,
|
|
164
289
|
);
|
|
165
290
|
|
|
166
291
|
render(
|
|
@@ -179,16 +304,16 @@ describe('CourseProductItem', () => {
|
|
|
179
304
|
});
|
|
180
305
|
|
|
181
306
|
it('renders product information in compact mode', async () => {
|
|
182
|
-
const
|
|
307
|
+
const offering = OfferingFactory().one();
|
|
183
308
|
fetchMock.get(
|
|
184
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
185
|
-
|
|
309
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
310
|
+
offering,
|
|
186
311
|
);
|
|
187
312
|
|
|
188
313
|
const { container } = render(
|
|
189
314
|
<CourseProductItem
|
|
190
315
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
191
|
-
productId={
|
|
316
|
+
productId={offering.product.id}
|
|
192
317
|
compact
|
|
193
318
|
/>,
|
|
194
319
|
{ queryOptions: { client: createTestQueryClient({ user: null }) } },
|
|
@@ -196,14 +321,14 @@ describe('CourseProductItem', () => {
|
|
|
196
321
|
|
|
197
322
|
// In the header, we should display the product title, the product price
|
|
198
323
|
// and product date range and languages
|
|
199
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
324
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
200
325
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
201
326
|
// but we want to it to visually look like a h6
|
|
202
327
|
|
|
203
328
|
const $price = screen.getByText(
|
|
204
329
|
// the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
|
|
205
330
|
// with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
|
|
206
|
-
priceFormatter(
|
|
331
|
+
priceFormatter(offering.product.price_currency, offering.product.price).replace(
|
|
207
332
|
/(\u202F|\u00a0)/g,
|
|
208
333
|
' ',
|
|
209
334
|
),
|
|
@@ -219,7 +344,7 @@ describe('CourseProductItem', () => {
|
|
|
219
344
|
expect($productWidgetContent).not.toBeInTheDocument();
|
|
220
345
|
|
|
221
346
|
// - Any target courses information should be displayed
|
|
222
|
-
|
|
347
|
+
offering.product.target_courses.forEach((course) => {
|
|
223
348
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
224
349
|
expect($item).not.toBeInTheDocument();
|
|
225
350
|
});
|
|
@@ -228,7 +353,7 @@ describe('CourseProductItem', () => {
|
|
|
228
353
|
expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
|
|
229
354
|
|
|
230
355
|
// - Render a login button
|
|
231
|
-
screen.getByRole('button', { name: `Login to purchase "${
|
|
356
|
+
screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
|
|
232
357
|
// - Does not render PurchaseButton cta
|
|
233
358
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
234
359
|
});
|
|
@@ -236,8 +361,8 @@ describe('CourseProductItem', () => {
|
|
|
236
361
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
237
362
|
'renders product informations for %s order',
|
|
238
363
|
async (state) => {
|
|
239
|
-
const
|
|
240
|
-
const { product } =
|
|
364
|
+
const offering = OfferingFactory().one();
|
|
365
|
+
const { product } = offering;
|
|
241
366
|
const order = CredentialOrderFactory({
|
|
242
367
|
product_id: product.id,
|
|
243
368
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -247,7 +372,7 @@ describe('CourseProductItem', () => {
|
|
|
247
372
|
|
|
248
373
|
fetchMock.get(
|
|
249
374
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
250
|
-
|
|
375
|
+
offering,
|
|
251
376
|
);
|
|
252
377
|
const orderQueryParameters = {
|
|
253
378
|
course_code: order.course.code,
|
|
@@ -261,13 +386,13 @@ describe('CourseProductItem', () => {
|
|
|
261
386
|
render(
|
|
262
387
|
<CourseProductItem
|
|
263
388
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
264
|
-
productId={
|
|
389
|
+
productId={offering.product.id}
|
|
265
390
|
/>,
|
|
266
391
|
);
|
|
267
392
|
|
|
268
393
|
// In the header, we should display the product title, the product price
|
|
269
394
|
// and product date range and languages
|
|
270
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
395
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
271
396
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
272
397
|
// but we want to it to visually look like a h6
|
|
273
398
|
|
|
@@ -297,8 +422,8 @@ describe('CourseProductItem', () => {
|
|
|
297
422
|
it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
|
|
298
423
|
'renders product informations for %s order in compact mode',
|
|
299
424
|
async (state) => {
|
|
300
|
-
const
|
|
301
|
-
const { product } =
|
|
425
|
+
const offering = OfferingFactory().one();
|
|
426
|
+
const { product } = offering;
|
|
302
427
|
const order = CredentialOrderFactory({
|
|
303
428
|
product_id: product.id,
|
|
304
429
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -308,7 +433,7 @@ describe('CourseProductItem', () => {
|
|
|
308
433
|
|
|
309
434
|
fetchMock.get(
|
|
310
435
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
311
|
-
|
|
436
|
+
offering,
|
|
312
437
|
);
|
|
313
438
|
const orderQueryParameters = {
|
|
314
439
|
course_code: order.course.code,
|
|
@@ -322,14 +447,14 @@ describe('CourseProductItem', () => {
|
|
|
322
447
|
render(
|
|
323
448
|
<CourseProductItem
|
|
324
449
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
325
|
-
productId={
|
|
450
|
+
productId={offering.product.id}
|
|
326
451
|
compact
|
|
327
452
|
/>,
|
|
328
453
|
);
|
|
329
454
|
|
|
330
455
|
// In the header, we should display the product title, the product price
|
|
331
456
|
// and product date range and languages
|
|
332
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
457
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
333
458
|
// the price shouldn't be a heading to prevent misdirection for screen reader users,
|
|
334
459
|
// but we want to it to visually look like a h6
|
|
335
460
|
|
|
@@ -339,7 +464,7 @@ describe('CourseProductItem', () => {
|
|
|
339
464
|
expect($enrolledInfo.classList.contains('h6')).toBe(true);
|
|
340
465
|
|
|
341
466
|
// - Any target courses information should be displayed
|
|
342
|
-
|
|
467
|
+
offering.product.target_courses.forEach((course) => {
|
|
343
468
|
const $item = screen.queryByTestId(`course-item-${course.code}`);
|
|
344
469
|
expect($item).not.toBeInTheDocument();
|
|
345
470
|
});
|
|
@@ -350,8 +475,8 @@ describe('CourseProductItem', () => {
|
|
|
350
475
|
);
|
|
351
476
|
|
|
352
477
|
it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
|
|
353
|
-
const
|
|
354
|
-
const { product } =
|
|
478
|
+
const offering = OfferingFactory().one();
|
|
479
|
+
const { product } = offering;
|
|
355
480
|
const order = CredentialOrderFactory({
|
|
356
481
|
product_id: product.id,
|
|
357
482
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -361,7 +486,7 @@ describe('CourseProductItem', () => {
|
|
|
361
486
|
|
|
362
487
|
fetchMock.get(
|
|
363
488
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
364
|
-
|
|
489
|
+
offering,
|
|
365
490
|
);
|
|
366
491
|
const orderQueryParameters = {
|
|
367
492
|
course_code: order.course.code,
|
|
@@ -412,17 +537,17 @@ describe('CourseProductItem', () => {
|
|
|
412
537
|
it.each(ENROLLABLE_ORDER_STATES)(
|
|
413
538
|
'renders product informations for a %s order in compact mode',
|
|
414
539
|
async (state) => {
|
|
415
|
-
const
|
|
540
|
+
const offering = OfferingFactory().one();
|
|
416
541
|
const order: CredentialOrder = CredentialOrderFactory({
|
|
417
|
-
product_id:
|
|
542
|
+
product_id: offering.product.id,
|
|
418
543
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
419
|
-
target_courses:
|
|
544
|
+
target_courses: offering.product.target_courses,
|
|
420
545
|
state,
|
|
421
546
|
}).one();
|
|
422
547
|
|
|
423
548
|
fetchMock.get(
|
|
424
|
-
`https://joanie.endpoint/api/v1.0/courses/00000/products/${
|
|
425
|
-
|
|
549
|
+
`https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
|
|
550
|
+
offering,
|
|
426
551
|
);
|
|
427
552
|
const orderQueryParameters = {
|
|
428
553
|
product_id: order.product_id,
|
|
@@ -436,14 +561,14 @@ describe('CourseProductItem', () => {
|
|
|
436
561
|
|
|
437
562
|
render(
|
|
438
563
|
<CourseProductItem
|
|
439
|
-
productId={
|
|
564
|
+
productId={offering.product.id}
|
|
440
565
|
course={PacedCourseFactory({ code: '00000' }).one()}
|
|
441
566
|
compact
|
|
442
567
|
/>,
|
|
443
568
|
);
|
|
444
569
|
|
|
445
570
|
// Wait for product information to be fetched
|
|
446
|
-
await screen.findByRole('heading', { level: 3, name:
|
|
571
|
+
await screen.findByRole('heading', { level: 3, name: offering.product.title });
|
|
447
572
|
|
|
448
573
|
// - In place of product price, a label should be displayed
|
|
449
574
|
const $enrolledInfo = await screen.findByText('Purchased');
|
|
@@ -480,8 +605,8 @@ describe('CourseProductItem', () => {
|
|
|
480
605
|
);
|
|
481
606
|
|
|
482
607
|
it('renders enrollment information when user is enrolled to a course run', async () => {
|
|
483
|
-
const
|
|
484
|
-
const { product } =
|
|
608
|
+
const offering = OfferingFactory().one();
|
|
609
|
+
const { product } = offering;
|
|
485
610
|
// - Create an order with an active enrollment
|
|
486
611
|
const enrollment: Enrollment = EnrollmentFactory({
|
|
487
612
|
course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
|
|
@@ -495,7 +620,7 @@ describe('CourseProductItem', () => {
|
|
|
495
620
|
|
|
496
621
|
fetchMock.get(
|
|
497
622
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
498
|
-
|
|
623
|
+
offering,
|
|
499
624
|
);
|
|
500
625
|
const orderQueryParameters = {
|
|
501
626
|
product_id: order.product_id,
|
|
@@ -547,8 +672,8 @@ describe('CourseProductItem', () => {
|
|
|
547
672
|
it.each(PURCHASABLE_ORDER_STATES)(
|
|
548
673
|
'renders sale tunnel button if user already has a %s order',
|
|
549
674
|
async (state) => {
|
|
550
|
-
const
|
|
551
|
-
const { product } =
|
|
675
|
+
const offering = OfferingFactory().one();
|
|
676
|
+
const { product } = offering;
|
|
552
677
|
const order = CredentialOrderFactory({
|
|
553
678
|
product_id: product.id,
|
|
554
679
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -557,7 +682,7 @@ describe('CourseProductItem', () => {
|
|
|
557
682
|
}).one();
|
|
558
683
|
fetchMock.get(
|
|
559
684
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
560
|
-
|
|
685
|
+
offering,
|
|
561
686
|
);
|
|
562
687
|
const orderQueryParameters = {
|
|
563
688
|
product_id: order.product_id,
|
|
@@ -588,7 +713,7 @@ describe('CourseProductItem', () => {
|
|
|
588
713
|
expect($price.classList.contains('h6')).toBe(true);
|
|
589
714
|
|
|
590
715
|
// - Render all target courses information
|
|
591
|
-
|
|
716
|
+
offering.product.target_courses.forEach((course) => {
|
|
592
717
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
593
718
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
594
719
|
// but we want to it to visually look like a h5
|
|
@@ -603,11 +728,11 @@ describe('CourseProductItem', () => {
|
|
|
603
728
|
);
|
|
604
729
|
|
|
605
730
|
it('renders sale tunnel button if user already has a canceled order', async () => {
|
|
606
|
-
const
|
|
607
|
-
const { product } =
|
|
731
|
+
const offering = OfferingFactory().one();
|
|
732
|
+
const { product } = offering;
|
|
608
733
|
fetchMock.get(
|
|
609
734
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
610
|
-
|
|
735
|
+
offering,
|
|
611
736
|
);
|
|
612
737
|
const orderQueryParameters = {
|
|
613
738
|
product_id: product.id,
|
|
@@ -638,7 +763,7 @@ describe('CourseProductItem', () => {
|
|
|
638
763
|
expect($price.classList.contains('h6')).toBe(true);
|
|
639
764
|
|
|
640
765
|
// - Render all target courses information
|
|
641
|
-
|
|
766
|
+
offering.product.target_courses.forEach((course) => {
|
|
642
767
|
const $item = screen.getByTestId(`course-item-${course.code}`);
|
|
643
768
|
// the course title shouldn't be a heading to prevent misdirection for screen reader users,
|
|
644
769
|
// but we want to it to visually look like a h5
|
|
@@ -652,7 +777,7 @@ describe('CourseProductItem', () => {
|
|
|
652
777
|
});
|
|
653
778
|
|
|
654
779
|
it('renders error message when product fetching has failed', async () => {
|
|
655
|
-
const { product } =
|
|
780
|
+
const { product } = OfferingFactory().one();
|
|
656
781
|
|
|
657
782
|
fetchMock.get(
|
|
658
783
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
@@ -673,10 +798,13 @@ describe('CourseProductItem', () => {
|
|
|
673
798
|
});
|
|
674
799
|
|
|
675
800
|
it('renders a warning message that tells that no seats are left', async () => {
|
|
676
|
-
const
|
|
677
|
-
|
|
801
|
+
const offering = OfferingFactory({
|
|
802
|
+
rules: {
|
|
803
|
+
nb_available_seats: 0,
|
|
804
|
+
has_seats_left: false,
|
|
805
|
+
},
|
|
678
806
|
}).one();
|
|
679
|
-
const { product } =
|
|
807
|
+
const { product } = offering;
|
|
680
808
|
const order = CredentialOrderFactory({
|
|
681
809
|
product_id: product.id,
|
|
682
810
|
course: PacedCourseFactory({ code: '00000' }).one(),
|
|
@@ -685,7 +813,7 @@ describe('CourseProductItem', () => {
|
|
|
685
813
|
}).one();
|
|
686
814
|
fetchMock.get(
|
|
687
815
|
`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
|
|
688
|
-
|
|
816
|
+
offering,
|
|
689
817
|
);
|
|
690
818
|
const orderQueryParameters = {
|
|
691
819
|
product_id: order.product_id,
|
|
@@ -710,86 +838,4 @@ describe('CourseProductItem', () => {
|
|
|
710
838
|
expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
|
|
711
839
|
screen.getByText('Sorry, no seats available for now');
|
|
712
840
|
});
|
|
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
841
|
});
|
|
@@ -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
|
+
OfferingFactory,
|
|
7
7
|
CourseRunFactory,
|
|
8
8
|
CredentialOrderFactory,
|
|
9
9
|
CredentialProductFactory,
|
|
@@ -22,7 +22,16 @@ 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
|
+
OfferingFactory({
|
|
26
|
+
product: CredentialProductFactory({
|
|
27
|
+
price: 840,
|
|
28
|
+
price_currency: 'EUR',
|
|
29
|
+
}).one(),
|
|
30
|
+
rules: {
|
|
31
|
+
discounted_price: 800,
|
|
32
|
+
discount_rate: 0.3,
|
|
33
|
+
},
|
|
34
|
+
}).one(),
|
|
26
35
|
{ overwriteRoutes: true },
|
|
27
36
|
);
|
|
28
37
|
fetchMock.get(
|