richie-education 2.28.2-dev39 → 2.28.2-dev58
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/.eslintrc.json +11 -2
- package/i18n/locales/ar-SA.json +209 -125
- package/i18n/locales/es-ES.json +210 -126
- package/i18n/locales/fa-IR.json +209 -125
- package/i18n/locales/fr-CA.json +209 -125
- package/i18n/locales/fr-FR.json +209 -125
- package/i18n/locales/ko-KR.json +209 -125
- package/i18n/locales/pt-PT.json +212 -128
- package/i18n/locales/ru-RU.json +209 -125
- package/i18n/locales/vi-VN.json +209 -125
- package/js/api/joanie.ts +14 -17
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
- 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 +202 -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/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Joanie.ts +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
- 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 +4 -6
- 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 +27 -27
- 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
|
@@ -1,40 +1,38 @@
|
|
|
1
|
-
import { act, cleanup,
|
|
1
|
+
import { act, cleanup, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
3
|
import queryString from 'query-string';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import { within } from '@testing-library/dom';
|
|
6
6
|
import { createIntl } from 'react-intl';
|
|
7
|
-
import {
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { OrderState, Product, ProductType, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
|
|
9
|
+
import {
|
|
10
|
+
RichieContextFactory as mockRichieContextFactory,
|
|
11
|
+
UserFactory,
|
|
12
|
+
PacedCourseFactory,
|
|
13
|
+
} from 'utils/test/factories/richie';
|
|
8
14
|
import {
|
|
9
15
|
AddressFactory,
|
|
10
|
-
|
|
11
|
-
CertificateOrderWithPaymentFactory,
|
|
16
|
+
CertificateOrderFactory,
|
|
12
17
|
CertificateProductFactory,
|
|
13
|
-
|
|
14
|
-
CredentialOrderWithPaymentFactory,
|
|
18
|
+
CredentialOrderFactory,
|
|
15
19
|
CredentialProductFactory,
|
|
16
20
|
CreditCardFactory,
|
|
17
21
|
EnrollmentFactory,
|
|
18
22
|
PaymentInstallmentFactory,
|
|
19
23
|
} from 'utils/test/factories/joanie';
|
|
20
|
-
import {
|
|
21
|
-
RichieContextFactory as mockRichieContextFactory,
|
|
22
|
-
PacedCourseFactory,
|
|
23
|
-
UserFactory,
|
|
24
|
-
} from 'utils/test/factories/richie';
|
|
25
24
|
import { render } from 'utils/test/render';
|
|
26
25
|
import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
27
26
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
28
|
-
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
27
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
29
28
|
import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
|
|
30
|
-
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
31
|
-
import { PAYMENT_SETTINGS } from 'settings';
|
|
32
29
|
import { User } from 'types/User';
|
|
33
30
|
import { OpenEdxApiProfile } from 'types/openEdx';
|
|
34
31
|
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
35
32
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
36
33
|
import { StringHelper } from 'utils/StringHelper';
|
|
37
34
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
35
|
+
import { Deferred } from 'utils/test/deferred';
|
|
38
36
|
|
|
39
37
|
jest.mock('utils/context', () => ({
|
|
40
38
|
__esModule: true,
|
|
@@ -55,648 +53,322 @@ jest.mock('utils/indirection/window', () => ({
|
|
|
55
53
|
}),
|
|
56
54
|
}));
|
|
57
55
|
|
|
58
|
-
jest.mock('
|
|
56
|
+
jest.mock('./SaleTunnelSavePaymentMethod', () => ({
|
|
57
|
+
__esModule: true,
|
|
58
|
+
default: () => <div data-testid="sale-tunnel-save-payment-method-step" />,
|
|
59
|
+
}));
|
|
60
|
+
jest.mock('components/ContractFrame/LearnerContractFrame', () => ({
|
|
61
|
+
__esModule: true,
|
|
62
|
+
default: () => <div data-testid="sale-tunnel-sign-step" />,
|
|
63
|
+
}));
|
|
59
64
|
|
|
60
65
|
describe.each([
|
|
61
66
|
{
|
|
62
67
|
productType: ProductType.CREDENTIAL,
|
|
63
68
|
ProductFactory: CredentialProductFactory,
|
|
64
|
-
|
|
65
|
-
OrderWithPaymentFactory: CredentialOrderWithPaymentFactory,
|
|
69
|
+
OrderFactory: CredentialOrderFactory,
|
|
66
70
|
},
|
|
67
71
|
{
|
|
68
72
|
productType: ProductType.CERTIFICATE,
|
|
69
73
|
ProductFactory: CertificateProductFactory,
|
|
70
|
-
|
|
71
|
-
OrderWithPaymentFactory: CertificateOrderWithPaymentFactory,
|
|
74
|
+
OrderFactory: CertificateOrderFactory,
|
|
72
75
|
},
|
|
73
|
-
])(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
openEdxAccount,
|
|
127
|
-
);
|
|
128
|
-
fetchMock.get(`https://auth.test/api/user/v1/preferences/${richieUser.username}`, {
|
|
129
|
-
'pref-lang': prefLang,
|
|
130
|
-
});
|
|
76
|
+
])('SaleTunnel for $productType product', ({ productType, ProductFactory, OrderFactory }) => {
|
|
77
|
+
let nbApiCalls: number;
|
|
78
|
+
|
|
79
|
+
const course = PacedCourseFactory().one();
|
|
80
|
+
const enrollment =
|
|
81
|
+
productType === ProductType.CERTIFICATE
|
|
82
|
+
? EnrollmentFactory({ course_run: { course } }).one()
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
let richieUser: User;
|
|
86
|
+
let openApiEdxProfile: OpenEdxApiProfile;
|
|
87
|
+
|
|
88
|
+
const formatPrice = (price: number, currency: string) =>
|
|
89
|
+
new Intl.NumberFormat('en', {
|
|
90
|
+
currency,
|
|
91
|
+
style: 'currency',
|
|
92
|
+
}).format(price);
|
|
93
|
+
|
|
94
|
+
const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
|
|
95
|
+
const [open, setOpen] = useState(true);
|
|
96
|
+
return (
|
|
97
|
+
<SaleTunnel
|
|
98
|
+
{...props}
|
|
99
|
+
enrollment={enrollment}
|
|
100
|
+
course={productType === ProductType.CREDENTIAL ? course : undefined}
|
|
101
|
+
isOpen={open}
|
|
102
|
+
onClose={() => setOpen(false)}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
jest.useFakeTimers();
|
|
109
|
+
jest.clearAllTimers();
|
|
110
|
+
jest.resetAllMocks();
|
|
111
|
+
|
|
112
|
+
fetchMock.restore();
|
|
113
|
+
sessionStorage.clear();
|
|
114
|
+
|
|
115
|
+
nbApiCalls = 3;
|
|
116
|
+
|
|
117
|
+
richieUser = UserFactory().one();
|
|
118
|
+
openApiEdxProfile = OpenEdxApiProfileFactory({
|
|
119
|
+
username: richieUser.username,
|
|
120
|
+
email: richieUser.email,
|
|
121
|
+
name: richieUser.full_name,
|
|
122
|
+
}).one();
|
|
123
|
+
|
|
124
|
+
const { 'pref-lang': prefLang, ...openEdxAccount } = openApiEdxProfile;
|
|
125
|
+
|
|
126
|
+
fetchMock.get(`https://auth.test/api/user/v1/accounts/${richieUser.username}`, openEdxAccount);
|
|
127
|
+
fetchMock.get(`https://auth.test/api/user/v1/preferences/${richieUser.username}`, {
|
|
128
|
+
'pref-lang': prefLang,
|
|
131
129
|
});
|
|
130
|
+
});
|
|
132
131
|
|
|
133
|
-
|
|
132
|
+
setupJoanieSession();
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
jest.useRealTimers();
|
|
140
|
-
cleanup();
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
act(() => {
|
|
136
|
+
jest.runOnlyPendingTimers();
|
|
141
137
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.get(
|
|
170
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
171
|
-
[],
|
|
172
|
-
)
|
|
173
|
-
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
174
|
-
.patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
|
|
175
|
-
paymentInfo,
|
|
176
|
-
})
|
|
177
|
-
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
|
|
178
|
-
...order,
|
|
179
|
-
})
|
|
180
|
-
.post(`https://joanie.endpoint/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK)
|
|
181
|
-
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
182
|
-
overwriteRoutes: true,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
render(<Wrapper product={product} />, {
|
|
186
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
187
|
-
});
|
|
188
|
-
nbApiCalls += 1; // useProductOrder call.
|
|
189
|
-
nbApiCalls += 1; // get user account call.
|
|
190
|
-
nbApiCalls += 1; // get user preferences call.
|
|
191
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
192
|
-
|
|
193
|
-
const $terms = screen.getByLabelText(
|
|
194
|
-
'By checking this box, you accept the General Terms of Sale',
|
|
195
|
-
);
|
|
196
|
-
const user = userEvent.setup({ delay: null });
|
|
197
|
-
await user.click($terms);
|
|
198
|
-
|
|
199
|
-
const $button = screen.getByRole<HTMLButtonElement>('button', {
|
|
200
|
-
name: `Subscribe`,
|
|
201
|
-
});
|
|
202
|
-
nbApiCalls += 1; // product payment-schedule call
|
|
203
|
-
|
|
204
|
-
// - Payment button should not be disabled.
|
|
205
|
-
expect($button.disabled).toBe(false);
|
|
206
|
-
|
|
207
|
-
// - wait for address to be loaded.
|
|
208
|
-
await screen.findByText(getAddressLabel(billingAddress));
|
|
209
|
-
|
|
210
|
-
// - Order should not have been submitted yet
|
|
211
|
-
expect(
|
|
212
|
-
fetchMock
|
|
213
|
-
.calls()
|
|
214
|
-
.filter(
|
|
215
|
-
(call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
|
|
216
|
-
),
|
|
217
|
-
).toHaveLength(0);
|
|
218
|
-
|
|
219
|
-
// - User clicks on pay button
|
|
220
|
-
await user.click($button);
|
|
221
|
-
|
|
222
|
-
// - Route to create order should have been called
|
|
223
|
-
nbApiCalls += 1; // order post create (invalidate queries)
|
|
224
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
225
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
226
|
-
nbApiCalls += 1; // order submit (invalidate queries)
|
|
227
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
228
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from submit)
|
|
229
|
-
|
|
230
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
231
|
-
expect(fetchMock.lastUrl()).toBe(
|
|
138
|
+
jest.useRealTimers();
|
|
139
|
+
cleanup();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const getFetchOrderQueryParams = (product: Product) => {
|
|
143
|
+
return product.type === ProductType.CREDENTIAL
|
|
144
|
+
? {
|
|
145
|
+
course_code: course.code,
|
|
146
|
+
product_id: product.id,
|
|
147
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
148
|
+
}
|
|
149
|
+
: {
|
|
150
|
+
enrollment_id: enrollment?.id,
|
|
151
|
+
product_id: product.id,
|
|
152
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
it('should create an order when the user clicks on subscribe button', async () => {
|
|
157
|
+
const product = ProductFactory().one();
|
|
158
|
+
const billingAddress = AddressFactory({
|
|
159
|
+
is_main: true,
|
|
160
|
+
}).one();
|
|
161
|
+
const order = OrderFactory({ state: OrderState.TO_SAVE_PAYMENT_METHOD }).one();
|
|
162
|
+
|
|
163
|
+
fetchMock
|
|
164
|
+
.get(
|
|
232
165
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// - Spinner should be displayed
|
|
245
|
-
screen.getByText('Payment in progress');
|
|
246
|
-
|
|
247
|
-
// - Payment interface should be displayed
|
|
248
|
-
screen.getByText('Payment interface component');
|
|
249
|
-
|
|
250
|
-
// - Simulate the payment aborting.
|
|
251
|
-
fetchMock.get(
|
|
252
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
253
|
-
[
|
|
254
|
-
{
|
|
255
|
-
...order,
|
|
256
|
-
state: OrderState.PENDING,
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
{ overwriteRoutes: true },
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
await user.click(screen.getByTestId('payment-abort'));
|
|
263
|
-
|
|
264
|
-
nbApiCalls += 1; // abort order.
|
|
265
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
266
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
267
|
-
|
|
268
|
-
await waitFor(() => {
|
|
269
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
166
|
+
[],
|
|
167
|
+
)
|
|
168
|
+
.get(
|
|
169
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
170
|
+
[],
|
|
171
|
+
)
|
|
172
|
+
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
173
|
+
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, order)
|
|
174
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
175
|
+
overwriteRoutes: true,
|
|
270
176
|
});
|
|
271
|
-
expect(fetchMock.calls()[fetchMock.calls().length - 3][0]).toBe(
|
|
272
|
-
`https://joanie.endpoint/api/v1.0/orders/${order.id}/abort/`,
|
|
273
|
-
);
|
|
274
|
-
expect(fetchMock.calls()[fetchMock.calls().length - 1][0]).toBe(
|
|
275
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
screen.getByText('You have aborted the payment.');
|
|
279
|
-
|
|
280
|
-
// - User clicks on pay button again.
|
|
281
|
-
await user.click($button);
|
|
282
|
-
|
|
283
|
-
// - Spinner should be displayed
|
|
284
|
-
await screen.findByText('Payment in progress');
|
|
285
|
-
|
|
286
|
-
// - Payment interface should be displayed
|
|
287
|
-
await screen.findByText('Payment interface component');
|
|
288
177
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
178
|
+
render(<Wrapper product={product} />, {
|
|
179
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
180
|
+
});
|
|
181
|
+
nbApiCalls += 1; // useProductOrder call.
|
|
182
|
+
nbApiCalls += 1; // get user account call.
|
|
183
|
+
nbApiCalls += 1; // get user preferences call.
|
|
184
|
+
nbApiCalls += 1; // product payment-schedule call
|
|
185
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
293
186
|
|
|
294
|
-
|
|
187
|
+
const user = userEvent.setup({ delay: null });
|
|
295
188
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
fetchMock
|
|
299
|
-
.calls()
|
|
300
|
-
.filter(
|
|
301
|
-
(call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
|
|
302
|
-
),
|
|
303
|
-
).toHaveLength(2);
|
|
304
|
-
expect(fetchMock.lastUrl()).toBe(
|
|
305
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
306
|
-
);
|
|
189
|
+
const $button = screen.getByRole<HTMLButtonElement>('button', {
|
|
190
|
+
name: `Subscribe`,
|
|
307
191
|
});
|
|
308
192
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const billingAddress = AddressFactory({
|
|
312
|
-
is_main: true,
|
|
313
|
-
}).one();
|
|
314
|
-
const creditCard = CreditCardFactory().one();
|
|
315
|
-
const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
|
|
193
|
+
// - wait for address to be loaded.
|
|
194
|
+
await screen.findByText(getAddressLabel(billingAddress));
|
|
316
195
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
state: OrderState.PENDING,
|
|
320
|
-
};
|
|
321
|
-
const orderSubmitted = {
|
|
322
|
-
...order,
|
|
323
|
-
state: OrderState.SUBMITTED,
|
|
324
|
-
};
|
|
196
|
+
// - Subscribe button should not be disabled.
|
|
197
|
+
expect($button.disabled).toBe(false);
|
|
325
198
|
|
|
199
|
+
// - Order should not have been created yet
|
|
200
|
+
expect(
|
|
326
201
|
fetchMock
|
|
327
|
-
.get(
|
|
328
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
329
|
-
[initialOrder],
|
|
330
|
-
)
|
|
331
|
-
.get(
|
|
332
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
333
|
-
[],
|
|
334
|
-
)
|
|
335
|
-
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
336
|
-
.patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
|
|
337
|
-
payment_info: paymentInfo,
|
|
338
|
-
})
|
|
339
|
-
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, orderSubmitted)
|
|
340
|
-
.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
|
|
341
|
-
overwriteRoutes: true,
|
|
342
|
-
})
|
|
343
|
-
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
344
|
-
overwriteRoutes: true,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
render(<Wrapper product={product} />, {
|
|
348
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
349
|
-
});
|
|
350
|
-
await waitFor(() => {
|
|
351
|
-
expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const user = userEvent.setup({ delay: null });
|
|
355
|
-
const $terms = screen.getByLabelText(
|
|
356
|
-
'By checking this box, you accept the General Terms of Sale',
|
|
357
|
-
);
|
|
358
|
-
await user.click($terms);
|
|
359
|
-
|
|
360
|
-
nbApiCalls += 1; // useProductOrder get order with filters
|
|
361
|
-
nbApiCalls += 1; // get user account call.
|
|
362
|
-
nbApiCalls += 1; // get user preferences call.
|
|
363
|
-
nbApiCalls += 1; // product payment schedule call.
|
|
364
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
365
|
-
const $button = screen.getByRole('button', {
|
|
366
|
-
name: `Subscribe`,
|
|
367
|
-
}) as HTMLButtonElement;
|
|
368
|
-
|
|
369
|
-
// - Payment button should not be disabled.
|
|
370
|
-
expect($button.disabled).toBe(false);
|
|
371
|
-
|
|
372
|
-
// - wait for address to be loaded.
|
|
373
|
-
await screen.findByText(getAddressLabel(billingAddress));
|
|
374
|
-
|
|
375
|
-
// - User clicks on Subscribe button
|
|
376
|
-
fetchMock.get(
|
|
377
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
378
|
-
[orderSubmitted],
|
|
379
|
-
{ overwriteRoutes: true },
|
|
380
|
-
);
|
|
381
|
-
await user.click($button);
|
|
382
|
-
|
|
383
|
-
// - In real world condition the success callback is immediately called for one click payments.
|
|
384
|
-
// - but here we need to click manually.
|
|
385
|
-
const $success = await screen.findByTestId('payment-success');
|
|
386
|
-
await user.click($success);
|
|
387
|
-
|
|
388
|
-
// - Route to submit an existing order
|
|
389
|
-
// - Furthermore, as payment succeeded immediately, order should have been refetched
|
|
390
|
-
nbApiCalls += 1; // order submit
|
|
391
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
392
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from submit)
|
|
393
|
-
nbApiCalls += 1; // order get on id
|
|
394
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
395
|
-
|
|
396
|
-
const submitCall = fetchMock
|
|
397
202
|
.calls()
|
|
398
|
-
.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// - Order should have been refetched
|
|
427
|
-
nbApiCalls += 1; // order get on id
|
|
428
|
-
nbApiCalls += 1; // orders get (invalidate queries)
|
|
429
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from success)
|
|
430
|
-
nbApiCalls += 1; // orders get (invalidate queries)
|
|
431
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
432
|
-
|
|
433
|
-
// - As order state is validated, success step is displayed
|
|
434
|
-
screen.getByTestId('generic-sale-tunnel-success-step');
|
|
435
|
-
screen.getByText('Congratulations!');
|
|
436
|
-
// - And poller should be stopped
|
|
437
|
-
await act(async () => {
|
|
438
|
-
jest.runOnlyPendingTimers();
|
|
439
|
-
});
|
|
440
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('should render SaleTunnelNotValidated if order is not validated after a given delay', async () => {
|
|
444
|
-
const product = ProductFactory().one();
|
|
445
|
-
const billingAddress = AddressFactory({
|
|
446
|
-
is_main: true,
|
|
447
|
-
}).one();
|
|
448
|
-
const creditCard = CreditCardFactory().one();
|
|
449
|
-
const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
|
|
450
|
-
const orderSubmitted = {
|
|
451
|
-
...order,
|
|
452
|
-
state: OrderState.SUBMITTED,
|
|
453
|
-
};
|
|
454
|
-
|
|
203
|
+
.filter(
|
|
204
|
+
([url, metadata]) =>
|
|
205
|
+
url === 'https://joanie.endpoint/api/v1.0/orders/' && metadata?.method === 'POST',
|
|
206
|
+
),
|
|
207
|
+
).toHaveLength(0);
|
|
208
|
+
|
|
209
|
+
// - User clicks on pay button
|
|
210
|
+
await user.click($button);
|
|
211
|
+
|
|
212
|
+
fetchMock.get(
|
|
213
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
214
|
+
[order],
|
|
215
|
+
{ overwriteRoutes: true },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// - Route to create order should have been called
|
|
219
|
+
nbApiCalls += 1; // order create
|
|
220
|
+
nbApiCalls += 1; // order get (invalidate queries)
|
|
221
|
+
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
222
|
+
|
|
223
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
224
|
+
expect(fetchMock.lastUrl()).toBe(
|
|
225
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// - Order should have been created once
|
|
229
|
+
expect(
|
|
455
230
|
fetchMock
|
|
456
|
-
.
|
|
457
|
-
|
|
458
|
-
[
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
});
|
|
480
|
-
await waitFor(() => {
|
|
481
|
-
expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
|
|
482
|
-
});
|
|
483
|
-
nbApiCalls += 1; // fetcher order for userProductOrder
|
|
484
|
-
nbApiCalls += 1; // get user account call.
|
|
485
|
-
nbApiCalls += 1; // get user preferences call.
|
|
486
|
-
nbApiCalls += 1; // get product payment schedule.
|
|
487
|
-
const apiCalls = fetchMock.calls().map((call) => call[0]);
|
|
488
|
-
expect(apiCalls).toContain(
|
|
489
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
const $terms = screen.getByLabelText(
|
|
493
|
-
'By checking this box, you accept the General Terms of Sale',
|
|
494
|
-
);
|
|
495
|
-
const user = userEvent.setup({ delay: null });
|
|
496
|
-
await user.click($terms);
|
|
497
|
-
|
|
498
|
-
const $button = screen.getByRole('button', {
|
|
499
|
-
name: `Subscribe`,
|
|
500
|
-
}) as HTMLButtonElement;
|
|
501
|
-
|
|
502
|
-
// - wait for address to be loaded.
|
|
503
|
-
await screen.findByText(getAddressLabel(billingAddress));
|
|
504
|
-
|
|
505
|
-
// - Payment button should not be disabled.
|
|
506
|
-
expect($button.disabled).toBe(false);
|
|
507
|
-
|
|
508
|
-
// - User clicks on pay button
|
|
509
|
-
await user.click($button);
|
|
510
|
-
|
|
511
|
-
// - In real world condition the success callback is immediately called for one click payments.
|
|
512
|
-
// - but here we need to click manually.
|
|
513
|
-
const $success = await screen.findByTestId('payment-success');
|
|
514
|
-
await user.click($success);
|
|
515
|
-
|
|
516
|
-
// - Route to create order should have been called
|
|
517
|
-
// - Furthermore, as payment succeeded immediately, order should have been refetched
|
|
518
|
-
const onClickApiCalls = fetchMock.calls().splice(nbApiCalls);
|
|
519
|
-
nbApiCalls += 1; // order submit
|
|
520
|
-
nbApiCalls += 1; // orders get (invalidate queries)
|
|
521
|
-
nbApiCalls += 1; // refetch order (submit invalidate)
|
|
522
|
-
nbApiCalls += 1; // fetch validated order
|
|
523
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
524
|
-
|
|
525
|
-
expect(onClickApiCalls[0][0]).toBe(
|
|
526
|
-
`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
|
|
527
|
-
);
|
|
528
|
-
expect(JSON.parse(onClickApiCalls[0][1]!.body as string)).toEqual({
|
|
529
|
-
billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
|
|
530
|
-
credit_card_id: creditCard.id,
|
|
531
|
-
});
|
|
532
|
-
expect(onClickApiCalls[2][0]).toBe(
|
|
231
|
+
.calls()
|
|
232
|
+
.filter(
|
|
233
|
+
([url, metadata]) =>
|
|
234
|
+
url === 'https://joanie.endpoint/api/v1.0/orders/' && metadata?.method === 'POST',
|
|
235
|
+
),
|
|
236
|
+
).toHaveLength(1);
|
|
237
|
+
|
|
238
|
+
// - Spinner should be displayed
|
|
239
|
+
screen.getByText('Order creation in progress');
|
|
240
|
+
|
|
241
|
+
// - Save payment step should be displayed
|
|
242
|
+
await screen.findByTestId('sale-tunnel-save-payment-method-step');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should render an error message when order creation fails', async () => {
|
|
246
|
+
const product = ProductFactory().one();
|
|
247
|
+
const billingAddress = AddressFactory({
|
|
248
|
+
is_main: true,
|
|
249
|
+
}).one();
|
|
250
|
+
const deferred = new Deferred();
|
|
251
|
+
|
|
252
|
+
fetchMock
|
|
253
|
+
.get(
|
|
533
254
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
await act(async () => {
|
|
544
|
-
await jest.advanceTimersByTimeAsync(
|
|
545
|
-
(PAYMENT_SETTINGS.pollLimit - 1) * PAYMENT_SETTINGS.pollInterval,
|
|
546
|
-
);
|
|
255
|
+
[],
|
|
256
|
+
)
|
|
257
|
+
.get(
|
|
258
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
259
|
+
[],
|
|
260
|
+
)
|
|
261
|
+
.post('https://joanie.endpoint/api/v1.0/orders/', deferred.promise)
|
|
262
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
263
|
+
overwriteRoutes: true,
|
|
547
264
|
});
|
|
548
265
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
266
|
+
render(<Wrapper product={product} />, {
|
|
267
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
268
|
+
});
|
|
269
|
+
nbApiCalls += 1; // useProductOrder get order with filters
|
|
270
|
+
nbApiCalls += 1; // get user account call.
|
|
271
|
+
nbApiCalls += 1; // get user preferences call.
|
|
272
|
+
nbApiCalls += 1; // get product payment schedule.
|
|
273
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
553
274
|
|
|
554
|
-
|
|
555
|
-
await act(async () => {
|
|
556
|
-
jest.runOnlyPendingTimers();
|
|
557
|
-
nbApiCalls += 1; // last poll round
|
|
558
|
-
});
|
|
275
|
+
const user = userEvent.setup({ delay: null });
|
|
559
276
|
|
|
560
|
-
|
|
561
|
-
|
|
277
|
+
const $button = screen.getByRole('button', {
|
|
278
|
+
name: `Subscribe`,
|
|
279
|
+
}) as HTMLButtonElement;
|
|
562
280
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
screen.getByText("Sorry, you'll have to wait a little longer!");
|
|
281
|
+
// - wait for address to be loaded.
|
|
282
|
+
await screen.findByText(getAddressLabel(billingAddress));
|
|
566
283
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const $link = screen.getByRole('link', {
|
|
570
|
-
name: 'Close',
|
|
571
|
-
});
|
|
572
|
-
expect($link.getAttribute('href')).toBe(`/en/dashboard/courses/orders/${order.id}`);
|
|
573
|
-
} else {
|
|
574
|
-
// Otherwise, the button should close the modal
|
|
575
|
-
screen.getByRole('button', {
|
|
576
|
-
name: 'Close',
|
|
577
|
-
});
|
|
578
|
-
}
|
|
284
|
+
// - As all information are provided, subscribe button should not be disabled.
|
|
285
|
+
expect($button.disabled).toBe(false);
|
|
579
286
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
jest.runOnlyPendingTimers();
|
|
583
|
-
});
|
|
584
|
-
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
585
|
-
}, 10000);
|
|
287
|
+
// - User clicks on subscribe button
|
|
288
|
+
await user.click($button);
|
|
586
289
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const billingAddress = AddressFactory({
|
|
590
|
-
is_main: true,
|
|
591
|
-
}).one();
|
|
592
|
-
const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
|
|
290
|
+
// - Route to create order should have been called
|
|
291
|
+
nbApiCalls += 1; // order post create (no query invalidation)
|
|
593
292
|
|
|
594
|
-
|
|
595
|
-
.get(
|
|
596
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
597
|
-
[],
|
|
598
|
-
)
|
|
599
|
-
.get(
|
|
600
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
601
|
-
[],
|
|
602
|
-
)
|
|
603
|
-
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
604
|
-
.patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
|
|
605
|
-
payment_info: paymentInfo,
|
|
606
|
-
})
|
|
607
|
-
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
|
|
608
|
-
...order,
|
|
609
|
-
state: OrderState.SUBMITTED,
|
|
610
|
-
})
|
|
611
|
-
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
612
|
-
overwriteRoutes: true,
|
|
613
|
-
});
|
|
293
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
614
294
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
nbApiCalls += 1; // useProductOrder get order with filters
|
|
619
|
-
nbApiCalls += 1; // get user account call.
|
|
620
|
-
nbApiCalls += 1; // get user preferences call.
|
|
621
|
-
nbApiCalls += 1; // get product payment schedule.
|
|
622
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
295
|
+
// - Spinner should be displayed and subscribe button should be disabled
|
|
296
|
+
screen.getByText('Order creation in progress');
|
|
297
|
+
expect($button.disabled).toBe(true);
|
|
623
298
|
|
|
624
|
-
|
|
625
|
-
|
|
299
|
+
// - Simulate the order creation has failed
|
|
300
|
+
await act(async () => {
|
|
301
|
+
deferred.reject(
|
|
302
|
+
new HttpError(HttpStatusCode.BAD_REQUEST, 'An error occurred during order creation.'),
|
|
626
303
|
);
|
|
627
|
-
|
|
628
|
-
await user.click($terms);
|
|
629
|
-
|
|
630
|
-
const $button = screen.getByRole('button', {
|
|
631
|
-
name: `Subscribe`,
|
|
632
|
-
}) as HTMLButtonElement;
|
|
633
|
-
|
|
634
|
-
// - wait for address to be loaded.
|
|
635
|
-
await screen.findByText(getAddressLabel(billingAddress));
|
|
636
|
-
|
|
637
|
-
// - As all information are provided, payment button should not be disabled.
|
|
638
|
-
expect($button.disabled).toBe(false);
|
|
639
|
-
|
|
640
|
-
// - User clicks on pay button
|
|
641
|
-
await user.click($button);
|
|
642
|
-
|
|
643
|
-
// - Route to create order should have been called
|
|
644
|
-
nbApiCalls += 1; // order post create (invalidate queries)
|
|
645
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
646
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
647
|
-
nbApiCalls += 1; // order submit (invalidate queries)
|
|
648
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
649
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from submit)
|
|
650
|
-
|
|
651
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
652
|
-
|
|
653
|
-
// - Spinner should be displayed and payment button should be disabled
|
|
654
|
-
screen.getByText('Payment in progress');
|
|
655
|
-
expect($button.disabled).toBe(true);
|
|
304
|
+
});
|
|
656
305
|
|
|
657
|
-
|
|
658
|
-
|
|
306
|
+
// - An error message should be displayed
|
|
307
|
+
const $error = screen.getByText('An error occurred during order creation. Please retry later.');
|
|
308
|
+
expect(document.activeElement).toBe($error);
|
|
659
309
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
310
|
+
// - Payment button should have been restore to its idle state
|
|
311
|
+
expect($button.disabled).toBe(false);
|
|
312
|
+
screen.getByRole('button', {
|
|
313
|
+
name: 'Subscribe',
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should start at the save payment method step if order is in state to_save_payment_method', async () => {
|
|
318
|
+
const product = ProductFactory().one();
|
|
319
|
+
const billingAddress = AddressFactory({
|
|
320
|
+
is_main: true,
|
|
321
|
+
}).one();
|
|
322
|
+
const creditCard = CreditCardFactory().one();
|
|
323
|
+
const order = OrderFactory({ state: OrderState.TO_SAVE_PAYMENT_METHOD }).one();
|
|
324
|
+
|
|
325
|
+
fetchMock
|
|
326
|
+
.get(
|
|
327
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
328
|
+
[order],
|
|
329
|
+
)
|
|
330
|
+
.get(
|
|
331
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
332
|
+
[],
|
|
333
|
+
)
|
|
334
|
+
.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
|
|
335
|
+
overwriteRoutes: true,
|
|
336
|
+
})
|
|
337
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
338
|
+
overwriteRoutes: true,
|
|
663
339
|
});
|
|
664
340
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
expect(document.activeElement).toBe($error);
|
|
668
|
-
// - Payment interface should have been closed
|
|
669
|
-
expect(screen.queryByText('Payment interface component')).toBeNull();
|
|
670
|
-
// - Payment button should have been restore to its idle state
|
|
671
|
-
expect($button.disabled).toBe(false);
|
|
672
|
-
screen.getByRole('button', {
|
|
673
|
-
name: 'Subscribe',
|
|
674
|
-
});
|
|
341
|
+
render(<Wrapper product={product} />, {
|
|
342
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
675
343
|
});
|
|
676
344
|
|
|
677
|
-
|
|
345
|
+
nbApiCalls += 3;
|
|
346
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
347
|
+
|
|
348
|
+
await screen.findByTestId('sale-tunnel-save-payment-method-step');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it.each([OrderState.TO_SIGN, OrderState.SIGNING])(
|
|
352
|
+
'should start at the sign step if order is in state %s',
|
|
353
|
+
async (state) => {
|
|
678
354
|
const product = ProductFactory().one();
|
|
679
355
|
const billingAddress = AddressFactory({
|
|
680
356
|
is_main: true,
|
|
681
357
|
}).one();
|
|
682
|
-
const
|
|
358
|
+
const creditCard = CreditCardFactory().one();
|
|
359
|
+
const order = OrderFactory({ state }).one();
|
|
683
360
|
|
|
684
361
|
fetchMock
|
|
685
362
|
.get(
|
|
686
363
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
687
|
-
[],
|
|
364
|
+
[order],
|
|
688
365
|
)
|
|
689
366
|
.get(
|
|
690
367
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
691
368
|
[],
|
|
692
369
|
)
|
|
693
|
-
.
|
|
694
|
-
|
|
695
|
-
payment_info: paymentInfo,
|
|
696
|
-
})
|
|
697
|
-
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
|
|
698
|
-
...order,
|
|
699
|
-
state: OrderState.SUBMITTED,
|
|
370
|
+
.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
|
|
371
|
+
overwriteRoutes: true,
|
|
700
372
|
})
|
|
701
373
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
702
374
|
overwriteRoutes: true,
|
|
@@ -705,202 +377,81 @@ describe.each([
|
|
|
705
377
|
render(<Wrapper product={product} />, {
|
|
706
378
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
707
379
|
});
|
|
708
|
-
nbApiCalls += 1; // useProductOrder get order with filters
|
|
709
|
-
nbApiCalls += 1; // get user account call.
|
|
710
|
-
nbApiCalls += 1; // get user preferences call.
|
|
711
|
-
nbApiCalls += 1; // product payment schedule call.
|
|
712
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
713
|
-
|
|
714
|
-
const $terms = screen.getByLabelText(
|
|
715
|
-
'By checking this box, you accept the General Terms of Sale',
|
|
716
|
-
);
|
|
717
|
-
const user = userEvent.setup({ delay: null });
|
|
718
|
-
await user.click($terms);
|
|
719
|
-
|
|
720
|
-
const $button = screen.getByRole('button', {
|
|
721
|
-
name: `Subscribe`,
|
|
722
|
-
}) as HTMLButtonElement;
|
|
723
|
-
|
|
724
|
-
// - wait for address to be loaded.
|
|
725
|
-
await screen.findByText(getAddressLabel(billingAddress));
|
|
726
380
|
|
|
727
|
-
|
|
728
|
-
await
|
|
381
|
+
nbApiCalls += 3;
|
|
382
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
729
383
|
|
|
730
|
-
|
|
731
|
-
|
|
384
|
+
await screen.findByTestId('sale-tunnel-sign-step');
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
it('should show the product payment schedule', async () => {
|
|
389
|
+
const intl = createIntl({ locale: 'en' });
|
|
390
|
+
const product = ProductFactory().one();
|
|
391
|
+
const schedule = PaymentInstallmentFactory().many(2);
|
|
392
|
+
fetchMock
|
|
393
|
+
.get(
|
|
732
394
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
733
|
-
[
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
],
|
|
739
|
-
{ overwriteRoutes: true },
|
|
395
|
+
[],
|
|
396
|
+
)
|
|
397
|
+
.get(
|
|
398
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
399
|
+
schedule,
|
|
740
400
|
);
|
|
741
401
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
745
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
746
|
-
nbApiCalls += 1; // order submit (invalidate queries)
|
|
747
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
748
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from submit)
|
|
749
|
-
|
|
750
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
751
|
-
|
|
752
|
-
// - Payment interface should be displayed
|
|
753
|
-
await screen.findByText('Payment interface component');
|
|
754
|
-
|
|
755
|
-
// - Simulate the payment has failed
|
|
756
|
-
await user.click(screen.getByTestId('payment-failure'));
|
|
757
|
-
|
|
758
|
-
// - An error message should be displayed
|
|
759
|
-
const $error = screen.getByText('An error occurred during payment. Please retry later.');
|
|
760
|
-
expect(document.activeElement).toBe($error);
|
|
761
|
-
|
|
762
|
-
// - Payment button should have been restore to its idle state
|
|
763
|
-
expect($button.disabled).toBe(false);
|
|
764
|
-
screen.getByRole('button', {
|
|
765
|
-
name: `Subscribe`,
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// - User clicks on pay button again
|
|
769
|
-
await user.click($button);
|
|
770
|
-
|
|
771
|
-
nbApiCalls += 1; // order submit (invalidate queries)
|
|
772
|
-
nbApiCalls += 1; // order get (invalidate queries)
|
|
773
|
-
nbApiCalls += 1; // useProductOrder call (invalidate from submit)
|
|
774
|
-
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
402
|
+
render(<Wrapper product={product} />, {
|
|
403
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
775
404
|
});
|
|
776
405
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
fetchMock
|
|
782
|
-
.get(
|
|
783
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
784
|
-
[],
|
|
785
|
-
)
|
|
786
|
-
.get(
|
|
787
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
788
|
-
[],
|
|
789
|
-
)
|
|
790
|
-
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
791
|
-
overwriteRoutes: true,
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
render(<Wrapper product={product} />, {
|
|
795
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
const $button = screen.getByRole('button', {
|
|
799
|
-
name: 'Subscribe',
|
|
800
|
-
}) as HTMLButtonElement;
|
|
801
|
-
|
|
802
|
-
// - As all information are provided, payment button should not be disabled.
|
|
803
|
-
expect($button.disabled).toBe(false);
|
|
804
|
-
|
|
805
|
-
expect(screen.queryByText('You must accept the terms')).not.toBeInTheDocument();
|
|
806
|
-
|
|
807
|
-
// - User clicks on pay button
|
|
808
|
-
await act(async () => {
|
|
809
|
-
fireEvent.click($button);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
expect(screen.getByText('You must accept the terms.')).toBeInTheDocument();
|
|
406
|
+
await screen.findByRole('heading', {
|
|
407
|
+
level: 4,
|
|
408
|
+
name: 'Payment schedule',
|
|
813
409
|
});
|
|
814
410
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
fetchMock
|
|
819
|
-
.get(
|
|
820
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
821
|
-
[],
|
|
822
|
-
)
|
|
823
|
-
.get(
|
|
824
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
825
|
-
[],
|
|
826
|
-
);
|
|
411
|
+
const scheduleTable = screen.getByRole('table');
|
|
412
|
+
const scheduleTableRows = within(scheduleTable).getAllByRole('row');
|
|
413
|
+
expect(scheduleTableRows).toHaveLength(schedule.length);
|
|
827
414
|
|
|
828
|
-
|
|
829
|
-
|
|
415
|
+
scheduleTableRows.forEach((row, index) => {
|
|
416
|
+
const installment = schedule[index];
|
|
417
|
+
// A first column should show the installment index
|
|
418
|
+
within(row).getByRole('cell', {
|
|
419
|
+
name: (index + 1).toString(),
|
|
830
420
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
it('should show the product payment schedule', async () => {
|
|
837
|
-
const intl = createIntl({ locale: 'en' });
|
|
838
|
-
const product = ProductFactory().one();
|
|
839
|
-
const schedule = PaymentInstallmentFactory().many(2);
|
|
840
|
-
fetchMock
|
|
841
|
-
.get(
|
|
842
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
843
|
-
[],
|
|
844
|
-
)
|
|
845
|
-
.get(
|
|
846
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
847
|
-
schedule,
|
|
848
|
-
);
|
|
849
|
-
|
|
850
|
-
render(<Wrapper product={product} />, {
|
|
851
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
421
|
+
// A 2nd column should show the installment amount
|
|
422
|
+
within(row).getByRole('cell', {
|
|
423
|
+
name: formatPrice(installment.amount, installment.currency),
|
|
852
424
|
});
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
425
|
+
// A 3rd column should show the installment withdraw date
|
|
426
|
+
within(row).getByRole('cell', {
|
|
427
|
+
name: `Withdrawn on ${intl.formatDate(installment.due_date, {
|
|
428
|
+
...DEFAULT_DATE_FORMAT,
|
|
429
|
+
})}`,
|
|
857
430
|
});
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
expect(scheduleTableRows).toHaveLength(schedule.length);
|
|
862
|
-
|
|
863
|
-
scheduleTableRows.forEach((row, index) => {
|
|
864
|
-
const installment = schedule[index];
|
|
865
|
-
// A first column should show the installment index
|
|
866
|
-
within(row).getByRole('cell', {
|
|
867
|
-
name: (index + 1).toString(),
|
|
868
|
-
});
|
|
869
|
-
// A 2nd column should show the installment amount
|
|
870
|
-
within(row).getByRole('cell', {
|
|
871
|
-
name: formatPrice(installment.amount, installment.currency),
|
|
872
|
-
});
|
|
873
|
-
// A 3rd column should show the installment withdraw date
|
|
874
|
-
within(row).getByRole('cell', {
|
|
875
|
-
name: `Withdrawn on ${intl.formatDate(installment.due_date, {
|
|
876
|
-
...DEFAULT_DATE_FORMAT,
|
|
877
|
-
})}`,
|
|
878
|
-
});
|
|
879
|
-
// A 4th column should show the installment state
|
|
880
|
-
within(row).getByRole('cell', {
|
|
881
|
-
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
882
|
-
});
|
|
431
|
+
// A 4th column should show the installment state
|
|
432
|
+
within(row).getByRole('cell', {
|
|
433
|
+
name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
|
|
883
434
|
});
|
|
884
435
|
});
|
|
436
|
+
});
|
|
885
437
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
render(<Wrapper product={product} />, {
|
|
900
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
901
|
-
});
|
|
438
|
+
it('should show a walkthrough to explain the subscription process', async () => {
|
|
439
|
+
const product = ProductFactory().one();
|
|
440
|
+
const schedule = PaymentInstallmentFactory().many(2);
|
|
441
|
+
fetchMock
|
|
442
|
+
.get(
|
|
443
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
444
|
+
[],
|
|
445
|
+
)
|
|
446
|
+
.get(
|
|
447
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
|
|
448
|
+
schedule,
|
|
449
|
+
);
|
|
902
450
|
|
|
903
|
-
|
|
451
|
+
render(<Wrapper product={product} />, {
|
|
452
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
904
453
|
});
|
|
905
|
-
|
|
906
|
-
);
|
|
454
|
+
|
|
455
|
+
screen.getByTestId('walkthrough-banner');
|
|
456
|
+
});
|
|
457
|
+
});
|