Compare commits

..

1 Commits

Author SHA1 Message Date
sundasnoreen12
3355a014e6 feat: implemented restricted countries functionality 2025-02-24 13:44:39 +05:00
55 changed files with 6673 additions and 17799 deletions

2
.env
View File

@@ -41,5 +41,3 @@ BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -41,5 +41,3 @@ APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -18,4 +18,3 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

View File

@@ -16,7 +16,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Nodejs
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'

2
.nvmrc
View File

@@ -1 +1 @@
24
20

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/2u-infinity

View File

@@ -29,13 +29,7 @@ Getting Started
Installation
============
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
@@ -57,7 +51,7 @@ Devstack (Deprecated) instructions
Environment Variables/Setup Notes
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
The authentication micro-frontend also requires the following additional variable:
@@ -148,13 +142,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
How To Contribute
=================
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.

View File

@@ -3,7 +3,7 @@ Enable Social Auth Locally
Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.

23433
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,15 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -30,32 +33,33 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-platform": "^8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/paragon": "^23.4.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^16.2.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.26.0",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.43.0",
"core-js": "3.40.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.6",
"form-urlencoded": "6.1.5",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"react-router": "6.29.0",
"react-router-dom": "6.29.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
@@ -68,13 +72,14 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.6.2",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "^14.0.3",
"babel-plugin-formatjs": "10.5.35",
"eslint-plugin-import": "2.31.0",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.2.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
"husky": "9.1.7",
"jest": "29.7.0",
"react-test-renderer": "^17.0.2"
}
}

View File

