Compare commits
5 Commits
sundas/INF
...
2u-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd02f8b3f4 | ||
|
|
5ba6adcd7d | ||
|
|
527bc5aa37 | ||
|
|
fd5122bc10 | ||
|
|
97c9a09efa |
52
package-lock.json
generated
52
package-lock.json
generated
@@ -2957,9 +2957,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ts-transformer/node_modules/@types/node": {
|
||||
"version": "22.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.3.tgz",
|
||||
"integrity": "sha512-gTVM8js2twdtqM+AE2PdGEe9zGQY4UvmFjan9rZcVb6FGdStfjWoWejdmy4CfWVO9rh5MiYQGZloKAGkJt8lMw==",
|
||||
"version": "22.18.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.4.tgz",
|
||||
"integrity": "sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -5436,9 +5436,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/json-pack": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz",
|
||||
"integrity": "sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.12.0.tgz",
|
||||
"integrity": "sha512-qPwWjCmcwtDiY9MZ4hz3YY4UYkVleWq5rXt+EEx4Jtl22byaLlfBEn8vXze43P2/30DkwzQvdA56EB3TH7t3Pg==",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/base64": "^1.1.2",
|
||||
"@jsonjoy.com/buffers": "^1.0.0",
|
||||
@@ -8200,12 +8200,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz",
|
||||
"integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==",
|
||||
"version": "24.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz",
|
||||
"integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.11.0"
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
@@ -10162,9 +10162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz",
|
||||
"integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==",
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -10181,7 +10181,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.2",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
"electron-to-chromium": "^1.5.218",
|
||||
"node-releases": "^2.0.21",
|
||||
@@ -10346,9 +10346,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001741",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
||||
"integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
|
||||
"version": "1.0.30001743",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
|
||||
"integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -12563,9 +12563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
@@ -25175,9 +25175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz",
|
||||
"integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==",
|
||||
"version": "29.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.2.tgz",
|
||||
"integrity": "sha512-pBNOkn4HtuLpNrXTMVRC9b642CBaDnKqWXny4OzuoULT9S7Kf8MMlaRe2veKax12rjf5WcpMBhVPbQurlWGNxA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bs-logger": "^0.2.6",
|
||||
@@ -25547,9 +25547,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz",
|
||||
"integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==",
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
|
||||
@@ -34,13 +34,13 @@ import './index.scss';
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => {
|
||||
const recaptchaSiteKeyWeb = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
return (
|
||||
<GoogleReCaptchaProvider
|
||||
reCaptchaKey={recaptchaSiteKeyWeb}
|
||||
useEnterprise
|
||||
>
|
||||
<GoogleReCaptchaProvider
|
||||
reCaptchaKey={recaptchaKey}
|
||||
useEnterprise
|
||||
>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
FORM_SUBMISSION_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import useRecaptchaSubmission from './data/hooks';
|
||||
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import getBackendValidations from './data/selectors';
|
||||
@@ -61,8 +61,8 @@ import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracki
|
||||
const RegistrationPage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
|
||||
const { executeWithFallback } = useRecaptchaSubmission('submit_registration_form');
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const flags = {
|
||||
@@ -108,8 +108,6 @@ const RegistrationPage = (props) => {
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
// temporary error state for embedded experience because we don't want to show errors on blur
|
||||
const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors });
|
||||
const [captchaError, setCaptchaError] = useState('');
|
||||
const intl = useIntl();
|
||||
|
||||
const { cta, host } = queryParams;
|
||||
const buttonLabel = cta
|
||||
@@ -254,7 +252,7 @@ const RegistrationPage = (props) => {
|
||||
fieldDescriptions,
|
||||
formatMessage,
|
||||
);
|
||||
setErrors({ ...fieldErrors });
|
||||
setErrors({ ...fieldErrors, captchaError: '' });
|
||||
dispatch(setEmailSuggestionInStore(emailSuggestion));
|
||||
|
||||
// returning if not valid
|
||||
@@ -263,30 +261,29 @@ const RegistrationPage = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (executeRecaptcha) {
|
||||
const recaptchaToken = await executeRecaptcha('submit_post');
|
||||
if (!recaptchaToken) {
|
||||
setCaptchaError(intl.formatMessage(messages['discussions.captcha.verification.label']));
|
||||
return;
|
||||
}
|
||||
setCaptchaError('');
|
||||
// Preparing payload for submission
|
||||
if (recaptchaToken) {
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams);
|
||||
|
||||
const updatedpayload = {
|
||||
...payload,
|
||||
captcha_token: recaptchaToken,
|
||||
};
|
||||
// making register call
|
||||
dispatch(registerNewUser(updatedpayload));
|
||||
}
|
||||
let recaptchaToken = null;
|
||||
try {
|
||||
recaptchaToken = await executeWithFallback();
|
||||
} catch (err) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
captchaError: err.message,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams,
|
||||
);
|
||||
if (recaptchaToken) {
|
||||
payload = { ...payload, captcha_token: recaptchaToken };
|
||||
}
|
||||
|
||||
dispatch(registerNewUser(payload));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
@@ -413,9 +410,9 @@ const RegistrationPage = (props) => {
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
countriesCodesList={countriesCodesList}
|
||||
/>
|
||||
{captchaError && (
|
||||
{errors?.captchaError && (
|
||||
<div className="mt-3 pgn__form-text-invalid pgn__form-text">
|
||||
{captchaError}
|
||||
{errors.captchaError}
|
||||
</div>
|
||||
)}
|
||||
<StatefulButton
|
||||
|
||||
@@ -5,10 +5,7 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
|
||||
import {
|
||||
configure, getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -20,6 +17,7 @@ import {
|
||||
} from './data/actions';
|
||||
import { INTERNAL_SERVER_ERROR } from './data/constants';
|
||||
import mockTagular from '../cohesion/utils';
|
||||
import useRecaptchaSubmission from './data/hooks';
|
||||
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
@@ -28,12 +26,6 @@ import {
|
||||
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
// eslint-disable-next-line react/prop-types
|
||||
GoogleReCaptchaProvider: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
sendTrackEvent: jest.fn(),
|
||||
@@ -43,6 +35,12 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
jest.mock('./data/hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
executeWithFallback: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
@@ -142,8 +140,12 @@ describe('RegistrationPage', () => {
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue(null),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -185,36 +187,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form submission ********
|
||||
|
||||
it('should show captcha error if executeRecaptcha returns null token', async () => {
|
||||
const mockExecuteRecaptcha = jest.fn().mockResolvedValue(null);
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptcha });
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
next: '/course/demo-course-url',
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
waitFor(() => expect(screen.getByText('CAPTCHA verification failed.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should submit form for valid input', () => {
|
||||
it('should submit form for valid input', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
@@ -239,10 +212,12 @@ describe('RegistrationPage', () => {
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit form without password field when current provider is present', () => {
|
||||
it('should submit form without password field when current provider is present', async () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
const formPayload = {
|
||||
@@ -272,7 +247,9 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' })); });
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error when form is submitted with an invalid email', () => {
|
||||
@@ -323,7 +300,7 @@ describe('RegistrationPage', () => {
|
||||
expect(validationErrors.textContent).toContain(usernameError);
|
||||
});
|
||||
|
||||
it('should submit form with marketing email opt in value', () => {
|
||||
it('should submit form with marketing email opt in value', async () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
@@ -347,14 +324,16 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
|
||||
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', async () => {
|
||||
mergeConfig({
|
||||
ENABLE_AUTO_GENERATED_USERNAME: true,
|
||||
});
|
||||
@@ -374,7 +353,9 @@ describe('RegistrationPage', () => {
|
||||
populateRequiredFields(getByLabelText, payload, false, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
waitFor(() => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); });
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
mergeConfig({
|
||||
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||
});
|
||||
@@ -406,6 +387,124 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form validations ********
|
||||
|
||||
it('should submit form with valid reCAPTCHA token', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue('mock-recaptcha-token'),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const actions = store.dispatch.mock.calls.map(call => call[0]);
|
||||
const registerAction = actions.find(a => a.type === registerNewUser().type);
|
||||
|
||||
expect(registerAction).toBeTruthy();
|
||||
expect(registerAction.payload).toMatchObject({
|
||||
registrationInfo: {
|
||||
...payload,
|
||||
country: 'PK',
|
||||
captcha_token: 'mock-recaptcha-token',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error when reCAPTCHA verification fails', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockRejectedValue(new Error('CAPTCHA verification failed.')),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const captchaError = container.querySelector('.pgn__form-text-invalid');
|
||||
expect(captchaError.textContent).toContain('CAPTCHA verification failed.');
|
||||
});
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: registerNewUser().type,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should submit without reCAPTCHA token if reCAPTCHA is disabled', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue(null),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
registerNewUser({
|
||||
...payload,
|
||||
country: 'PK',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
@@ -885,7 +984,7 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationFormElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', () => {
|
||||
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', async () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
@@ -929,7 +1028,7 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
@@ -937,6 +1036,7 @@ describe('RegistrationPage', () => {
|
||||
country: 'PK',
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { APP_NAME } from '../../../data/constants';
|
||||
import { registerNewUser } from '../../data/actions';
|
||||
import { FIELDS } from '../../data/constants';
|
||||
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||
@@ -26,11 +26,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
// eslint-disable-next-line react/prop-types
|
||||
GoogleReCaptchaProvider: ({ children }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
@@ -225,20 +220,16 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
expect(document.querySelector('#tos')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should submit form with fields returned by backend in payload', () => {
|
||||
it('should submit form with fields returned by backend in payload', async () => {
|
||||
mergeConfig({
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
const mockExecuteRecaptchaNew = jest.fn(() => Promise.resolve('mock-token'));
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: mockExecuteRecaptchaNew });
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -274,8 +265,8 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
40
src/register/data/hooks.js
Normal file
40
src/register/data/hooks.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const useRecaptchaSubmission = (actionName = 'submit') => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
const isReady = !!executeRecaptcha || !recaptchaKey;
|
||||
|
||||
const executeWithFallback = useCallback(async () => {
|
||||
if (executeRecaptcha && recaptchaKey) {
|
||||
const token = await executeRecaptcha(actionName);
|
||||
if (!token) {
|
||||
throw new Error(formatMessage(messages['registration.captcha.verification.label']));
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// Fallback: no reCAPTCHA or not ready
|
||||
if (recaptchaKey) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`reCAPTCHA not ready for action: ${actionName}. Proceeding without token.`);
|
||||
}
|
||||
return null;
|
||||
}, [executeRecaptcha, recaptchaKey, actionName, formatMessage]);
|
||||
|
||||
return {
|
||||
executeWithFallback,
|
||||
isReady,
|
||||
isLoading: recaptchaKey && !executeRecaptcha,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRecaptchaSubmission;
|
||||
90
src/register/data/hooks.test.jsx
Normal file
90
src/register/data/hooks.test.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
|
||||
import useRecaptchaSubmission from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({ formatMessage: (msg) => msg.defaultMessage || msg }),
|
||||
}));
|
||||
|
||||
describe('useRecaptchaSubmission', () => {
|
||||
beforeEach(() => {
|
||||
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: 'test-key' });
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: jest.fn() });
|
||||
});
|
||||
|
||||
it('should throw error if reCAPTCHA returns empty token', async () => {
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
await expect(result.current.executeWithFallback()).rejects.toThrow(
|
||||
'CAPTCHA verification failed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn and return null if reCAPTCHA key exists but executeRecaptcha is not ready', async () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
|
||||
expect(token).toBeNull();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'reCAPTCHA not ready for action: test_action. Proceeding without token.',
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle undefined RECAPTCHA_SITE_KEY_WEB gracefully', async () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: undefined });
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
|
||||
expect(token).toBeNull();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return token if reCAPTCHA succeeds', async () => {
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: jest.fn().mockResolvedValue('valid-token'),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
expect(token).toBe('valid-token');
|
||||
});
|
||||
});
|
||||
@@ -206,8 +206,8 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Did you mean',
|
||||
description: 'Did you mean alert suggestion',
|
||||
},
|
||||
'discussions.captcha.verification.label': {
|
||||
id: 'discussions.captcha.verification.label',
|
||||
'registration.captcha.verification.label': {
|
||||
id: 'registration.captcha.verification.label',
|
||||
defaultMessage: 'CAPTCHA verification failed.',
|
||||
description: 'CAPTCHA verification failed',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user