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:
leangseu-edx
2022-08-02 13:28:50 -04:00
committed by GitHub
parent 279ff18e49
commit 776c6989bd
14 changed files with 429 additions and 24 deletions

View File

@@ -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);

View File

@@ -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.",

View File

@@ -0,0 +1,3 @@
.confirm-email-now-button {
text-decoration: underline !important;
}

View File

@@ -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`] = `""`;

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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;

View File

@@ -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();
});
});
});

View File

@@ -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;

View File

@@ -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}>

View File

@@ -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,
});

View File

@@ -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', () => ({

View File

@@ -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 });
}
}