Files
frontend-app-authn/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
Adolfo R. Brandes cb3ad5c53a feat: migrate from Redux to React Query and React Context
Replace Redux + Redux-Saga with React Query (useMutation/useQuery) for
server state and React Context for UI/form state across all modules:
login, registration, forgot-password, reset-password, progressive-
profiling, and common-components.

Port of master commits 0d709d15 and 93bd0f24, adapted for
@openedx/frontend-base:
- getSiteConfig() instead of getConfig()
- useAppConfig() for per-app configuration
- @tanstack/react-query as peerDependency (shell provides QueryClient)
- CurrentAppProvider instead of AppProvider

Also fixes EnvironmentTypes circular dependency in site.config.test.tsx
by using string literal instead of enum import.

Co-Authored-By: Jesus Balderrama <jesus.balderrama.wgu@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00

621 lines
21 KiB
JavaScript

import {
CurrentAppProvider, IntlProvider, mergeAppConfig,
} from '@openedx/frontend-base';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { useThirdPartyAuthContext } from '../../../common-components/components/ThirdPartyAuthContext';
import { appId } from '../../../constants';
import { useFieldValidations, useRegistration } from '../../data/apiHook';
import { FIELDS } from '../../data/constants';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
import { useRegisterContext } from '../RegisterContext';
jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
getLocale: jest.fn(),
}));
// eslint-disable-next-line import/first
import { getLocale } from '@openedx/frontend-base';
// Mock React Query hooks
jest.mock('../../data/apiHook', () => ({
useRegistration: jest.fn(),
useFieldValidations: jest.fn(),
}));
jest.mock('../RegisterContext', () => ({
RegisterProvider: ({ children }) => children,
useRegisterContext: jest.fn(),
useRegisterContextOptional: jest.fn(),
}));
jest.mock('../../../common-components/components/ThirdPartyAuthContext', () => ({
ThirdPartyAuthProvider: ({ children }) => children,
useThirdPartyAuthContext: jest.fn(),
}));
jest.mock('../../../common-components/data/apiHook', () => ({
useThirdPartyAuthHook: jest.fn().mockReturnValue({
data: null,
isSuccess: false,
error: null,
isLoading: false,
}),
}));
jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn();
// eslint-disable-next-line react/prop-types
const Navigate = ({ to }) => {
mockNavigation(to);
return <div />;
};
return {
...jest.requireActual('react-router-dom'),
Navigate,
mockNavigate: mockNavigation,
};
});
describe('ConfigurableRegistrationForm', () => {
mergeAppConfig(appId, {
PRIVACY_POLICY: 'https://privacy-policy.com',
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
});
let props = {};
let queryClient;
const registrationFormData = {
configurableFormFields: {
marketingEmailsOptIn: true,
},
formFields: {
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
name: '', email: '', username: '', password: '',
},
};
const renderWrapper = children => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<CurrentAppProvider appId={appId}>
{children}
</CurrentAppProvider>
</IntlProvider>
</QueryClientProvider>
);
const routerWrapper = children => (
<MemoryRouter>
{children}
</MemoryRouter>
);
const mockRegisterContext = {
registrationResult: { success: false, redirectUrl: '', authenticatedUser: null },
registrationError: {},
registrationFormData,
usernameSuggestions: [],
validations: null,
submitState: 'default',
userPipelineDataLoaded: false,
validationApiRateLimited: false,
backendValidations: null,
backendCountryCode: '',
setValidationsSuccess: jest.fn(),
setValidationsFailure: jest.fn(),
clearUsernameSuggestions: jest.fn(),
clearRegistrationBackendError: jest.fn(),
updateRegistrationFormData: jest.fn(),
setRegistrationResult: jest.fn(),
setBackendCountryCode: jest.fn(),
setUserPipelineDataLoaded: jest.fn(),
setRegistrationError: jest.fn(),
setEmailSuggestionContext: jest.fn(),
};
const mockThirdPartyAuthContext = {
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
};
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
// Setup default mocks
useRegistration.mockReturnValue({
mutate: jest.fn(),
isLoading: false,
error: null,
});
useRegisterContext.mockReturnValue(mockRegisterContext);
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
useFieldValidations.mockReturnValue({
mutate: jest.fn(),
isLoading: false,
error: null,
});
props = {
email: '',
fieldDescriptions: {},
fieldErrors: {},
formFields: {},
setFieldErrors: jest.fn(),
setFormFields: jest.fn(),
registrationEmbedded: false,
autoSubmitRegistrationForm: false,
handleInstitutionLogin: jest.fn(),
institutionLogin: false,
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
});
afterEach(() => {
jest.clearAllMocks();
});
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
fireEvent.blur(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
if (!isThirdPartyAuth) {
fireEvent.change(getByLabelText('Password'), { target: { value: payload.password, name: 'password' } });
}
};
describe('Test Configurable Fields', () => {
mergeAppConfig(appId, {
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
});
it('should render fields returned by backend as field descriptions', () => {
props = {
...props,
fieldDescriptions: {
profession: { name: 'profession', type: 'text', label: 'Profession' },
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
},
};
render(routerWrapper(renderWrapper(
<ConfigurableRegistrationForm {...props} />,
)));
expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy();
});
it('should check TOS and honor code fields if they exist when auto submitting register form', () => {
props = {
...props,
formFields: {
country: {
countryCode: '',
displayValue: '',
},
},
fieldDescriptions: {
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
honor_code: {
name: FIELDS.HONOR_CODE,
error_message: 'You must agree to the Honor Code agreement of our site',
},
},
autoSubmitRegistrationForm: true,
};
render(routerWrapper(renderWrapper(
<ConfigurableRegistrationForm {...props} />,
)));
expect(props.setFormFields).toHaveBeenCalledTimes(2);
expect(props.setFormFields.mock.calls[0][0]()).toEqual({
[FIELDS.HONOR_CODE]: true,
});
expect(props.setFormFields.mock.calls[1][0]()).toEqual({
[FIELDS.TERMS_OF_SERVICE]: true,
});
});
it('should render fields returned by backend', () => {
useThirdPartyAuthContext.mockReturnValue({
...mockThirdPartyAuthContext,
fieldDescriptions: {
profession: { name: 'profession', type: 'text', label: 'Profession' },
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
},
});
render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy();
});
it('should submit form with fields returned by backend in payload', () => {
mergeAppConfig(appId, {
SHOW_CONFIGURABLE_EDX_FIELDS: true,
});
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
fieldDescriptions: {
profession: { name: 'profession', type: 'text', label: 'Profession' },
country: { name: 'country' },
},
optionalFields: ['profession'],
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
password: 'password1',
country: 'Pakistan',
profession: 'Engineer',
total_registration_time: 0,
};
const mockRegisterUser = jest.fn();
useRegistration.mockReturnValue({
mutate: mockRegisterUser,
isLoading: false,
error: null,
});
useThirdPartyAuthContext.mockReturnValue({
...mockThirdPartyAuthContext,
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession',
},
country: { name: 'country' },
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
});
const { getByLabelText, container } = render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const professionInput = getByLabelText('Profession');
fireEvent.change(professionInput, { target: { value: 'Engineer', name: 'profession' } });
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
expect(mockRegisterUser).toHaveBeenCalledWith({ ...payload, country: 'PK' });
});
it('should show error messages for required fields on empty form submission', () => {
const professionError = 'Enter your profession';
const countryError = 'Select your country or region of residence';
const confirmEmailError = 'Enter your email';
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
},
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError,
},
country: { name: 'country' },
},
optionalFields: [],
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
});
const { container } = render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
const professionErrorElement = container.querySelector('#profession-error');
const countryErrorElement = container.querySelector('div[feedback-for="country"]');
const confirmEmailErrorElement = container.querySelector('#confirm_email-error');
expect(professionErrorElement.textContent).toEqual(professionError);
expect(countryErrorElement.textContent).toEqual(countryError);
expect(confirmEmailErrorElement.textContent).toEqual(confirmEmailError);
});
it('should show country field validation when country name is invalid', () => {
const invalidCountryError = 'Country must match with an option available in the dropdown.';
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
fieldDescriptions: {
country: { name: 'country' },
},
optionalFields: [],
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
});
const { container } = render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
const countryErrorElement = container.querySelector('div[feedback-for="country"]');
expect(countryErrorElement.textContent).toEqual(invalidCountryError);
});
it('should show error if email and confirm email fields do not match', () => {
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
fieldDescriptions: {
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email',
},
},
optionalFields: [],
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
});
const { getByLabelText, container } = render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
const emailInput = getByLabelText('Email');
const confirmEmailInput = getByLabelText('Confirm Email');
fireEvent.change(emailInput, { target: { value: 'test1@gmail.com', name: 'email' } });
fireEvent.blur(confirmEmailInput, { target: { value: 'test2@gmail.com', name: 'confirm_email' } });
const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
});
it('should show error if email and confirm email fields do not match on submit click', () => {
const formPayload = {
name: 'Petro',
username: 'petro_qa',
email: 'petro@example.com',
password: 'password1',
country: 'Ukraine',
total_registration_time: 0,
};
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
fieldDescriptions: {
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email',
},
country: { name: 'country' },
},
optionalFields: [],
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
});
const { getByLabelText, container } = render(routerWrapper(renderWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change(
getByLabelText('Confirm Email'),
{ target: { value: 'test2@gmail.com', name: 'confirm_email' } },
);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
const validationErrors = container.querySelector('#validation-errors');
expect(validationErrors.textContent).toContain(
"We couldn't create your account.Please check your responses and try again.",
);
});
it('should run validations for configurable focused field on form submission', () => {
const professionError = 'Enter your profession';
useThirdPartyAuthContext.mockReturnValue({
currentProvider: null,
platformName: '',
providers: [],
secondaryProviders: [],
handleInstitutionLogin: jest.fn(),
handleInstitutionLogout: jest.fn(),
isInstitutionAuthActive: false,
institutionLogin: false,
pipelineDetails: {},
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
pipelineUserDetails: null,
providers: [],
secondaryProviders: [],
errorMessage: null,
},
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
},
},
optionalFields: [],
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
setEmailSuggestionContext: jest.fn(),
clearThirdPartyAuthErrorMessage: jest.fn(),
});
const { getByLabelText, container } = render(
routerWrapper(renderWrapper(<RegistrationPage {...props} />)),
);
const professionInput = getByLabelText('Profession');
fireEvent.focus(professionInput);
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
const professionErrorElement = container.querySelector('#profession-error');
expect(professionErrorElement.textContent).toEqual(professionError);
});
});
});