From 200f19dd9a1bac0eeaf08b6b673e0ce03b1443e9 Mon Sep 17 00:00:00 2001 From: Thomas Tracy Date: Wed, 4 May 2022 12:49:02 -0400 Subject: [PATCH] feat: [MICROBA-1528] add schedule email UI (#36) This handles a few things around the scheduled email UI. This includes: 1. Adding the schedule email UI date/time picker. 2. Adds states to the submit button for scheduling emails. 3. Drys out intl code and some submitting states. 4. Matches the email form UI to mocks. This however does not include: 1. A table to show scheduled emails. Scheduled emails at the moment are displayed in the "pending tasks" section. 2. Matching the tasks section to the mocks. --- .env | 1 + .env.development | 1 + .../bulk-email-tool/BulkEmailTool.jsx | 9 +- .../bulk-email-form/BulkEmailForm.jsx | 256 ++++++++++-------- .../bulk-email-form/ScheduleEmailForm.jsx | 80 ++++++ .../bulk-email-form/messages.js | 89 ++++++ .../test/BulkEmailForm.test.jsx | 72 +++-- .../BulkEmailTaskManager.jsx | 2 +- .../test/BulkEmailContentHistory.test.jsx | 5 +- .../test/BulkEmailPendingTasks.test.jsx | 5 +- .../test/BulkEmailTaskHistory.test.jsx | 5 +- .../test/BulkEmailTool.test.jsx | 7 +- .../test/PageContainer.test.jsx | 5 +- src/index.jsx | 19 +- src/index.scss | 9 +- src/setupTest.js | 57 ++-- 16 files changed, 435 insertions(+), 187 deletions(-) create mode 100644 src/components/bulk-email-tool/bulk-email-form/ScheduleEmailForm.jsx create mode 100644 src/components/bulk-email-tool/bulk-email-form/messages.js diff --git a/.env b/.env index 805961d..17a9b2f 100644 --- a/.env +++ b/.env @@ -18,3 +18,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='' SEGMENT_KEY='' SITE_NAME='' USER_INFO_COOKIE_NAME='' +SCHEDULE_EMAIL_SECTION='' diff --git a/.env.development b/.env.development index 718497b..4ddc0f9 100644 --- a/.env.development +++ b/.env.development @@ -19,3 +19,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' SEGMENT_KEY='' SITE_NAME=localhost USER_INFO_COOKIE_NAME='edx-user-info' +SCHEDULE_EMAIL_SECTION='true' diff --git a/src/components/bulk-email-tool/BulkEmailTool.jsx b/src/components/bulk-email-tool/BulkEmailTool.jsx index b908f65..cf6eda3 100644 --- a/src/components/bulk-email-tool/BulkEmailTool.jsx +++ b/src/components/bulk-email-tool/BulkEmailTool.jsx @@ -1,21 +1,16 @@ import React, { useRef } from 'react'; -import classnames from 'classnames'; import { useParams } from 'react-router-dom'; import { ErrorPage } from '@edx/frontend-platform/react'; import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager'; import NavigationTabs from '../navigation-tabs/NavigationTabs'; -import useMobileResponsive from '../../utils/useMobileResponsive'; import BulkEmailForm from './bulk-email-form'; import { CourseMetadataContext } from '../page-container/PageContainer'; export default function BulkEmailTool() { const { courseId } = useParams(); - - const isMobile = useMobileResponsive(); const textEditorRef = useRef(); - const copyTextToEditor = (body) => { if (textEditorRef?.current) { textEditorRef.current.setContent(body); @@ -27,11 +22,11 @@ export default function BulkEmailTool() { {(courseMetadata) => (courseMetadata.isStaff ? (
-
+
-
+
diff --git a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx index 0aa920b..4c41a18 100644 --- a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx +++ b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx @@ -1,39 +1,57 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Form, Icon, StatefulButton, useCheckboxSetValues, useToggle, } from '@edx/paragon'; -import { SpinnerSimple, CheckCircle, Cancel } from '@edx/paragon/icons'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + SpinnerSimple, Cancel, Send, Event, Check, +} from '@edx/paragon/icons'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import classNames from 'classnames'; +import { getConfig } from '@edx/frontend-platform'; import TextEditor from '../text-editor/TextEditor'; import { postBulkEmail } from './api'; import BulkEmailRecipient from './bulk-email-recipient'; import TaskAlertModal from './TaskAlertModal'; import useTimeout from '../../../utils/useTimeout'; +import useMobileResponsive from '../../../utils/useMobileResponsive'; +import ScheduleEmailForm from './ScheduleEmailForm'; +import messages from './messages'; export const FORM_SUBMIT_STATES = { DEFAULT: 'default', PENDING: 'pending', COMPLETE: 'complete', - COMPLETED_DEFAULT: 'completed_default', + COMPLETE_SCHEDULE: 'completed_schedule', + SCHEDULE: 'schedule', ERROR: 'error', }; -export default function BulkEmailForm(props) { - const { courseId, cohorts, editorRef } = props; +function BulkEmailForm(props) { + const { + courseId, cohorts, editorRef, intl, + } = props; const [subject, setSubject] = useState(''); + const [dateTime, setDateTime] = useState({ scheduleDate: '', scheduleTime: '' }); const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT); const [emailFormValidation, setEmailFormValidation] = useState({ // set these as true on initialization, to prevent invalid messages from prematurely showing subject: true, body: true, recipients: true, + schedule: true, }); const [selectedRecipients, { add, remove }] = useCheckboxSetValues([]); const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false); + const [isScheduled, toggleScheduled] = useState(false); const resetEmailForm = useTimeout(() => { - setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETED_DEFAULT); + if (isScheduled) { + setEmailFormStatus(FORM_SUBMIT_STATES.SCHEDULE); + } else { + setEmailFormStatus(FORM_SUBMIT_STATES.DEFAULT); + } }, 3000); + const isMobile = useMobileResponsive(); const onRecipientChange = (event) => { if (event.target.checked) { @@ -47,19 +65,31 @@ export default function BulkEmailForm(props) { }; const onSubjectChange = (event) => setSubject(event.target.value); + const onDateTimeChange = (event) => { + const next = { [event.target.name]: event.target.value }; + setDateTime((prev) => ({ ...prev, ...next })); + }; + + const validateDateTime = (date, time) => { + if (isScheduled) { + return !!date && !!time; + } + return true; + }; const validateEmailForm = () => { const subjectValid = subject.length !== 0; const bodyValid = editorRef.current.getContent().length !== 0; const recipientsValid = selectedRecipients.length !== 0; - + const { scheduleDate, scheduleTime } = dateTime; + const scheduleValid = validateDateTime(scheduleDate, scheduleTime); setEmailFormValidation({ subject: subjectValid, recipients: recipientsValid, body: bodyValid, + schedule: scheduleValid, }); - - return subjectValid && bodyValid && recipientsValid; + return subjectValid && bodyValid && recipientsValid && scheduleValid; }; const createEmailTask = async () => { @@ -70,6 +100,11 @@ export default function BulkEmailForm(props) { emailData.append('send_to', JSON.stringify(selectedRecipients)); emailData.append('subject', subject); emailData.append('message', editorRef.current.getContent()); + if (isScheduled) { + const { scheduleDate, scheduleTime } = dateTime; + emailData.append('schedule', new Date(`${scheduleDate} ${scheduleTime}`).toISOString()); + emailData.append('browser_timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); + } let data; try { data = await postBulkEmail(emailData, courseId); @@ -78,33 +113,42 @@ export default function BulkEmailForm(props) { return; } if (data.status === 200) { - setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE); + if (isScheduled) { + setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE_SCHEDULE); + } else { + setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE); + } resetEmailForm(); } } }; + useEffect(() => { + if (isScheduled) { + setEmailFormStatus(FORM_SUBMIT_STATES.SCHEDULE); + } else { + setEmailFormStatus(FORM_SUBMIT_STATES.DEFAULT); + } + }, [isScheduled]); + return ( -
+
-

- -

+

{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject })}

    {selectedRecipients.map((group) => (
  • {group}
  • ))}
+ {!isScheduled && ( +

+ {intl.formatMessage(messages.bulkEmailInstructionsCaution)} + {intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)} +

+ )} )} close={(event) => { @@ -115,13 +159,7 @@ export default function BulkEmailForm(props) { }} />
-

- -

+

{intl.formatMessage(messages.bulkEmailToolLabel)}

- - - + {intl.formatMessage(messages.bulkEmailSubjectLabel)} {!emailFormValidation.subject && ( - + {intl.formatMessage(messages.bulkEmailFormSubjectError)} )} - - - + {intl.formatMessage(messages.bulkEmailBodyLabel)} {!emailFormValidation.body && ( - + {intl.formatMessage(messages.bulkEmailFormBodyError)} )}
-

- -

-

- - - - -

+

{intl.formatMessage(messages.bulkEmailInstructionsProofreading)}

- - { - event.preventDefault(); - openTaskAlert(); - }} - state={emailFormStatus} - icons={{ - default: , - pending: , - complete: , - error: , - }} - labels={{ - default: 'Submit', - pending: 'Submitting', - complete: 'Task Created', - error: 'Error', - }} - disabledStates={['pending', 'complete']} - > - + {getConfig().SCHEDULE_EMAIL_SECTION && ( +
+ toggleScheduled((prev) => !prev)} + disabled={emailFormStatus === FORM_SUBMIT_STATES.PENDING} + > + {intl.formatMessage(messages.bulkEmailFormScheduleBox)} + +
+ )} + {isScheduled && ( + -
- {emailFormStatus === FORM_SUBMIT_STATES.ERROR && ( - - - - )} - {(emailFormStatus === FORM_SUBMIT_STATES.COMPLETED_DEFAULT - || emailFormStatus === FORM_SUBMIT_STATES.COMPLETE) && ( - - - )} +
+ { + event.preventDefault(); + openTaskAlert(); + }} + state={emailFormStatus} + icons={{ + [FORM_SUBMIT_STATES.DEFAULT]: , + [FORM_SUBMIT_STATES.SCHEDULE]: , + [FORM_SUBMIT_STATES.PENDING]: , + [FORM_SUBMIT_STATES.COMPLETE]: , + [FORM_SUBMIT_STATES.COMPLETE_SCHEDULE]: , + [FORM_SUBMIT_STATES.ERROR]: , + }} + labels={{ + [FORM_SUBMIT_STATES.DEFAULT]: intl.formatMessage(messages.bulkEmailSubmitButtonDefault), + [FORM_SUBMIT_STATES.SCHEDULE]: intl.formatMessage(messages.bulkEmailSubmitButtonSchedule), + [FORM_SUBMIT_STATES.PENDING]: intl.formatMessage(messages.bulkEmailSubmitButtonPending), + [FORM_SUBMIT_STATES.COMPLETE]: intl.formatMessage(messages.bulkEmailSubmitButtonComplete), + [FORM_SUBMIT_STATES.COMPLETE_SCHEDULE]: intl.formatMessage( + messages.bulkEmailSubmitButtonCompleteSchedule, + ), + [FORM_SUBMIT_STATES.ERROR]: intl.formatMessage(messages.bulkEmailSubmitButtonError), + }} + disabledStates={[ + FORM_SUBMIT_STATES.PENDING, + FORM_SUBMIT_STATES.COMPLETE, + FORM_SUBMIT_STATES.COMPLETE_SCHEDULE, + ]} + /> + {emailFormStatus === FORM_SUBMIT_STATES.ERROR && ( + + {intl.formatMessage(messages.bulkEmailFormError)} + + )} +
@@ -251,4 +266,7 @@ BulkEmailForm.propTypes = { cohorts: PropTypes.arrayOf(PropTypes.string), editorRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]) .isRequired, + intl: intlShape.isRequired, }; + +export default injectIntl(BulkEmailForm); diff --git a/src/components/bulk-email-tool/bulk-email-form/ScheduleEmailForm.jsx b/src/components/bulk-email-tool/bulk-email-form/ScheduleEmailForm.jsx new file mode 100644 index 0000000..bcd73e2 --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/ScheduleEmailForm.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Form, Icon } from '@edx/paragon'; +import { Event, WatchOutline } from '@edx/paragon/icons'; +import useMobileResponsive from '../../../utils/useMobileResponsive'; + +function ScheduleEmailForm(props) { + const isMobile = useMobileResponsive(); + const { isValid, onDateTimeChange, dateTime } = props; + const { scheduleDate, scheduleTime } = dateTime; + return ( + +
+
+ + + + } + name="scheduleDate" + data-testid="scheduleDate" + onChange={onDateTimeChange} + value={scheduleDate} + /> +
+
+ + + + } + name="scheduleTime" + data-testid="scheduleTime" + onChange={onDateTimeChange} + value={scheduleTime} + /> +
+
+ {!isValid && ( + + + + )} +
+ ); +} + +ScheduleEmailForm.defaultProps = { + dateTime: { + scheduleDate: '', + scheduleTime: '', + }, +}; + +ScheduleEmailForm.propTypes = { + isValid: PropTypes.bool.isRequired, + onDateTimeChange: PropTypes.func.isRequired, + dateTime: PropTypes.shape({ + scheduleDate: PropTypes.string, + scheduleTime: PropTypes.string, + }), +}; + +export default ScheduleEmailForm; diff --git a/src/components/bulk-email-tool/bulk-email-form/messages.js b/src/components/bulk-email-tool/bulk-email-form/messages.js new file mode 100644 index 0000000..737c50f --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/messages.js @@ -0,0 +1,89 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + /* BulkEmailForm.jsx Messages */ + bulkEmailSubmitButtonDefault: { + id: 'bulk.email.submit.button.default', + defaultMessage: 'Send Email', + }, + bulkEmailSubmitButtonSchedule: { + id: 'bulk.email.submit.button.schedule', + defaultMessage: 'Schedule Email', + }, + bulkEmailSubmitButtonPending: { + id: 'bulk.email.submit.button.pending', + defaultMessage: 'Submitting', + }, + bulkEmailSubmitButtonComplete: { + id: 'bulk.email.submit.button.send.complete', + defaultMessage: 'Email Created', + }, + bulkEmailSubmitButtonError: { + id: 'bulk.email.submit.button.error', + defaultMessage: 'Error', + }, + bulkEmailSubmitButtonCompleteSchedule: { + id: 'bulk.email.submit.button.schedule.complete', + defaultMessage: 'Scheduling Done', + }, + bulkEmailTaskAlertRecipients: { + id: 'bulk.email.task.alert.recipients', + defaultMessage: 'You are sending an email message with the subject {subject} to the following recipients:', + description: 'A warning shown to the user after submitting the email, to confirm the email recipients.', + }, + bulkEmailToolLabel: { + id: 'bulk.email.tool.label', + defaultMessage: 'Email', + description: 'Tool label. Describes the function of the tool (to send email).', + }, + bulkEmailSubjectLabel: { + id: 'bulk.email.subject.label', + defaultMessage: 'Subject:', + description: 'Email subject line input label. Meant to have colon or equivilant punctuation.', + }, + bulkEmailFormSubjectError: { + id: 'bulk.email.form.subject.error', + defaultMessage: 'A subject is required', + description: 'An Error message located under the subject line. Visible only on failure.', + }, + bulkEmailBodyLabel: { + id: 'bulk.email.body.label', + defaultMessage: 'Body:', + description: 'Email Body label. Meant to have colon or equivilant punctuation.', + }, + bulkEmailFormBodyError: { + id: 'bulk.email.form.body.error', + defaultMessage: 'The message cannot be blank', + description: 'An error message located under the body editor. Visible only on failure.', + }, + bulkEmailInstructionsProofreading: { + id: 'bulk.email.instructions.proofreading', + defaultMessage: 'We recommend sending learners no more than one email message per week. Before you send your email, review the text carefully and send it to yourself first, so that you can preview the formatting and make sure embedded images and links work correctly.', + description: 'A set of instructions to give users a heads up about the formatting of the email they are about to send', + }, + bulkEmailInstructionsCaution: { id: 'bulk.email.instructions.caution', defaultMessage: 'Caution!' }, + + bulkEmailInstructionsCautionMessage: { + id: 'bulk.email.instructions.caution.message', + defaultMessage: + ' When you select Send Email, your email message is added to the queue for sending, and cannot be cancelled.', + description: 'A warning about how emails are sent out to users', + }, + bulkEmailFormScheduleBox: { + id: 'bulk.email.form.scheduleBox', + defaultMessage: 'Schedule this email for a future date', + description: 'Checkbox to schedule sending the email at a later date', + }, + bulkEmailSendEmailButton: { + id: 'bulk.email.send.email.button', + defaultMessage: 'Send Email', + description: 'Schedule/Send email button', + }, + bulkEmailFormError: { + id: 'bulk.email.form.error', + defaultMessage: 'An error occured while attempting to send the email.', + description: 'An Error message located under the submit button for the email form. Visible only on a failure.', + }, +}); + +export default messages; diff --git a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx index 7371656..a2030d0 100644 --- a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx +++ b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx @@ -3,7 +3,7 @@ */ import React from 'react'; import { - render, screen, cleanup, act, fireEvent, + render, screen, cleanup, fireEvent, initializeMockApp, } from '../../../../setupTest'; import BulkEmailForm from '..'; import { postBulkEmail } from '../api'; @@ -13,23 +13,28 @@ jest.mock('../api', () => ({ __esModule: true, postBulkEmail: jest.fn(() => ({ status: 200 })), })); +const appendMock = jest.spyOn(FormData.prototype, 'append'); describe('bulk-email-form', () => { + beforeAll(async () => { + await initializeMockApp(); + }); + beforeEach(() => jest.resetModules()); afterEach(cleanup); test('it renders', () => { render(); - expect(screen.getByText('Submit')).toBeTruthy(); + expect(screen.getByText('Send Email')).toBeTruthy(); }); test('it shows a warning when clicking submit', async () => { render(); - fireEvent.click(screen.getByText('Submit')); + fireEvent.click(screen.getByText('Send Email')); const warning = await screen.findByText('CAUTION!', { exact: false }); expect(warning).toBeTruthy(); }); test('Prevent form POST if invalid', async () => { render(); - fireEvent.click(screen.getByText('Submit')); + fireEvent.click(screen.getByText('Send Email')); expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /continue/i })); expect(await screen.findByText('At least one recipient is required', { exact: false })).toBeInTheDocument(); @@ -40,26 +45,59 @@ describe('bulk-email-form', () => { fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' })); expect(screen.getByRole('checkbox', { name: 'Myself' })).toBeChecked(); fireEvent.change(screen.getByRole('textbox', { name: 'Subject:' }), { target: { value: 'test subject' } }); - fireEvent.click(screen.getByText('Submit')); + fireEvent.click(screen.getByText('Send Email')); expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /continue/i })); expect(await screen.findByText('Submitting')).toBeInTheDocument(); - expect(await screen.findByText('A task to send the emails has been successfully created!')).toBeInTheDocument(); + expect(await screen.findByText('Email Created')).toBeInTheDocument(); }); test('Shows Error on failed POST', async () => { postBulkEmail.mockImplementation(() => { throw Error('api-response-error'); }); - await act(async () => { - render(); - const subjectLine = screen.getByRole('textbox', { name: 'Subject:' }); - const recipient = screen.getByRole('checkbox', { name: 'Myself' }); - fireEvent.click(recipient); - fireEvent.change(subjectLine, { target: { value: 'test subject' } }); - fireEvent.click(screen.getByText('Submit')); - expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /continue/i })); - expect(await screen.findByText('Error')).toBeInTheDocument(); - }); + render(); + const subjectLine = screen.getByRole('textbox', { name: 'Subject:' }); + const recipient = screen.getByRole('checkbox', { name: 'Myself' }); + fireEvent.click(recipient); + fireEvent.change(subjectLine, { target: { value: 'test subject' } }); + fireEvent.click(screen.getByText('Send Email')); + expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /continue/i })); + expect(await screen.findByText('Error')).toBeInTheDocument(); + }); + test('Shows scheduling form when checkbox is checked and submit is changed', async () => { + render(); + const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); + fireEvent.click(scheduleCheckbox); + expect(screen.getByText('Time')); + expect(screen.getByText('Date')); + expect(screen.getByText('Schedule Email')); + }); + test('Prevents sending email when scheduling inputs are empty', async () => { + render(); + const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); + fireEvent.click(scheduleCheckbox); + const submitButton = await screen.findByText('Schedule Email'); + fireEvent.click(submitButton); + const continueButton = await screen.findByRole('button', { name: /continue/i }); + fireEvent.click(continueButton); + expect(screen.getByText('Date and time cannot be blank')); + }); + test('Adds scheduling data to POST requests when schedule is selected', async () => { + render(); + fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' })); + fireEvent.change(screen.getByRole('textbox', { name: 'Subject:' }), { target: { value: 'test subject' } }); + const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); + fireEvent.click(scheduleCheckbox); + const submitButton = screen.getByText('Schedule Email'); + const scheduleDate = screen.getByTestId('scheduleDate'); + const scheduleTime = screen.getByTestId('scheduleTime'); + fireEvent.change(scheduleDate, { target: { value: '2020-05-24' } }); + fireEvent.change(scheduleTime, { target: { value: '10:00' } }); + fireEvent.click(submitButton); + const continueButton = await screen.findByRole('button', { name: /continue/i }); + fireEvent.click(continueButton); + expect(appendMock).toHaveBeenCalledWith('schedule', expect.stringContaining('2020-05-24')); + expect(postBulkEmail).toHaveBeenCalledWith(expect.any(FormData), expect.stringContaining('test-course-id')); }); }); diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx index e6bfa23..41b6374 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx @@ -9,7 +9,7 @@ import messages from './messages'; function BulkEmailTaskManager({ intl, copyTextToEditor }) { return ( -
+

{intl.formatMessage(messages.pendingTasksHeader)} diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailContentHistory.test.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailContentHistory.test.jsx index 80b0ac1..a446848 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailContentHistory.test.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailContentHistory.test.jsx @@ -3,7 +3,7 @@ */ import React from 'react'; import { - render, screen, fireEvent, cleanup, act, + render, screen, fireEvent, cleanup, act, initializeMockApp, } from '../../../../setupTest'; import BulkEmailContentHistory from '../BulkEmailContentHistory'; import { getSentEmailHistory } from '../data/api'; @@ -16,6 +16,9 @@ jest.mock('../data/api', () => ({ describe('BulkEmailContentHistory component', () => { beforeEach(() => jest.resetModules()); + beforeAll(async () => { + await initializeMockApp(); + }); afterEach(cleanup); test('renders correctly', async () => { diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailPendingTasks.test.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailPendingTasks.test.jsx index 2b14c38..b2ddf11 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailPendingTasks.test.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailPendingTasks.test.jsx @@ -3,7 +3,7 @@ */ import React from 'react'; import { - render, screen, cleanup, act, + render, screen, cleanup, act, initializeMockApp, } from '../../../../setupTest'; import BulkEmailPendingTasks from '../BulkEmailPendingTasks'; import { getInstructorTasks } from '../data/api'; @@ -16,6 +16,9 @@ jest.mock('../data/api', () => ({ describe('BulkEmailPendingTasks component', () => { beforeEach(() => jest.resetModules()); + beforeAll(async () => { + await initializeMockApp(); + }); afterEach(cleanup); test('renders correctly', async () => { diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailTaskHistory.test.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailTaskHistory.test.jsx index 031932a..fdbd07f 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailTaskHistory.test.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/test/BulkEmailTaskHistory.test.jsx @@ -3,7 +3,7 @@ */ import React from 'react'; import { - render, screen, fireEvent, cleanup, act, + render, screen, fireEvent, cleanup, act, initializeMockApp, } from '../../../../setupTest'; import BulkEmailTaskHistory from '../BulkEmailTaskHistory'; import { getEmailTaskHistory } from '../data/api'; @@ -16,6 +16,9 @@ jest.mock('../data/api', () => ({ describe('BulkEmailTaskHistory component', () => { beforeEach(() => jest.resetModules()); + beforeAll(async () => { + await initializeMockApp(); + }); afterEach(cleanup); test('renders correctly ', async () => { diff --git a/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx b/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx index d82ba49..aaa6f40 100644 --- a/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx +++ b/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx @@ -3,7 +3,9 @@ */ import React from 'react'; import { Factory } from 'rosie'; -import { render, screen, cleanup } from '../../../setupTest'; +import { + render, screen, cleanup, initializeMockApp, +} from '../../../setupTest'; import BulkEmailTool from '../BulkEmailTool'; import { CourseMetadataContext } from '../../page-container/PageContainer'; import '../../page-container/data/__factories__/cohort.factory'; @@ -23,6 +25,9 @@ jest.mock('react-router-dom', () => ({ describe('BulkEmailTool', () => { beforeEach(() => jest.resetModules()); + beforeAll(async () => { + await initializeMockApp(); + }); afterEach(cleanup); /** diff --git a/src/components/page-container/test/PageContainer.test.jsx b/src/components/page-container/test/PageContainer.test.jsx index 9556806..109fdd9 100644 --- a/src/components/page-container/test/PageContainer.test.jsx +++ b/src/components/page-container/test/PageContainer.test.jsx @@ -4,7 +4,7 @@ import React from 'react'; import { Factory } from 'rosie'; import { - act, cleanup, render, screen, + act, cleanup, initializeMockApp, render, screen, } from '../../../setupTest'; import PageContainer from '../PageContainer'; @@ -26,6 +26,9 @@ jest.mock('react-router-dom', () => ({ describe('PageContainer', () => { beforeEach(() => jest.resetModules()); + beforeAll(async () => { + await initializeMockApp(); + }); afterEach(cleanup); test('PageContainer renders properly when given course metadata', async () => { diff --git a/src/index.jsx b/src/index.jsx index 743fef8..427c3ef 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -39,13 +39,16 @@ subscribe(APP_INIT_ERROR, (error) => { }); initialize({ - config: () => { - mergeConfig({ - }, 'CommuncationsAppConfig'); + handlers: { + config: () => { + mergeConfig( + { + // MICROBA-1505: Remove this when we remove the flag from config + SCHEDULE_EMAIL_SECTION: process.env.SCHEDULE_EMAIL_SECTION || null, + }, + 'CommunicationsAppConfig', + ); + }, }, - messages: [ - appMessages, - headerMessages, - footerMessages, - ], + messages: [appMessages, headerMessages, footerMessages], }); diff --git a/src/index.scss b/src/index.scss index 37cb1e9..b047c2c 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,8 +1,7 @@ -@import "@edx/brand/paragon/fonts.scss"; -@import "@edx/brand/paragon/variables.scss"; -@import "@edx/paragon/scss/core/core.scss"; -@import "@edx/brand/paragon/overrides.scss"; - +@import "~@edx/brand/paragon/fonts"; +@import "~@edx/brand/paragon/variables"; +@import "~@edx/paragon/scss/core/core"; +@import "~@edx/brand/paragon/overrides"; @import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-footer/dist/footer"; diff --git a/src/setupTest.js b/src/setupTest.js index 661bd5d..db7c53f 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -2,17 +2,16 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import '@testing-library/jest-dom'; import React from 'react'; -import PropTypes from 'prop-types'; -import { render } from '@testing-library/react'; +import { render as rtlRender } from '@testing-library/react'; import AppProvider from '@edx/frontend-platform/react/AppProvider'; -import { configure as configureI18n } from '@edx/frontend-platform/i18n'; +import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure as configureLogging } from '@edx/frontend-platform/logging'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, mergeConfig } from '@edx/frontend-platform'; import appMessages from './i18n'; Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation(query => ({ + value: jest.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, @@ -38,32 +37,40 @@ class MockLoggingService { logError = jest.fn(); } -const loggingService = configureLogging(MockLoggingService, { - config: getConfig(), -}); +export function initializeMockApp() { + mergeConfig({ + // MICROBA-1505: Remove this when we remove the flag from config + SCHEDULE_EMAIL_SECTION: true, + }); -configureI18n({ - config: getConfig(), - loggingService, - messages: [appMessages], -}); + const loggingService = configureLogging(MockLoggingService, { + config: getConfig(), + }); -function Wrapper({ children }) { - return ( - // eslint-disable-next-line react/jsx-filename-extension - {children} - ); + configureI18n({ + config: getConfig(), + loggingService, + messages: [appMessages], + }); + + return { loggingService }; } -const renderWithProviders = (ui, options) => { - render(ui, { wrapper: Wrapper, ...options }); -}; -Wrapper.propTypes = { - children: PropTypes.node.isRequired, -}; +function render(ui, options) { + // eslint-disable-next-line react/prop-types + function Wrapper({ children }) { + return ( + // eslint-disable-next-line react/jsx-filename-extension + + {children} + + ); + } + return rtlRender(ui, { wrapper: Wrapper, ...options }); +} // Re-export everything. export * from '@testing-library/react'; // Override `render` method. -export { renderWithProviders as render }; +export { render };