leangseu edx/confirm email banner (#4)
* chore: working ui for confirm email * test: update unit test * chore: implement get values for state test * chore: update formatMessage test to support more than primitive variable
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn(),
|
||||
}));
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
@@ -37,15 +33,6 @@ const render = (overrides = {}) => {
|
||||
};
|
||||
|
||||
describe('EntitlementBanner', () => {
|
||||
beforeEach(() => {
|
||||
useIntl.mockReturnValue({
|
||||
formatDate: (date) => date,
|
||||
formatMessage: (message, values) => <div {...{ message, values }} />,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('initializes data with course number from entitlements', () => {
|
||||
render();
|
||||
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`EntitlementBanner snapshot: expiration warning 1`] = `
|
||||
<Banner>
|
||||
<div
|
||||
<formatMessageFunction
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
|
||||
@@ -18,15 +18,7 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
<div
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "select a session",
|
||||
"description": "Entitlements session selection link text",
|
||||
"id": "learner-dash.courseCard.banners.selectSession",
|
||||
}
|
||||
}
|
||||
/>
|
||||
select a session
|
||||
</Button>,
|
||||
}
|
||||
}
|
||||
@@ -38,7 +30,7 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = `
|
||||
<Banner
|
||||
variant="warning"
|
||||
>
|
||||
<div
|
||||
<formatMessageFunction
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.confirm-email-now-button {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
|
||||
<Fragment>
|
||||
<PageBanner
|
||||
dismissible={true}
|
||||
onDismiss={[MockFunction closePageBanner]}
|
||||
show={[MockFunction showPageBanner]}
|
||||
>
|
||||
<formatMessageFunction
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Remember to confirm your email so that you can keep learning on edX! {confirmNowButton}.",
|
||||
"description": "Text for reminding user to confirm email",
|
||||
"id": "leanerDashboard.confirmEmailTextReminderBanner",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"confirmNowButton": <Button
|
||||
className="confirm-email-now-button"
|
||||
onClick={[MockFunction openConfirmModalButtonClick]}
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
Confirm Now
|
||||
</Button>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</PageBanner>
|
||||
<MarketingModal
|
||||
footerNode={
|
||||
<Button
|
||||
className="mx-auto my-3"
|
||||
onClick={[MockFunction userConfirmEmailButtonClick]}
|
||||
variant="danger"
|
||||
>
|
||||
I've confirmed my email
|
||||
</Button>
|
||||
}
|
||||
hasCloseButton={false}
|
||||
heroNode={
|
||||
<ModalDialog.Hero
|
||||
className="bg-gray-300"
|
||||
>
|
||||
<img
|
||||
alt="confirm email background"
|
||||
className="m-auto"
|
||||
src="confirm-email.svg"
|
||||
/>
|
||||
</ModalDialog.Hero>
|
||||
}
|
||||
isOpen={[MockFunction showConfirmModal]}
|
||||
onClose={[MockFunction closeConfirmModal]}
|
||||
title=""
|
||||
>
|
||||
<h1
|
||||
className="text-center p-3"
|
||||
>
|
||||
Confirm your email
|
||||
</h1>
|
||||
<p
|
||||
className="text-center"
|
||||
>
|
||||
We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.
|
||||
</p>
|
||||
</MarketingModal>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`ConfirmEmailBanner snapshot do not show on already verified 1`] = `""`;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { hooks as appHooks, thunkActions } from 'data/redux';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
showPageBanner: (val) => React.useState(val), // eslint-disable-line
|
||||
showConfirmModal: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useConfirmEmailBannerData = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { isNeeded } = appHooks.useEmailConfirmationData();
|
||||
const [showPageBanner, setShowPageBanner] = module.state.showPageBanner(isNeeded);
|
||||
const [showConfirmModal, setShowConfirmModal] = module.state.showConfirmModal(false);
|
||||
const closePageBanner = () => setShowPageBanner(false);
|
||||
const closeConfirmModal = () => setShowConfirmModal(false);
|
||||
const openConfirmModal = () => setShowConfirmModal(true);
|
||||
|
||||
const openConfirmModalButtonClick = () => {
|
||||
dispatch(thunkActions.app.sendConfirmEmail());
|
||||
openConfirmModal();
|
||||
};
|
||||
|
||||
const userConfirmEmailButtonClick = () => {
|
||||
closeConfirmModal();
|
||||
closePageBanner();
|
||||
};
|
||||
return {
|
||||
isNeeded,
|
||||
showPageBanner,
|
||||
closePageBanner,
|
||||
showConfirmModal,
|
||||
closeConfirmModal,
|
||||
openConfirmModalButtonClick,
|
||||
userConfirmEmailButtonClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useConfirmEmailBannerData;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { hooks as appHooks, thunkActions } from 'data/redux';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useEmailConfirmationData: jest.fn(),
|
||||
},
|
||||
thunkActions: {
|
||||
app: {
|
||||
sendConfirmEmail: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const emailConfirmation = {
|
||||
isNeeded: true,
|
||||
};
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('ConfirmEmailBanner hooks', () => {
|
||||
let out;
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.showPageBanner);
|
||||
state.testGetter(state.keys.showConfirmModal);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('useEmailConfirmationData', () => {
|
||||
beforeEach(() => state.mock());
|
||||
afterEach(state.restore);
|
||||
|
||||
test('show page banner on unverified email', () => {
|
||||
appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
|
||||
out = hooks.useConfirmEmailBannerData();
|
||||
expect(out.isNeeded).toEqual(emailConfirmation.isNeeded);
|
||||
appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
|
||||
});
|
||||
|
||||
test('hide page banner on verified email', () => {
|
||||
appHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
|
||||
out = hooks.useConfirmEmailBannerData();
|
||||
expect(out.isNeeded).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
appHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
|
||||
out = hooks.useConfirmEmailBannerData();
|
||||
});
|
||||
afterEach(state.restore);
|
||||
test('closePageBanner', () => {
|
||||
out.closePageBanner();
|
||||
expect(state.values.showPageBanner).toEqual(false);
|
||||
});
|
||||
test('closeConfirmModal', () => {
|
||||
out.closeConfirmModal();
|
||||
expect(state.values.showConfirmModal).toEqual(false);
|
||||
});
|
||||
test('openConfirmModalButtonClick', () => {
|
||||
out.openConfirmModalButtonClick();
|
||||
expect(state.values.showConfirmModal).toEqual(true);
|
||||
expect(thunkActions.app.sendConfirmEmail).toBeCalled();
|
||||
});
|
||||
test('userConfirmEmailButtonClick', () => {
|
||||
out.userConfirmEmailButtonClick();
|
||||
expect(state.values.showConfirmModal).toEqual(false);
|
||||
expect(state.values.showPageBanner).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import {
|
||||
PageBanner, ModalDialog, MarketingModal, Button,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import './ConfirmEmailBanner.scss';
|
||||
import useConfirmEmailBannerData from './hooks';
|
||||
|
||||
export const ConfirmEmailBanner = () => {
|
||||
const {
|
||||
isNeeded,
|
||||
showConfirmModal,
|
||||
showPageBanner,
|
||||
closePageBanner,
|
||||
closeConfirmModal,
|
||||
openConfirmModalButtonClick,
|
||||
userConfirmEmailButtonClick,
|
||||
} = useConfirmEmailBannerData();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (!isNeeded) { return null; }
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageBanner show={showPageBanner} dismissible onDismiss={closePageBanner}>
|
||||
{formatMessage(messages.confirmEmailTextReminderBanner, {
|
||||
confirmNowButton: (
|
||||
<Button
|
||||
className="confirm-email-now-button"
|
||||
variant="link"
|
||||
size="inline"
|
||||
onClick={openConfirmModalButtonClick}
|
||||
>
|
||||
{formatMessage(messages.confirmNowButton)}
|
||||
</Button>
|
||||
),
|
||||
})}
|
||||
</PageBanner>
|
||||
<MarketingModal
|
||||
title=""
|
||||
isOpen={showConfirmModal}
|
||||
onClose={closeConfirmModal}
|
||||
hasCloseButton={false}
|
||||
heroNode={(
|
||||
<ModalDialog.Hero className="bg-gray-300">
|
||||
<img className="m-auto" src="confirm-email.svg" alt={formatMessage(messages.confirmEmailImageAlt)} />
|
||||
</ModalDialog.Hero>
|
||||
)}
|
||||
footerNode={(
|
||||
<Button className="mx-auto my-3" variant="danger" onClick={userConfirmEmailButtonClick}>
|
||||
{formatMessage(messages.verifiedConfirmEmailButton)}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<h1 className="text-center p-3">{formatMessage(messages.confirmEmailModalHeader)}</h1>
|
||||
<p className="text-center">{formatMessage(messages.confirmEmailModalBody)}</p>
|
||||
</MarketingModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
ConfirmEmailBanner.propTypes = {};
|
||||
|
||||
export default ConfirmEmailBanner;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import hooks from './hooks';
|
||||
import ConfirmEmailBanner from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const hookProps = {
|
||||
isNeeded: true,
|
||||
showPageBanner: jest.fn().mockName('showPageBanner'),
|
||||
closePageBanner: jest.fn().mockName('closePageBanner'),
|
||||
showConfirmModal: jest.fn().mockName('showConfirmModal'),
|
||||
closeConfirmModal: jest.fn().mockName('closeConfirmModal'),
|
||||
openConfirmModalButtonClick: jest.fn().mockName('openConfirmModalButtonClick'),
|
||||
userConfirmEmailButtonClick: jest.fn().mockName('userConfirmEmailButtonClick'),
|
||||
};
|
||||
|
||||
describe('ConfirmEmailBanner', () => {
|
||||
describe('snapshot', () => {
|
||||
test('do not show on already verified', () => {
|
||||
hooks.mockReturnValueOnce({ ...hookProps, isNeeded: false });
|
||||
const el = shallow(<ConfirmEmailBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Show on unverified', () => {
|
||||
hooks.mockReturnValueOnce({ ...hookProps });
|
||||
const el = shallow(<ConfirmEmailBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
confirmNowButton: {
|
||||
id: 'leanerDashboard.confirmEmailBanner',
|
||||
description: 'Button for sending confirm email and open modal',
|
||||
defaultMessage: 'Confirm Now',
|
||||
},
|
||||
confirmEmailTextReminderBanner: {
|
||||
id: 'leanerDashboard.confirmEmailTextReminderBanner',
|
||||
description: 'Text for reminding user to confirm email',
|
||||
defaultMessage: 'Remember to confirm your email so that you can keep learning on edX! {confirmNowButton}.',
|
||||
},
|
||||
verifiedConfirmEmailButton: {
|
||||
id: 'leanerDashboard.verifiedConfirmEmailButton',
|
||||
description: 'Button for verified confirming email',
|
||||
defaultMessage: 'I\'ve confirmed my email',
|
||||
},
|
||||
confirmEmailModalHeader: {
|
||||
id: 'leanerDashboard.confirmEmailModalHeader',
|
||||
description: 'title for confirming email modal',
|
||||
defaultMessage: 'Confirm your email',
|
||||
},
|
||||
confirmEmailModalBody: {
|
||||
id: 'leanerDashboard.confirmEmailModalBody',
|
||||
description: 'text hint for confirming email modal',
|
||||
defaultMessage: 'We\'ve sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.',
|
||||
},
|
||||
confirmEmailImageAlt: {
|
||||
id: 'leanerDashboard.confirmEmailImageAlt',
|
||||
description: 'text alt confirm email image',
|
||||
defaultMessage: 'confirm email background',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -8,12 +8,14 @@ import { Button } from '@edx/paragon';
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import GreetingBanner from './GreetingBanner';
|
||||
import messages from './messages';
|
||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div className="d-flex flex-column bg-primary">
|
||||
<ConfirmEmailBanner />
|
||||
<header className="learner-dashboard-header">
|
||||
<div className="d-flex">
|
||||
<Button variant="inverse-tertiary" iconBefore={Program}>
|
||||
|
||||
@@ -30,7 +30,11 @@ export const refreshList = () => (dispatch) => (
|
||||
}))
|
||||
);
|
||||
|
||||
// TODO: connect hook to actual api later
|
||||
export const sendConfirmEmail = () => () => console.log('send confirm email');
|
||||
|
||||
export default StrictDict({
|
||||
initialize,
|
||||
refreshList,
|
||||
sendConfirmEmail,
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Heading: 'Alert.Heading',
|
||||
},
|
||||
AlertModal: 'AlertModal',
|
||||
MarketingModal: 'MarketingModal',
|
||||
ActionRow: 'ActionRow',
|
||||
Badge: 'Badge',
|
||||
Button: 'Button',
|
||||
@@ -90,6 +91,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
ModalDialog: {
|
||||
Header: 'ModalDialog.Header',
|
||||
Body: 'ModalDialog.Body',
|
||||
Hero: 'ModalDialog.Hero',
|
||||
},
|
||||
MultiSelectDropdownFilter: 'MultiSelectDropdownFilter',
|
||||
OverlayTrigger: 'OverlayTrigger',
|
||||
@@ -100,6 +102,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
StatefulButton: 'StatefulButton',
|
||||
TextFilter: 'TextFilter',
|
||||
Spinner: 'Spinner',
|
||||
PageBanner: 'PageBanner',
|
||||
}));
|
||||
|
||||
jest.mock('@fortawesome/react-fontawesome', () => ({
|
||||
|
||||
@@ -10,6 +10,11 @@ export const formatMessage = (msg, values) => {
|
||||
if (values === undefined) {
|
||||
return message;
|
||||
}
|
||||
// check if value is not a primitive type.
|
||||
if (Object.values(values).filter(value => Object(value) === value).length) {
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
return <formatMessageFunction {...{ message: msg, values }} />;
|
||||
}
|
||||
Object.keys(values).forEach((key) => {
|
||||
// eslint-disable-next-line
|
||||
message = message.replace(`{${key}}`, values[key]);
|
||||
@@ -188,4 +193,8 @@ export class MockUseState {
|
||||
expect(this.hooks.state[key](testValue)).toEqual(useState(testValue));
|
||||
});
|
||||
}
|
||||
|
||||
get values() {
|
||||
return StrictDict({ ...this.hooks.state });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user