@@ -13,9 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
export const getThirdPartyAuthContextSuccess = (
fieldDescriptions,
optionalFields,
thirdPartyAuthContext,
countries) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
payload: {
fieldDescriptions, optionalFields, thirdPartyAuthContext, countries,
},
});
export const getThirdPartyAuthContextFailure = () => ({

View File

@@ -35,6 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
countries: action.payload.countries,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:

View File

@@ -8,6 +8,7 @@ import {
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import {
getCountryList,
getThirdPartyAuthContext,
} from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
@@ -18,9 +19,10 @@ export function* fetchThirdPartyAuthContext(action) {
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const countries = (yield call(getCountryList)) || [];
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext, countries));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -1,5 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { FIELD_LABELS } from '../../data/constants';
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
@@ -23,3 +26,28 @@ export async function getThirdPartyAuthContext(urlParams) {
thirdPartyAuthContext: data.contextData || {},
};
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value, name }) => ({ code: value, name })) || [];
}
export async function getCountryList() {
try {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
requestConfig,
);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -8,6 +8,11 @@ import * as api from '../service';
const { loggingService } = initializeMockLogging();
jest.mock('../service', () => ({
getCountryList: jest.fn(),
getThirdPartyAuthContext: jest.fn(),
}));
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
@@ -31,6 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
countries: [],
}));
const dispatched = [];
@@ -44,7 +50,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
actions.getThirdPartyAuthContextSuccess({}, {}, data, []),
]);
getThirdPartyAuthContext.mockClear();
});

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
@@ -36,6 +37,7 @@ describe('FormGroup', () => {
describe('PasswordField', () => {
const mockStore = configureStore();
const IntlPasswordField = injectIntl(PasswordField);
let props = {};
let store = {};
@@ -64,7 +66,7 @@ describe('PasswordField', () => {
});
it('should show/hide password on icon click', () => {
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password');
@@ -77,7 +79,7 @@ describe('PasswordField', () => {
});
it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -94,7 +96,7 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -117,7 +119,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -140,7 +142,7 @@ describe('PasswordField', () => {
});
it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password');
@@ -161,7 +163,7 @@ describe('PasswordField', () => {
...props,
handleBlur: jest.fn(),
};
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -179,7 +181,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -202,7 +204,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -222,7 +224,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -246,7 +248,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
@@ -266,7 +268,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');

View File

@@ -37,3 +37,6 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';
export const FIELD_LABELS = {
COUNTRY: 'country',
};

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -25,6 +26,7 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
}));
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
const mockStore = configureStore();
const initialState = {
@@ -76,7 +78,7 @@ describe('ForgotPasswordPage', () => {
);
it('not should display need other help signing in button', () => {
const { queryByTestId } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const forgotPasswordButton = queryByTestId('forgot-password');
expect(forgotPasswordButton).toBeNull();
});
@@ -85,14 +87,14 @@ describe('ForgotPasswordPage', () => {
mergeConfig({
LOGIN_ISSUE_SUPPORT_LINK: '/support',
});
render(reduxWrapper(<ForgotPasswordPage {...props} />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const forgotPasswordButton = screen.findByText('Need help signing in?');
expect(forgotPasswordButton).toBeDefined();
});
it('should display email validation error message', async () => {
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -113,7 +115,7 @@ describe('ForgotPasswordPage', () => {
const expectedMessage = 'We were unable to contact you.'
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
@@ -122,7 +124,7 @@ describe('ForgotPasswordPage', () => {
it('should display empty email validation message', async () => {
const validationMessage = 'We were unable to contact you.Enter your email below.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
@@ -139,7 +141,7 @@ describe('ForgotPasswordPage', () => {
forgotPassword: { status: 'forbidden' },
});
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
@@ -147,7 +149,7 @@ describe('ForgotPasswordPage', () => {
});
it('should not display any error message on change event', () => {
render(reduxWrapper(<ForgotPasswordPage {...props} />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -170,7 +172,7 @@ describe('ForgotPasswordPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ForgotPasswordPage {...props} />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.blur(emailInput);
@@ -185,7 +187,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: validationMessage,
email: '',
};
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const validationElement = container.querySelector('.pgn__form-text-invalid');
expect(validationElement.textContent).toEqual(validationMessage);
});
@@ -203,7 +205,7 @@ describe('ForgotPasswordPage', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ForgotPasswordPage {...props} />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.focus(emailInput);
@@ -217,7 +219,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: '',
email: '',
};
render(reduxWrapper(<ForgotPasswordPage {...props} />));
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const errorElement = screen.queryByTestId('email-invalid-feedback');
expect(errorElement).toBeNull();
});
@@ -234,7 +236,7 @@ describe('ForgotPasswordPage', () => {
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
+ ' or check your spam folder. If you need further assistance, contact technical support.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined();
@@ -252,7 +254,7 @@ describe('ForgotPasswordPage', () => {
+ 'This password reset link is invalid. It may have been used already. '
+ 'Enter your email below to receive a new link.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined();
@@ -260,7 +262,7 @@ describe('ForgotPasswordPage', () => {
});
it('should redirect onto login page', async () => {
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const navElement = container.querySelector('nav');
const anchorElement = navElement.querySelector('a');

View File

@@ -1,36 +1,27 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React, { StrictMode } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
} from '@edx/frontend-platform';
import { ErrorPage } from '@edx/frontend-platform/react';
import { createRoot } from 'react-dom/client';
import configuration from './config';
import messages from './i18n';
import MainApp from './MainApp';
subscribe(APP_READY, () => {
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<MainApp />
</StrictMode>,
ReactDOM.render(
<MainApp />,
document.getElementById('root'),
);
});
subscribe(APP_INIT_ERROR, (error) => {
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
});
initialize({

View File

@@ -1,2 +1,6 @@
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "sass/style";

View File

@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
} from '@openedx/paragon';
@@ -365,4 +365,4 @@ export default connect(
loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext,
},
)(LoginPage);
)(injectIntl(LoginPage));

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
render, screen,
} from '@testing-library/react';
@@ -7,6 +9,8 @@ import {
import AccountActivationMessage from '../AccountActivationMessage';
import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
describe('AccountActivationMessage', () => {
beforeEach(() => {
mergeConfig({
@@ -17,7 +21,7 @@ describe('AccountActivationMessage', () => {
it('should match account already activated message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
</IntlProvider>,
);
@@ -32,7 +36,7 @@ describe('AccountActivationMessage', () => {
it('should match account activated success message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
</IntlProvider>,
);
@@ -49,7 +53,7 @@ describe('AccountActivationMessage', () => {
it('should match account activation error message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
</IntlProvider>,
);
@@ -65,7 +69,7 @@ describe('AccountActivationMessage', () => {
it('should not display anything for invalid message type', () => {
const { container } = render(
<IntlProvider locale="en">
<AccountActivationMessage messageType="invalid-message" />
<IntlAccountActivationMessage messageType="invalid-message" />
</IntlProvider>,
);
@@ -84,7 +88,7 @@ describe('EmailConfirmationMessage', () => {
it('should match email already confirmed message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
</IntlProvider>,
);
@@ -99,7 +103,7 @@ describe('EmailConfirmationMessage', () => {
it('should match email confirmation success message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
</IntlProvider>,
);
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
@@ -113,7 +117,7 @@ describe('EmailConfirmationMessage', () => {
it('should match email confirmation error message', () => {
render(
<IntlProvider locale="en">
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
</IntlProvider>,
);
const expectedMessage = 'Your email could not be confirmed'

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -9,6 +11,7 @@ import { MemoryRouter } from 'react-router-dom';
import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
@@ -41,7 +44,7 @@ describe('ChangePasswordPromptTests', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<ChangePasswordPrompt {...props} />
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -58,7 +61,7 @@ describe('ChangePasswordPromptTests', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<ChangePasswordPrompt {...props} />
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -1,4 +1,6 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import React from 'react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
render, screen,
} from '@testing-library/react';
@@ -24,6 +26,8 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
describe('LoginFailureMessage', () => {
let props = {};
@@ -44,7 +48,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -72,7 +76,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -102,7 +106,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -128,7 +132,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -148,7 +152,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -172,7 +176,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -192,7 +196,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -212,7 +216,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -232,7 +236,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -251,7 +255,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -271,7 +275,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -297,7 +301,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -323,7 +327,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -355,7 +359,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<LoginFailureMessage {...props} />
<IntlLoginFailureMessage {...props} />
</IntlProvider>,
);

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
@@ -23,6 +24,7 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore();
describe('LoginPage', () => {
@@ -86,7 +88,7 @@ describe('LoginPage', () => {
it('should submit form for valid input', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText(
'',
@@ -107,7 +109,7 @@ describe('LoginPage', () => {
it('should not dispatch loginRequest on empty form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
@@ -126,7 +128,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
@@ -140,7 +142,7 @@ describe('LoginPage', () => {
it('should match state for invalid email (less than 2 characters), on form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText(
'',
@@ -160,7 +162,7 @@ describe('LoginPage', () => {
});
it('should show error messages for required fields on empty form submission', () => {
const { container } = render(reduxWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
@@ -174,7 +176,7 @@ describe('LoginPage', () => {
});
it('should run frontend validations for emailOrUsername field on form submission', () => {
const { container } = render(reduxWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText(
'',
@@ -193,7 +195,7 @@ describe('LoginPage', () => {
it('should reset field related error messages on onFocus event', async () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
await act(async () => {
// clicking submit button with empty fields to make the errors appear
@@ -222,7 +224,7 @@ describe('LoginPage', () => {
// ******** test form buttons and links ********
it('should match default button state', () => {
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText('Sign in')).toBeDefined();
});
@@ -235,7 +237,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'pending',
@@ -243,7 +245,7 @@ describe('LoginPage', () => {
});
it('should show forgot password link', () => {
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'Forgot password',
@@ -263,7 +265,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: `#${ssoProvider.id}` },
@@ -285,7 +287,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
@@ -305,7 +307,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
});
@@ -325,7 +327,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -350,7 +352,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -378,7 +380,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -398,7 +400,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
expect(queryByText('Company or school credentials')).toBeNull();
@@ -416,7 +418,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -439,7 +441,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -463,7 +465,7 @@ describe('LoginPage', () => {
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`;
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#tpa-alert' },
@@ -482,7 +484,7 @@ describe('LoginPage', () => {
},
},
});
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -499,7 +501,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -523,7 +525,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(dashboardURL);
});
@@ -550,7 +552,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
@@ -569,7 +571,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
@@ -598,7 +600,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
});
@@ -620,7 +622,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: `#${ssoProvider.id}` },
@@ -647,7 +649,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(reduxWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
});
@@ -669,7 +671,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null;
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
});
@@ -689,7 +691,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(reduxWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
mergeConfig({
@@ -713,7 +715,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'Show me other ways to sign in or register',
).textContent).toBeDefined();
@@ -739,7 +741,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText(
'Show me other ways to sign in',
).textContent).toBeDefined();
@@ -748,7 +750,7 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ********
it('should send page event when login page is rendered', () => {
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
});
@@ -762,7 +764,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
@@ -776,7 +778,7 @@ describe('LoginPage', () => {
});
it('should send track event when forgot password link is clicked', () => {
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText(
'Forgot password',
{ selector: '#forgot-password' },
@@ -795,7 +797,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
@@ -824,7 +826,7 @@ describe('LoginPage', () => {
},
});
const { container } = render(reduxWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password');
});

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -22,6 +23,7 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth');
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => {
let store = {};
@@ -93,7 +95,7 @@ describe('Logistration', () => {
});
it('should do nothing when user clicks on the same tab (login/register) again', () => {
const { container } = render(reduxWrapper(<Logistration />));
const { container } = render(reduxWrapper(<IntlLogistration />));
// While staying on the registration form, clicking the register tab again
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
@@ -105,14 +107,14 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
});
const { container } = render(reduxWrapper(<Logistration />));
const { container } = render(reduxWrapper(<IntlLogistration />));
expect(container.querySelector('RegistrationPage')).toBeDefined();
});
it('should render login page', () => {
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />));
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
expect(container.querySelector('LoginPage')).toBeDefined();
});
@@ -123,7 +125,7 @@ describe('Logistration', () => {
});
let props = { selectedPage: LOGIN_PAGE };
const { rerender } = render(reduxWrapper(<Logistration {...props} />));
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
// verifying sign in heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -131,7 +133,7 @@ describe('Logistration', () => {
// register page is still accessible when SHOW_REGISTRATION_LINKS is false
// but it needs to be accessed directly
props = { selectedPage: REGISTER_PAGE };
rerender(reduxWrapper(<Logistration {...props} />));
rerender(reduxWrapper(<IntlLogistration {...props} />));
// verifying register heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
@@ -158,7 +160,7 @@ describe('Logistration', () => {
});
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />));
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
// verifying sign in heading for institution login false
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -188,7 +190,7 @@ describe('Logistration', () => {
});
const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<Logistration {...props} />));
render(reduxWrapper(<IntlLogistration {...props} />));
expect(screen.getByText('Institution/campus credentials')).toBeDefined();
// on clicking "Institution/campus credentials" button, it should display institution login page
@@ -219,7 +221,7 @@ describe('Logistration', () => {
});
const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<Logistration {...props} />));
render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
@@ -251,7 +253,7 @@ describe('Logistration', () => {
delete window.location;
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
render(reduxWrapper(<Logistration />));
render(reduxWrapper(<IntlLogistration />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(screen.getByText('Test University')).toBeDefined();
@@ -262,7 +264,7 @@ describe('Logistration', () => {
it('should fire action to backup registration form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />));
const { container } = render(reduxWrapper(<IntlLogistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
});
@@ -270,14 +272,14 @@ describe('Logistration', () => {
it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />));
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
});
it('should clear tpa context errorMessage tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />));
const { container } = render(reduxWrapper(<IntlLogistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
});

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -21,6 +22,7 @@ import {
import { saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -112,7 +114,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
});
const { queryByRole } = render(reduxWrapper(<ProgressiveProfiling />));
const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const button = queryByRole('button', { name: /learn more about how we use this information/i });
expect(button).toBeNull();
@@ -123,7 +125,7 @@ describe('ProgressiveProfilingTests', () => {
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
});
const { getByText } = render(reduxWrapper(<ProgressiveProfiling />));
const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const learnMoreButton = getByText('Learn more about how we use this information.');
@@ -133,7 +135,7 @@ describe('ProgressiveProfilingTests', () => {
it('should open modal on pressing skip for now button', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
const { getByRole } = render(reduxWrapper(<ProgressiveProfiling />));
const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const skipButton = getByRole('button', { name: /skip for now/i });
fireEvent.click(skipButton);
@@ -148,7 +150,7 @@ describe('ProgressiveProfilingTests', () => {
// ******** test event functionality ********
it('should make identify call to segment on progressive profiling page', () => {
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
expect(identifyAuthenticatedUser).toHaveBeenCalled();
@@ -158,7 +160,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
});
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink);
@@ -176,7 +178,7 @@ describe('ProgressiveProfilingTests', () => {
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = screen.getByText('Next');
fireEvent.click(nextButton);
@@ -192,7 +194,7 @@ describe('ProgressiveProfilingTests', () => {
extended_profile: [{ field_name: 'company', field_value: 'test company' }],
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, getByText } = render(reduxWrapper(<ProgressiveProfiling />));
const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const genderSelect = getByLabelText('Gender');
const companyInput = getByLabelText('Company');
@@ -214,7 +216,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const errorElement = container.querySelector('#pp-page-errors');
expect(errorElement).toBeTruthy();
@@ -230,7 +232,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL,
};
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(window.location.href).toEqual(DASHBOARD_URL);
});
@@ -248,7 +250,7 @@ describe('ProgressiveProfilingTests', () => {
success: true,
},
});
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Next');
@@ -275,7 +277,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit');
@@ -309,7 +311,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}&variant=${EMBEDDED}`,
};
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
@@ -334,7 +336,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const tpaSpinnerElement = container.querySelector('#tpa-spinner');
expect(tpaSpinnerElement).toBeTruthy();
@@ -353,7 +355,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}`,
};
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Next');
fireEvent.click(submitButton);
@@ -368,7 +370,7 @@ describe('ProgressiveProfilingTests', () => {
search: `?variant=${EMBEDDED}&host=${host}`,
};
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const genderField = container.querySelector('#gender');
expect(genderField).toBeTruthy();
@@ -389,7 +391,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(window.location.href).toBe(DASHBOARD_URL);
});
@@ -417,7 +419,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
render(reduxWrapper(<ProgressiveProfiling />));
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
expect(window.location.href).toBe(redirectUrl);

View File

@@ -1,9 +1,13 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import React from 'react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
import SmallLayout from './SmallLayout';
import mockedRecommendedProducts from '../data/tests/mockedData';
const IntlRecommendationsSmallLayoutPage = injectIntl(SmallLayout);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -32,7 +36,7 @@ describe('RecommendationsPageTests', () => {
});
it('should render recommendations when recommendations are not loading', () => {
const { container } = render(reduxWrapper(<SmallLayout {...props} />));
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -44,7 +48,7 @@ describe('RecommendationsPageTests', () => {
...props,
isLoading: true,
};
const { container } = render(reduxWrapper(<SmallLayout {...props} />));
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');

View File

@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData';

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
import configureStore from 'redux-mock-store';
import mockedProductData from './mockedData';
import RecommendationList from '../RecommendationsList';
const IntlRecommendationList = injectIntl(RecommendationList);
const mockStore = configureStore();
describe('RecommendationsListTests', () => {
@@ -23,7 +25,7 @@ describe('RecommendationsListTests', () => {
userId: 1234567,
};
const { container } = render(reduxWrapper(<RecommendationList {...props} />));
const { container } = render(reduxWrapper(<IntlRecommendationList {...props} />));
const recommendationCards = container.querySelectorAll('.recommendation-card');
expect(recommendationCards.length).toEqual(mockedProductData.length);
@@ -35,7 +37,7 @@ describe('RecommendationsListTests', () => {
userId: 1234567,
};
const { getByText } = render(reduxWrapper(<RecommendationList {...props} />));
const { getByText } = render(reduxWrapper(<IntlRecommendationList {...props} />));
const firstFooterContent = getByText('1 Course');
const secondFooterContent = getByText('2 Courses');

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { useMediaQuery } from '@openedx/paragon';
import { fireEvent, render } from '@testing-library/react';
import { useLocation } from 'react-router-dom';
@@ -15,6 +16,7 @@ import mockedRecommendedProducts from '../data/tests/mockedData';
import RecommendationsPage from '../RecommendationsPage';
import { eventNames, getProductMapping } from '../track';
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -75,7 +77,7 @@ describe('RecommendationsPageTests', () => {
});
it('should redirect to dashboard if user is not coming from registration workflow', () => {
render(reduxWrapper(<RecommendationsPage />));
render(reduxWrapper(<IntlRecommendationsPage />));
expect(window.location.href).toEqual(dashboardUrl);
});
@@ -84,14 +86,14 @@ describe('RecommendationsPageTests', () => {
recommendations: [],
isLoading: false,
});
render(reduxWrapper(<RecommendationsPage />));
render(reduxWrapper(<IntlRecommendationsPage />));
expect(window.location.href).toEqual(dashboardUrl);
});
it('should redirect user if they click "Skip for now" button', () => {
mockUseLocation();
jest.useFakeTimers();
const { container } = render(reduxWrapper(<RecommendationsPage />));
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
const skipButton = container.querySelector('.pgn__stateful-btn-state-default');
fireEvent.click(skipButton);
jest.advanceTimersByTime(300);
@@ -101,7 +103,7 @@ describe('RecommendationsPageTests', () => {
it('should display recommendations small layout for small screen', () => {
mockUseLocation();
useMediaQuery.mockReturnValue(true);
const { container } = render(reduxWrapper(<RecommendationsPage />));
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
const recommendationsSmallLayout = container.querySelector('#recommendations-small-layout');
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -113,7 +115,7 @@ describe('RecommendationsPageTests', () => {
it('should display recommendations large layout for large screen', () => {
mockUseLocation();
useMediaQuery.mockReturnValue(false);
const { container } = render(reduxWrapper(<RecommendationsPage />));
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
const pgnCollapsible = container.querySelector('.pgn_collapsible');
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -129,7 +131,7 @@ describe('RecommendationsPageTests', () => {
recommendations: [],
isLoading: true,
});
const { container } = render(reduxWrapper(<RecommendationsPage />));
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -143,7 +145,7 @@ describe('RecommendationsPageTests', () => {
recommendations: [],
isLoading: true,
});
const { container } = render(reduxWrapper(<RecommendationsPage />));
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -158,7 +160,7 @@ describe('RecommendationsPageTests', () => {
});
useMediaQuery.mockReturnValue(false);
render(reduxWrapper(<RecommendationsPage />));
render(reduxWrapper(<IntlRecommendationsPage />));
expect(sendTrackEvent).toBeCalled();
expect(sendTrackEvent).toHaveBeenCalledWith(

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -9,6 +10,7 @@ import configureStore from 'redux-mock-store';
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
import { CountryField } from '../index';
const IntlCountryField = injectIntl(CountryField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -80,7 +82,7 @@ describe('CountryField', () => {
};
it('should run country field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, {
@@ -95,7 +97,7 @@ describe('CountryField', () => {
});
it('should run country field validation when country name is invalid', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, {
@@ -110,7 +112,7 @@ describe('CountryField', () => {
});
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
@@ -123,7 +125,7 @@ describe('CountryField', () => {
});
it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
@@ -133,7 +135,7 @@ describe('CountryField', () => {
});
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.focus(countryInput);
@@ -151,7 +153,7 @@ describe('CountryField', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
container.querySelector('input[name="country"]');
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
@@ -162,7 +164,7 @@ describe('CountryField', () => {
});
it('should set option on dropdown menu item click', () => {
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button');
fireEvent.click(dropdownButton);
@@ -179,7 +181,7 @@ describe('CountryField', () => {
it('should set value on change', () => {
const { container } = render(
routerWrapper(reduxWrapper(<CountryField {...props} />)),
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)),
);
const countryInput = container.querySelector('input[name="country"]');
@@ -198,7 +200,7 @@ describe('CountryField', () => {
errorMessage: 'country error message',
};
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const feedbackElement = container.querySelector('div[feedback-for="country"]');
expect(feedbackElement).toBeTruthy();

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -9,6 +10,7 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { EmailField } from '../index';
const IntlEmailField = injectIntl(EmailField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -78,7 +80,7 @@ describe('EmailField', () => {
};
it('should run email field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: '', name: 'email' } });
@@ -90,7 +92,7 @@ describe('EmailField', () => {
});
it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } });
@@ -103,7 +105,7 @@ describe('EmailField', () => {
});
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.focus(emailInput, { target: { value: '', name: 'email' } });
@@ -117,7 +119,7 @@ describe('EmailField', () => {
it('should call backend validation api on blur event, if frontend validations have passed', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
// Enter a valid email so that frontend validations are passed
const emailInput = container.querySelector('input#email');
@@ -127,7 +129,7 @@ describe('EmailField', () => {
});
it('should give email suggestions for common service provider domain typos', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
@@ -137,7 +139,7 @@ describe('EmailField', () => {
});
it('should be able to click on email suggestions and set it as value', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
@@ -152,7 +154,7 @@ describe('EmailField', () => {
});
it('should give error for common top level domain mistakes', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
@@ -162,7 +164,7 @@ describe('EmailField', () => {
});
it('should give error and suggestion for invalid email', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } });
@@ -192,7 +194,7 @@ describe('EmailField', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.focus(emailInput, { target: { value: 'a@gmail.com', name: 'email' } });
@@ -201,7 +203,7 @@ describe('EmailField', () => {
});
it('should clear email suggestions when close icon is clicked', () => {
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
@@ -222,7 +224,7 @@ describe('EmailField', () => {
confirmEmailValue: 'confirmEmail@yopmail.com',
};
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'differentEmail@yopmail.com', name: 'email' } });

View File

@@ -1,9 +1,13 @@
import React from 'react';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
import { HonorCode } from '../index';
const IntlHonorCode = injectIntl(HonorCode);
describe('HonorCodeTest', () => {
mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com',
@@ -24,7 +28,7 @@ describe('HonorCodeTest', () => {
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Honor Code`;
const { container } = render(
<IntlProvider locale="en">
<HonorCode
<IntlHonorCode
errorMessage={errorMessage}
onChangeHandler={changeHandler}
/>
@@ -39,7 +43,7 @@ describe('HonorCodeTest', () => {
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab';
const { container } = render(
<IntlProvider locale="en">
<HonorCode onChangeHandler={changeHandler} />
<IntlHonorCode onChangeHandler={changeHandler} />
</IntlProvider>,
);
@@ -52,7 +56,7 @@ describe('HonorCodeTest', () => {
it('should render Terms of Service and Honor code field', () => {
const { container } = render(
<IntlProvider locale="en">
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you '

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -8,6 +9,7 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { NameField } from '../index';
const IntlNameField = injectIntl(NameField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -67,7 +69,7 @@ describe('NameField', () => {
const fieldValidation = { name: 'Enter your full name' };
it('should run name field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
@@ -80,7 +82,7 @@ describe('NameField', () => {
});
it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
@@ -93,7 +95,7 @@ describe('NameField', () => {
});
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const nameInput = container.querySelector('input#name');
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
@@ -111,7 +113,7 @@ describe('NameField', () => {
...props,
shouldFetchUsernameSuggestions: true,
};
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const nameInput = container.querySelector('input#name');
// Enter a valid name so that frontend validations are passed
@@ -133,7 +135,7 @@ describe('NameField', () => {
});
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const nameInput = container.querySelector('input#name');

View File

@@ -1,9 +1,13 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { TermsOfService } from '../index';
const IntlTermsOfService = injectIntl(TermsOfService);
describe('TermsOfServiceTest', () => {
let value = false;
@@ -19,7 +23,7 @@ describe('TermsOfServiceTest', () => {
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Terms of Service`;
const { container } = render(
<IntlProvider locale="en">
<TermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
</IntlProvider>,
);
const errorElement = container.querySelector('.form-text-size');
@@ -29,7 +33,7 @@ describe('TermsOfServiceTest', () => {
it('should render Terms of Service field', () => {
const { container } = render(
<IntlProvider locale="en">
<TermsOfService onChangeHandler={changeHandler} />
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
@@ -44,7 +48,7 @@ describe('TermsOfServiceTest', () => {
it('should change value when Terms of Service field is checked', () => {
const { container } = render(
<IntlProvider locale="en">
<TermsOfService onChangeHandler={changeHandler} />
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
const field = container.querySelector('input#tos');

View File

@@ -101,7 +101,7 @@ const UsernameField = (props) => {
};
const suggestedUsernames = () => (
<div className={className} role="listbox">
<div className={className}>
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
<div className="username-scroll-suggested--form-field">
{usernameSuggestions.map((username, index) => (
@@ -112,9 +112,7 @@ const UsernameField = (props) => {
className="username-suggestions--chip data-hj-suppress"
autoComplete={props.autoComplete}
key={`suggestion-${index.toString()}`}
tabIndex={0}
onClick={(e) => handleSuggestionClick(e, username)}
role="option"
>
{username}
</Button>
@@ -125,7 +123,7 @@ const UsernameField = (props) => {
);
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
className = 'username-suggestions';
className = 'username-suggestions__error';
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
suggestedUsernameDiv = suggestedUsernames();
} else if (usernameSuggestions.length > 0 && value === ' ') {
@@ -136,15 +134,14 @@ const UsernameField = (props) => {
suggestedUsernameDiv = suggestedUsernames();
}
return (
<div className="username__form-group-wrapper">
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
>
{suggestedUsernameDiv}
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
/>
</div>
</FormGroup>
);
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -8,6 +9,7 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions';
import { UsernameField } from '../index';
const IntlUsernameField = injectIntl(UsernameField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -71,7 +73,7 @@ describe('UsernameField', () => {
};
it('should run username field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
@@ -84,7 +86,7 @@ describe('UsernameField', () => {
});
it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
@@ -97,7 +99,7 @@ describe('UsernameField', () => {
});
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
@@ -110,7 +112,7 @@ describe('UsernameField', () => {
});
it('should remove space from field on focus if space exists', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } });
@@ -123,7 +125,7 @@ describe('UsernameField', () => {
it('should call backend validation api on blur event, if frontend validations have passed', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
// Enter a valid username so that frontend validations are passed
@@ -133,7 +135,7 @@ describe('UsernameField', () => {
});
it('should remove space from the start of username on change', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } });
@@ -144,7 +146,7 @@ describe('UsernameField', () => {
});
it('should not set username if it is more than 30 character long', () => {
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
@@ -155,7 +157,7 @@ describe('UsernameField', () => {
it('should clear username suggestions when username field is focused in', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField);
@@ -177,7 +179,7 @@ describe('UsernameField', () => {
errorMessage: 'It looks like this username is already taken',
};
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3);
});
@@ -196,7 +198,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3);
});
@@ -216,7 +218,7 @@ describe('UsernameField', () => {
errorMessage: 'username error',
};
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3);
});
@@ -230,7 +232,7 @@ describe('UsernameField', () => {
},
});
render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
expect(props.handleChange).toHaveBeenCalledTimes(1);
expect(props.handleChange).toHaveBeenCalledWith(
{ target: { name: 'username', value: ' ' } },
@@ -251,7 +253,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameSuggestion = container.querySelector('.username-suggestions--chip');
fireEvent.click(usernameSuggestion);
expect(props.handleChange).toHaveBeenCalledTimes(1);
@@ -275,7 +277,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
let closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -285,7 +287,7 @@ describe('UsernameField', () => {
errorMessage: 'username error',
};
render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -307,7 +309,7 @@ describe('UsernameField', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });

View File

@@ -85,6 +85,7 @@ const RegistrationPage = (props) => {
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
const countries = useSelector(state => state.commonComponents.countries);
const backendValidations = useSelector(getBackendValidations);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -358,6 +359,7 @@ const RegistrationPage = (props) => {
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
countries={countries}
/>
<StatefulButton
id="register-user"

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure, getLocale, IntlProvider,
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
@@ -30,6 +31,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -186,7 +188,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -218,7 +220,7 @@ describe('RegistrationPage', () => {
},
});
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand');
@@ -241,7 +243,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand');
@@ -266,7 +268,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -293,7 +295,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -319,7 +321,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload, false, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -334,7 +336,7 @@ describe('RegistrationPage', () => {
ENABLE_AUTO_GENERATED_USERNAME: true,
});
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(queryByLabelText('Username')).toBeNull();
mergeConfig({
@@ -345,7 +347,7 @@ describe('RegistrationPage', () => {
it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -356,7 +358,7 @@ describe('RegistrationPage', () => {
// ******** test registration form validations ********
it('should show error messages for required fields on empty form submission', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -384,7 +386,7 @@ describe('RegistrationPage', () => {
},
},
});
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><RegistrationPage {...props} /></IntlProvider>)));
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><IntlRegistrationPage {...props} /></IntlProvider>)));
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
const emailFeedback = container.querySelector('div[feedback-for="email"]');
@@ -393,7 +395,7 @@ describe('RegistrationPage', () => {
});
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
@@ -422,7 +424,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(
<RegistrationPage {...props} />,
<IntlRegistrationPage {...props} />,
)));
const emailInput = container.querySelector('input#email');
@@ -433,7 +435,7 @@ describe('RegistrationPage', () => {
// ******** test form buttons and fields ********
it('should match default button state', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span');
expect(button.textContent).toEqual('Create an account for free');
});
@@ -447,7 +449,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span.sr-only');
expect(button.textContent).toEqual('pending');
@@ -458,7 +460,7 @@ describe('RegistrationPage', () => {
MARKETING_EMAILS_OPT_IN: 'true',
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const checkboxDivs = container.querySelectorAll('div.form-field--checkbox');
expect(checkboxDivs.length).toEqual(1);
@@ -471,7 +473,7 @@ describe('RegistrationPage', () => {
const buttonLabel = 'Register';
delete window.location;
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
const button = container.querySelector('button[type="submit"] span');
const buttonText = button.textContent;
@@ -490,7 +492,7 @@ describe('RegistrationPage', () => {
},
});
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
});
@@ -508,7 +510,7 @@ describe('RegistrationPage', () => {
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardURL);
});
@@ -535,7 +537,7 @@ describe('RegistrationPage', () => {
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardUrl);
});
@@ -566,7 +568,7 @@ describe('RegistrationPage', () => {
render(reduxWrapper(
<Router>
<RegistrationPage {...props} />
<IntlRegistrationPage {...props} />
</Router>,
));
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
@@ -584,12 +586,12 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
});
it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
@@ -607,7 +609,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
});
@@ -633,7 +635,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(
<Router>
<RegistrationPage {...props} />
<IntlRegistrationPage {...props} />
</Router>,
));
@@ -656,7 +658,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const validationErrors = container.querySelector('div#validation-errors');
expect(validationErrors.textContent).toContain(
'An error has occurred. Try refreshing the page, or check your internet connection.',
@@ -683,7 +685,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const fullNameInput = container.querySelector('input#name');
const usernameInput = container.querySelector('input#username');
@@ -729,14 +731,14 @@ describe('RegistrationPage', () => {
},
},
});
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
});
it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
const usernameInput = container.querySelector('input#username');
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
@@ -764,7 +766,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(
<RegistrationPage {...props} />),
<IntlRegistrationPage {...props} />),
));
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
@@ -781,7 +783,7 @@ describe('RegistrationPage', () => {
search: '?host=http://localhost/host-website',
};
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
@@ -822,7 +824,7 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const spinnerElement = container.querySelector('#tpa-spinner');
const registrationFormElement = container.querySelector('#registration-form');
@@ -873,7 +875,7 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
name: 'John Doe',
username: 'john_doe',

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
@@ -31,13 +31,13 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
countries,
} = props;
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
@@ -70,6 +70,18 @@ const ConfigurableRegistrationForm = (props) => {
}
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
const removeDisabledCountries = useCallback((countryList) => {
if (!countries.length) {
return countryList;
}
const allowedCountries = new Set(countries.map(({ code }) => code));
return countryList.filter(({ code }) => allowedCountries.has(code));
}, [countries]);
const countryList = useMemo(() => removeDisabledCountries(
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []), [removeDisabledCountries]);
const handleErrorChange = (fieldName, error) => {
if (fieldName) {
setFieldErrors(prevErrors => ({
@@ -231,11 +243,16 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
countries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})),
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
countries: [],
};
export default ConfigurableRegistrationForm;

View File

@@ -9,7 +9,6 @@ import PropTypes from 'prop-types';
import { windowScrollTo } from '../../data/utils';
import {
FORBIDDEN_REQUEST,
FORBIDDEN_USERNAME,
INTERNAL_SERVER_ERROR,
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
@@ -49,9 +48,6 @@ const RegistrationFailureMessage = (props) => {
case TPA_SESSION_EXPIRED:
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
case FORBIDDEN_USERNAME:
errorMessage = formatMessage(messages['registration.forbidden.username']);
break;
default:
errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
break;

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import {
getLocale, IntlProvider,
getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -22,6 +23,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -155,7 +158,7 @@ describe('ConfigurableRegistrationForm', () => {
};
render(routerWrapper(reduxWrapper(
<ConfigurableRegistrationForm {...props} />,
<IntlConfigurableRegistrationForm {...props} />,
)));
expect(document.querySelector('#profession')).toBeTruthy();
@@ -182,10 +185,11 @@ describe('ConfigurableRegistrationForm', () => {
},
},
autoSubmitRegistrationForm: true,
countries: [{ code: 'AX', name: 'Åland Islands' }, { code: 'AL', name: 'Albania' }],
};
render(routerWrapper(reduxWrapper(
<ConfigurableRegistrationForm {...props} />,
<IntlConfigurableRegistrationForm {...props} />,
)));
expect(props.setFormFields).toHaveBeenCalledTimes(2);
@@ -212,7 +216,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy();
});
@@ -246,7 +250,7 @@ describe('ConfigurableRegistrationForm', () => {
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
@@ -281,7 +285,7 @@ describe('ConfigurableRegistrationForm', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
@@ -307,7 +311,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
@@ -332,7 +336,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const emailInput = getByLabelText('Email');
const confirmEmailInput = getByLabelText('Confirm Email');
@@ -368,7 +372,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change(
@@ -403,7 +407,7 @@ describe('ConfigurableRegistrationForm', () => {
});
const { getByLabelText, container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
);
const professionInput = getByLabelText('Profession');

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, IntlProvider,
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -23,6 +24,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -134,7 +137,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -150,7 +153,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -169,7 +172,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -188,7 +191,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -208,7 +211,7 @@ describe('RegistrationFailure', () => {
},
});
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
expect(validationError).not.toBeNull();

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, IntlProvider,
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -22,6 +23,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -155,7 +157,7 @@ describe('ThirdPartyAuth', () => {
});
const { queryByLabelText } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
);
const passwordField = queryByLabelText('Password');
@@ -180,7 +182,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
);
const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
@@ -205,7 +207,7 @@ describe('ThirdPartyAuth', () => {
search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
};
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const skeletonElement = container.querySelector('.react-loading-skeleton');
expect(skeletonElement).toBeTruthy();
@@ -229,7 +231,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null;
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
expect(iconElement).toBeTruthy();
@@ -252,7 +254,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl);
});
@@ -273,7 +275,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
expect(providerButton.textContent).toEqual(expectedMessage);
@@ -292,7 +294,7 @@ describe('ThirdPartyAuth', () => {
});
const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
);
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -313,7 +315,7 @@ describe('ThirdPartyAuth', () => {
});
const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
);
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -327,7 +329,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: true,
};
const { getByText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const headingElement = getByText('Register with institution/campus credentials');
expect(headingElement).toBeTruthy();
});
@@ -352,7 +354,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL };
const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
);
const ssoButton = container.querySelector('button#oa2-apple-id');
@@ -383,7 +385,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
@@ -404,7 +406,7 @@ describe('ThirdPartyAuth', () => {
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with '}${ getConfig().SITE_NAME }.`;
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const tpaAlert = container.querySelector('#tpa-alert p');
expect(tpaAlert.textContent).toEqual(expectedMessage);
});
@@ -435,7 +437,7 @@ describe('ThirdPartyAuth', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
);
const alertHeading = container.querySelector('div.alert-heading');

View File

@@ -11,4 +11,3 @@ export const FORM_SUBMISSION_ERROR = 'form-submission-error';
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
export const TPA_SESSION_EXPIRED = 'tpa-session-expired';
export const FORBIDDEN_USERNAME = 'forbidden-username';

View File

@@ -1,14 +1,11 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import {
call, put, race, take, takeEvery,
} from 'redux-saga/effects';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
fetchRealtimeValidationsBegin,
fetchRealtimeValidationsFailure,
fetchRealtimeValidationsSuccess,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
registerNewUserBegin,
@@ -44,15 +41,9 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
const { response } = yield race({
response: call(getFieldsValidations, action.payload.formPayload),
cancel: take(REGISTER_CLEAR_USERNAME_SUGGESTIONS),
});
if (response) {
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(response.fieldValidations)));
}
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());

View File

@@ -162,11 +162,6 @@ const messages = defineMessages({
defaultMessage: 'Registration using {provider} has timed out.',
description: '',
},
'registration.forbidden.username': {
id: 'registration.forbidden.username',
defaultMessage: 'Usernames can\'t include words that could be mistaken for course roles. Please choose a different username.',
description: '',
},
'registration.tpa.authentication.failure': {
id: 'registration.tpa.authentication.failure',
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -25,6 +26,7 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockReturnValue({ token }),
}));
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
const mockStore = configureStore();
describe('ResetPasswordPage', () => {
@@ -93,7 +95,7 @@ describe('ResetPasswordPage', () => {
}));
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const newPasswordInput = screen.getByLabelText('New password');
const confirmPasswordInput = screen.getByLabelText('Confirm password');
@@ -118,7 +120,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID,
},
});
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const resetPasswordButton = screen.getByRole('button', { name: /Reset password/i, id: 'submit-new-password' });
fireEvent.click(resetPasswordButton);
@@ -142,7 +144,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID,
},
});
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const confirmPasswordInput = screen.getByLabelText('Confirm password');
fireEvent.change(confirmPasswordInput, { target: { value: 'password-mismatch' } });
@@ -161,7 +163,7 @@ describe('ResetPasswordPage', () => {
},
});
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const rateLimitError = alertElements[0].textContent;
@@ -177,7 +179,7 @@ describe('ResetPasswordPage', () => {
},
});
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const internalServerError = alertElements[0].textContent;
expect(internalServerError).toBe(validationMessage);
@@ -186,7 +188,7 @@ describe('ResetPasswordPage', () => {
// ******** miscellaneous tests ********
it('should call validation on password field when blur event fires', () => {
const resetPasswordPage = render(reduxWrapper(<ResetPasswordPage {...props} />));
const resetPasswordPage = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const expectedText = 'Password criteria has not been metPassword must contain at least 8 characters, at least one letter, and at least one number';
const newPasswordInput = resetPasswordPage.container.querySelector('input#newPassword');
newPasswordInput.value = 'test-password';
@@ -205,7 +207,7 @@ describe('ResetPasswordPage', () => {
TOKEN_STATE.PENDING,
};
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(validateToken(token));
});
@@ -214,19 +216,19 @@ describe('ResetPasswordPage', () => {
status:
PASSWORD_RESET_ERROR,
};
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
});
it('should redirect the user to root url of the application ', async () => {
props = {
status: SUCCESS,
};
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
});
it('shows spinner during token validation', () => {
render(reduxWrapper(<ResetPasswordPage {...props} />));
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const spinnerElement = document.getElementsByClassName('div.spinner-header');
expect(spinnerElement).toBeTruthy();
@@ -235,7 +237,7 @@ describe('ResetPasswordPage', () => {
// ******** redirection tests ********
it('by clicking on sign in tab should redirect onto login page', async () => {
const { getByText } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const { getByText } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const signInTab = getByText('Sign in');

View File

@@ -2,19 +2,19 @@
.layout {
display: flex;
@media (--pgn-size-breakpoint-max-width-lg) {
@include media-breakpoint-down('lg') {
flex-direction: column;
justify-content: center;
align-items: center;
}
@media (--pgn-size-breakpoint-min-width-xl) {
@include media-breakpoint-up('xl') {
justify-content: space-between;
}
}
.content {
@media (--pgn-size-breakpoint-min-width-xl) {
@include media-breakpoint-up('xl') {
display: flex;
justify-content: center;
width: 50vw;
@@ -47,7 +47,7 @@
font-weight: 700;
line-height: 1;
@media (--pgn-size-breakpoint-max-width-xl) {
@include media-breakpoint-down('xl') {
font-size: 3.75rem;
}
@@ -60,7 +60,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@media (-pgn-size-breakpoint-max-width-xl) {
@include media-breakpoint-down('xl') {
font-size: 1.375rem;
line-height: 1.75rem;
}
@@ -72,7 +72,7 @@
}
.large-screen-left-container {
@media (-pgn-size-breakpoint-max-width-xl) {
@include media-breakpoint-down('xl') {
flex: 0 0 25%;
max-width: 25%;
}
@@ -87,43 +87,43 @@
height: 0.25rem;
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 20%,
var(--pgn-color-brand-base) 20%,
$brand-700,
$brand-700 20%,
$brand 20%,
);
background-repeat: no-repeat;
}
@media (--pgn-size-breakpoint-min-width-md) and (--pgn-size-breakpoint-max-width-md) {
@include media-breakpoint-only('md') {
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 90%,
var(--pgn-color-primary-700) 90%,
var(--pgn-color-primary-700) 100%,
$brand-700,
$brand-700 10%,
$brand 10%,
$brand 90%,
$primary-700 90%,
$primary-700 100%,
);
background-repeat: no-repeat;
}
}
@media (--pgn-size-breakpoint-min-width-lg) and (--pgn-size-breakpoint-max-width-lg){
@include media-breakpoint-only('lg') {
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 65%,
var(--pgn-color-primary-700) 65%,
var(--pgn-color-primary-700) 75%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-accent-a) 75%
$brand-700 10%,
$brand 10%,
$brand 65%,
$primary-700 65%,
$primary-700 75%,
$accent-a 75%,
$accent-a 75%
);
background-repeat: no-repeat;
}
@@ -131,20 +131,20 @@
.extra-large-screen-top-stripe { display: none; }
@media (--pgn-size-breakpoint-min-width-xl) {
@include media-breakpoint-up('xl') {
.extra-large-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 45%,
var(--pgn-color-primary-700) 45%,
var(--pgn-color-primary-700) 55%,
var(--pgn-color-accent-a) 55%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-info-200) 75%,
$brand-700 10%,
$brand 10%,
$brand 45%,
$primary-700 45%,
$primary-700 55%,
$accent-a 55%,
$accent-a 75%,
$info-200 75%,
);
background-repeat: no-repeat;
}
@@ -152,24 +152,24 @@
.large-screen-svg-light,
.large-screen-svg-primary {
fill: var(--pgn-color-light-200);
fill: $light-200;
overflow: hidden;
position: absolute;
}
.large-screen-svg-primary {
fill: var(--pgn-color-primary-400);
fill: $primary-400;
}
.medium-screen-svg-light,
.medium-screen-svg-primary {
fill: var(--pgn-color-light-200);
fill: $light-200;
overflow: inherit;
position: absolute;
}
.medium-screen-svg-primary {
fill: var(--pgn-color-primary-400);
fill: $primary-400;
}
[dir=rtl]{
@@ -184,20 +184,20 @@
.small-yellow-line {
width: 80px;
height: 0;
border: 2px solid var(--pgn-color-accent-b);
border: 2px solid $accent-b;
transform: rotate(102.02deg);
}
.medium-yellow-line {
width: 120px;
height: 0;
border: 3px solid var(--pgn-color-accent-b);
border: 3px solid $accent-b;
transform: rotate(102.02deg);
}
.large-yellow-line {
width: 240px;
height: 0;
border: 3px solid var(--pgn-color-accent-b);
border: 3px solid $accent-b;
transform: rotate(102.02deg);
}

View File

@@ -11,7 +11,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@media (--pgn-size-breakpoint-max-width-md) {
@include media-breakpoint-down('md') {
line-height: 1.5rem;
font-size: 1.125rem;
}

View File

@@ -64,52 +64,52 @@ $header-height: 104px;
}
&.light {
background-color: var(--pgn-color-white);
background-color: $white;
.title {
color: var(--pgn-color-black);
color: $black;
}
.subtitle {
color: var(--pgn-color-gray-700);
color: $gray-700;
}
.badge {
background-color: var(--pgn-color-light-500);
color: var(--pgn-color-black);
background-color: $light-500;
color: $black;
}
.footer-content {
color: var(--pgn-color-gray-700);
color: $gray-700;
}
}
&.dark {
background-color: var(--pgn-color-primary-500);
background-color: $primary-500;
.pgn__card-header-title-md {
color: var(--pgn-color-white);
color: $white;
}
.pgn__card-header-subtitle-md {
color: var(--pgn-color-light-200);
color: $light-200;
}
.title {
color: var(--pgn-color-white);
color: $white;
}
.subtitle {
color: var(--pgn-color-light-200);
color: $light-200;
}
.badge {
background-color: var(--pgn-color-dark-200);
color: var(--pgn-color-white);
background-color: $dark-200;
color: $white;
}
.footer-content {
color: var(--pgn-color-light-200);
color: $light-200;
}
}
}

View File

@@ -3,7 +3,7 @@ $card-gap: 24px;
.recommendations-container__card-list {
gap: $card-gap $card-gap;
@media (-pgn-size-breakpoint-max-width-sm) {
@include media-breakpoint-down(sm) {
margin-bottom: 0 !important;
}
@@ -11,15 +11,15 @@ $card-gap: 24px;
flex: 0 1 100%;
cursor: pointer;
@media (--pgn-size-breakpoint-min-width-sm) {
@include media-breakpoint-up(sm) {
flex: 0 1 calc(50% - #{$card-gap - 12});
}
@media (--pgn-size-breakpoint-min-width-md) {
@include media-breakpoint-up(md) {
flex: 0 1 calc(33.333% - #{$card-gap - 8});
}
@media (--pgn-size-breakpoint-min-width-lg) {
@include media-breakpoint-up(lg) {
flex: 0 1 calc(25% - #{$card-gap - 6});
}
}

View File

@@ -23,21 +23,21 @@
}
.alert-link {
color: var(--pgn-color-primary-base) !important;
color: $primary !important;
&:hover {
text-decoration: underline;
color: var(--pgn-color-info-700) !important;
color: $info-700 !important;
}
}
}
.email-suggestion-alert-warning {
color: var(--pgn-color-info-500) !important;
color: $info-500 !important;
&:hover {
text-decoration: underline;
color: var(--pgn-color-info-700) !important;
color: $info-700 !important;
}
}
@@ -56,7 +56,7 @@
line-height: 24px;
font-size: 12px;
font-weight: normal;
color: var(--pgn-color-primary-700);
color: $primary-700;
}
.username-suggestion--label {
@@ -65,15 +65,10 @@
margin-right: 0.25rem;
}
.username__form-group-wrapper {
position: relative;
}
.username-suggestions {
position: absolute;
inset: 0;
padding-left: 15px;
z-index: 100;
position: relative;
margin-top: -2.5rem;
margin-left: 15px;
}
.username-suggestions__close__button {
@@ -81,6 +76,13 @@
position: absolute;
}
.username-suggestions__error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.username-scroll-suggested--form-field {
width: 20rem;
white-space: nowrap;
@@ -97,7 +99,7 @@
}
}
@media (--pgn-size-breakpoint-max-width-sm) {
@media (max-width: map-get($grid-breakpoints, "sm")) {
.username-scroll-suggested--form-field {
width: 15rem;
}

View File

@@ -40,7 +40,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.main-content {
padding-top: calc(var(--pgn-spacing-spacer-base) * 1.5) !important;
@extend .pt-4;
min-width: 464px !important;
}
@@ -80,15 +80,15 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
.alert-link {
font-weight: normal;
text-decoration: underline;
color: var(--pgn-color-info-300) !important;
color: $info-300 !important;
&:hover {
color: var(--pgn-color-info-500) !important;
color: $info-500 !important;
}
}
.form-control {
background-color: var(--pgn-color-white) !important;
background-color: $white !important;
font-size: 0.875rem;
line-height: 1.5;
height: 2.75rem;
@@ -103,11 +103,11 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
margin-bottom: 1rem;
font-size: 14px;
background-color: var(--pgn-color-white);
border: 1px solid var(--pgn-color-primary-base);
background-color: $white;
border: 1px solid $primary;
width: 224px;
height: 36px;
color: var(--pgn-color-primary-base);
color: $primary;
.btn-tpa__image-icon{
background-color: transparent;
@@ -132,8 +132,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-tpa__font-container {
background-color: var(--pgn-color-primary-base);
color: var(--pgn-color-white);
background-color: $primary;
color: $white;
font-size: 11px;
margin-left: -6px;
@@ -143,7 +143,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-oa2-facebook {
color: var(--pgn-color-white);
color: $white;
border-color: $facebook-blue;
background-color: $facebook-blue;
@@ -151,12 +151,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $facebook-focus-blue;
border: 1px solid $facebook-focus-blue;
color: var(--pgn-color-white);
color: $white;
}
}
.btn-oa2-google-oauth2 {
color: var(--pgn-color-white);
color: $white;
border-color: $google-blue;
background-color: $google-blue;
@@ -171,12 +171,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $google-focus-blue;
border: 1px solid $google-focus-blue;
color: var(--pgn-color-white);
color: $white;
}
}
.btn-oa2-apple-id {
color: var(--pgn-color-white);
color: $white;
border-color: $apple-black;
background-color: $apple-black;
font-size: 16px;
@@ -190,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $apple-focus-black;
border: 1px solid $apple-focus-black;
color: var(--pgn-color-white);
color: $white;
}
}
.btn-oa2-azuread-oauth2 {
color: var(--pgn-color-white);
color: $white;
border-color: $microsoft-black;
background-color: $microsoft-black;
@@ -203,7 +203,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $microsoft-focus-black;
border: 1px solid $microsoft-focus-black;
color: var(--pgn-color-white);
color: $white;
}
}
@@ -214,8 +214,9 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.institute-icon {
margin: calc(var(--pgn-spacing-spacer-base) * 0.25) !important;
color: var(--pgn-color-gray-base) !important;
@extend .mr-1;
@extend .text-gray;
display: inline-block;
margin-bottom: 0.25rem;
height: 18px;
@@ -231,7 +232,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.invalid-feedback {
color: var(--pgn-color-red);
color: $red;
}
.full-vertical-height {
@@ -289,22 +290,22 @@ select.form-control {
#password-requirement-left {
opacity: 1;
font-size: var(--pgn-typography-font-size-xs) !important;
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
right: 0.2rem !important;
.tooltip-inner {
background: var(--pgn-color-white);
background: $white;
display: block;
color: var(--pgn-color-gray-500);
color: $gray-500;
}
.arrow::before {
border-left-color: var(--pgn-color-white);
border-left-color: $white;
}
}
#password-requirement-top {
font-size: var(--pgn-typography-font-size-xs) !important;
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important;
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
opacity: 1;
width: 90%;
bottom: 10px !important;
@@ -313,30 +314,30 @@ select.form-control {
.tooltip-inner {
min-width: 464px !important;
background: var(--pgn-color-white);
background: $white;
display: block;
color: var(--pgn-color-gray-500);
color: $gray-500;
}
.arrow::before {
border-top-color: var(--pgn-color-white);
border-top-color: $white;
}
}
.yellow-border {
border: 2px solid var(--pgn-color-accent-b);
border: 2px solid $accent-b;
}
.institutions__heading {
color: var(--pgn-color-primary-700);
color: $primary-700;
}
.logistration-button {
color: var(--pgn-color-gray-700);
color: $gray-700;
}
.logistration-button:hover{
color: var(--pgn-color-gray-700);
color: $gray-700;
text-decoration: none;
}
@@ -351,7 +352,7 @@ select.form-control {
width: 2.3rem;
}
.has-floating-label {
color: var(--pgn-color-gray-500);
color: $gray-500;
}
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
@@ -365,7 +366,7 @@ select.form-control {
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 16px;
color: var(--pgn-color-primary-700);
color: $primary-700;
}
.form-group__form-field .form-control:not([value='']):not(:focus) ~
@@ -443,14 +444,14 @@ select.form-control {
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--pgn-color-light-200);
background-color: $light-200;
}
.institutions--provider-link {
font-weight: normal;
font-size: 0.875rem;
line-height: 1.5rem;
color: var(--pgn-color-primary-700)
color: $primary-700
}
.pgn__form-control-decorator-trailing {