From 0094f7c30f42d158b32741b993070c2f6ba5091d Mon Sep 17 00:00:00 2001 From: Zachary Hancock Date: Wed, 5 Aug 2020 12:30:13 -0400 Subject: [PATCH] Translation Support (#25) --- .../ProctoredExamSettings.jsx | 158 ++++++++++++++---- .../ProctoredExamSettings.messages.jsx | 71 ++++++++ .../ProctoredExamSettings.test.jsx | 44 +++-- 3 files changed, 220 insertions(+), 53 deletions(-) create mode 100644 src/proctored-exam-settings/ProctoredExamSettings.messages.jsx diff --git a/src/proctored-exam-settings/ProctoredExamSettings.jsx b/src/proctored-exam-settings/ProctoredExamSettings.jsx index 4d21e7bfb..8cac56e0b 100644 --- a/src/proctored-exam-settings/ProctoredExamSettings.jsx +++ b/src/proctored-exam-settings/ProctoredExamSettings.jsx @@ -6,7 +6,13 @@ import { Alert, Button, Form, Spinner, } from '@edx/paragon'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { + injectIntl, + intlShape, + FormattedMessage, +} from '@edx/frontend-platform/i18n'; +import messages from './ProctoredExamSettings.messages'; import StudioApiService from '../data/services/StudioApiService'; function ExamSettings(props) { @@ -102,7 +108,7 @@ function ExamSettings(props) { function onButtonClick() { if (proctoringProvider === 'proctortrack' && !EmailValidator.validate(proctortrackEscalationEmail)) { if (proctortrackEscalationEmail === '') { - const errorMessage = 'The Proctortrack Escalation Email field cannot be empty if proctortrack is the selected provider.'; + const errorMessage = props.intl.formatMessage(messages['authoring.examsettings.escalationemail.error.blank']); setFormStatus({ isValid: false, @@ -114,7 +120,7 @@ function ExamSettings(props) { }, }); } else { - const errorMessage = 'The Proctortrack Escalation Email field is in the wrong format and is not valid.'; + const errorMessage = props.intl.formatMessage(messages['authoring.examsettings.escalationemail.error.invalid']); setFormStatus({ isValid: false, @@ -169,10 +175,11 @@ function ExamSettings(props) { function getFormErrorMessage() { const numOfErrors = Object.keys(formStatus.errors).length; const errors = Object.entries(formStatus.errors).map(([id, error]) =>
  • {error.dialogErrorMessage}
  • ); + const messageId = numOfErrors > 1 ? 'authoring.examsettings.error.multiple' : 'authoring.examsettings.error.single'; return ( <> -
    {numOfErrors > 1 ? `There are ${numOfErrors} errors in this form.` : 'There is 1 error in this form.'}
    +
    {props.intl.formatMessage(messages[messageId], { numOfErrors })}
    @@ -201,14 +208,18 @@ function ExamSettings(props) { - If checked, proctored exams are enabled in your course. + @@ -216,12 +227,18 @@ function ExamSettings(props) { { isEdxStaff && enableProctoredExams && (
    - Allow Opting Out of Proctored Exams + + + onAllowOptingOutChange(true)} @@ -231,16 +248,22 @@ function ExamSettings(props) { type="radio" id="allowOptingOutNo" name="allowOptingOut" - label="No" + label={props.intl.formatMessage(messages['authoring.examsettings.allowoptout.no'])} inline checked={!allowOptingOut} onChange={() => onAllowOptingOutChange(false)} data-test-id="allowOptingOutNo" /> - If this value is "Yes", learners can choose to take proctored exams without proctoring. - If this value is "No", all learners must take the exam with proctoring. - This setting only applies if proctored exams are enabled for the course. +
    @@ -249,7 +272,13 @@ function ExamSettings(props) { {/* PROCTORING PROVIDER */} { enableProctoredExams && ( - Proctoring Provider + + + {getProctoringProviderOptions(availableProctoringProviders)} - {cannotEditProctoringProvider() ? ('Proctoring provider cannot be modified after course start date.') : ('Select the proctoring provider you want to use for this course run.')} + + {cannotEditProctoringProvider() ? props.intl.formatMessage(messages['authoring.examsettings.provider.help.aftercoursestart']) : props.intl.formatMessage(messages['authoring.examsettings.provider.help'])} + )} {/* PROCTORTRACK ESCALATION EMAIL */} {showProctortrackEscalationEmail && enableProctoredExams && ( - Proctortrack Escalation Email + + + {formStatus.errors.formProctortrackEscalationEmail && formStatus.errors.formProctortrackEscalationEmail.inputErrorMessage} - Required if "proctortrack" is selected as your proctoring provider. - Enter an email address to be contacted by the support team whenever there are escalations - (e.g. appeals, delayed reviews, etc.). + )} @@ -287,11 +329,17 @@ function ExamSettings(props) { { isEdxStaff && enableProctoredExams && (
    - Create Zendesk Tickets for Suspicious Proctored Exam Attempts + + + - If this value is "Yes", - a Zendesk ticket will be created for suspicious proctored exam attempts. +
    @@ -321,7 +372,11 @@ function ExamSettings(props) { onClick={onButtonClick} disabled={submissionInProgress} > - Submit + {' '} {submissionInProgress && } @@ -338,7 +393,13 @@ function ExamSettings(props) { data-test-id="spinnerContainer" > - Loading... + + + ); @@ -347,10 +408,15 @@ function ExamSettings(props) { function renderConnectionError() { return ( - We encountered a technical error when loading this page. - This might be a temporary issue, so please try again in a few minutes. - If the problem persists, - please go to edX Support Page for help. + {props.intl.formatMessage(messages['authoring.examsettings.support.text'])} }} + description="" + /> ); } @@ -358,8 +424,13 @@ function ExamSettings(props) { function renderPermissionError() { return ( - You are not authorized to view this page. If you feel you should have access, - please reach out to your course team admin to be given access. + ); } @@ -373,8 +444,14 @@ function ExamSettings(props) { data-test-id="saveSuccess" onClose={() => setSaveSuccess(false)} > - Proctored exam settings saved successfully. - You can go back to your course in Studio here. + here }} + /> ); } @@ -387,10 +464,16 @@ function ExamSettings(props) { data-test-id="saveError" onClose={() => setSaveError(false)} > - We encountered a technical error while trying to save proctored exam settings. - This might be a temporary issue, so please try again in a few minutes. - If the problem persists, - please go to edX Support Page for help. + {props.intl.formatMessage(messages['authoring.examsettings.support.text'])} }} + /> ); } @@ -459,9 +542,10 @@ function ExamSettings(props) { } ExamSettings.propTypes = { + intl: intlShape.isRequired, courseId: PropTypes.string.isRequired, }; ExamSettings.defaultProps = {}; -export default ExamSettings; +export default injectIntl(ExamSettings); diff --git a/src/proctored-exam-settings/ProctoredExamSettings.messages.jsx b/src/proctored-exam-settings/ProctoredExamSettings.messages.jsx new file mode 100644 index 000000000..86a7dbd7c --- /dev/null +++ b/src/proctored-exam-settings/ProctoredExamSettings.messages.jsx @@ -0,0 +1,71 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'authoring.examsettings.allowoptout.no': { + id: 'authoring.examsettings.allowoptout.no', + defaultMessage: 'No', + description: '"No" option for yes/no radio button set', + }, + 'authoring.examsettings.allowoptout.yes': { + id: 'authoring.examsettings.allowoptout.yes', + defaultMessage: 'Yes', + description: '"Yes" option for yes/no radio button set', + }, + 'authoring.examsettings.createzendesk.no': { + id: 'authoring.examsettings.createzendesk.no', + defaultMessage: 'No', + description: '"No" option for yes/no radio button set.', + }, + 'authoring.examsettings.createzendesk.yes': { + id: 'authoring.examsettings.createzendesk.yes', + defaultMessage: 'Yes', + description: '"Yes" option for yes/no radio button set.', + }, + 'authoring.examsettings.support.text': { + id: 'authoring.examsettings.support.text', + defaultMessage: 'edX Support Page', + description: 'Text linking to the support page.', + }, + 'authoring.examsettings.enableproctoredexams.label': { + id: 'authoring.examsettings.escalationemail.enableproctoredexams.label', + defaultMessage: 'Enable Proctored Exams', + description: 'Label for checkbox to enable proctored exams.', + }, + 'authoring.examsettings.escalationemail.error.blank': { + id: 'authoring.examsettings.escalationemail.error.blank', + defaultMessage: 'The Proctortrack Escalation Email field cannot be empty if proctortrack is the selected provider.', + description: 'Error message for missing required email field.', + }, + 'authoring.examsettings.escalationemail.error.invalid': { + id: 'authoring.examsettings.escalationemail.error.invalid', + defaultMessage: 'The Proctortrack Escalation Email field is in the wrong format and is not valid.', + description: 'Error message for a invalid email format.', + }, + 'authoring.examsettings.error.single': { + id: 'authoring.examsettings.error.single', + defaultMessage: 'There is 1 error in this form.', + description: 'Error alert for one and only one error in the form.', + }, + 'authoring.examsettings.error.multiple': { + id: 'authoring.examsettings.escalationemail.error.multiple', + defaultMessage: 'There are {numOfErrors} errors in this form.', + description: 'Error alert for multiple errors in the form.', + }, + 'authoring.examsettings.provider.label': { + id: 'authoring.examsettings.provider.label', + defaultMessage: 'Proctoring Provider', + description: 'Label for provider dropdown selection.', + }, + 'authoring.examsettings.provider.help': { + id: 'authoring.examsettings.provider.help', + defaultMessage: 'Select the proctoring provider you want to use for this course run.', + description: 'Help text for selecting a proctoring provider.', + }, + 'authoring.examsettings.provider.help.aftercoursestart': { + id: 'authoring.examsettings.provider.help.aftercoursestart', + defaultMessage: 'Proctoring provider cannot be modified after course start date.', + description: 'Help text notifying the user that the provider cannot be changed for a course that has already begun.', + }, +}); + +export default messages; diff --git a/src/proctored-exam-settings/ProctoredExamSettings.test.jsx b/src/proctored-exam-settings/ProctoredExamSettings.test.jsx index f59d0f6f3..fda4f60ad 100644 --- a/src/proctored-exam-settings/ProctoredExamSettings.test.jsx +++ b/src/proctored-exam-settings/ProctoredExamSettings.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { render, screen, cleanup, waitFor, waitForElementToBeRemoved, fireEvent, act, } from '@testing-library/react'; +import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import * as auth from '@edx/frontend-platform/auth'; import ProctoredExamSettings from './ProctoredExamSettings'; import StudioApiService from '../data/services/StudioApiService'; @@ -10,6 +11,14 @@ const defaultProps = { courseId: 'course-v1%3AedX%2BDemoX%2BDemo_Course', }; +const IntlProctoredExamSettings = injectIntl(ProctoredExamSettings); + +const intlWrapper = children => ( + + {children} + +); + describe('ProctoredExamSettings field dependency tests', () => { beforeEach(async () => { auth.getAuthenticatedHttpClient = jest.fn(() => ({ @@ -29,7 +38,7 @@ describe('ProctoredExamSettings field dependency tests', () => { })); auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, administrator: true })); - await act(async () => render()); + await act(async () => render(intlWrapper())); }); afterEach(() => { @@ -90,7 +99,7 @@ describe('ProctoredExamSettings field dependency tests', () => { }), })); - await act(async () => render()); + await act(async () => render(intlWrapper())); await waitFor(() => { screen.getByLabelText('Enable Proctored Exams'); }); @@ -148,7 +157,7 @@ describe('ProctoredExamSettings validation with invalid escalation email', () => })); auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, administrator: false })); - await act(async () => render()); + await act(async () => render(intlWrapper())); }); afterEach(() => { @@ -305,28 +314,28 @@ describe('Disables proctoring provider options', () => { it('disables irrelevant Proctoring Provider fields when user is not an administrator and it is after start date', async () => { mockAPI(mockGetPastCourseData, false); - await act(async () => render()); + await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(true); }); it('enables all Proctoring Provider options if user is not an administrator and it is before start date', async () => { mockAPI(mockGetFutureCourseData, false); - await act(async () => render()); + await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('enables all Proctoring Provider options if user administrator and it is after start date', async () => { mockAPI(mockGetPastCourseData, true); - await act(async () => render()); + await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('enables all Proctoring Provider options if user administrator and it is before start date', async () => { mockAPI(mockGetFutureCourseData, true); - await act(async () => render()); + await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); @@ -362,14 +371,14 @@ describe('Hides fields based on user permissions', () => { it('hides opting out and zendesk tickets for non edX staff', async () => { mockAuthentication(false); - await act(async () => render()); + await act(async () => render(intlWrapper())); expect(screen.queryByTestId('allowOptingOutYes')).toBeNull(); expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull(); }); it('shows opting out and zendesk tickets for edX staff', async () => { mockAuthentication(true); - await act(async () => render()); + await act(async () => render(intlWrapper())); expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull(); expect(screen.queryByTestId('createZendeskTicketsYes')).not.toBeNull(); }); @@ -377,10 +386,13 @@ describe('Hides fields based on user permissions', () => { describe('ProctoredExamSettings connection states tests', () => { it('shows the spinner before the connection is complete', async () => { - render(); + auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, administrator: false })); + auth.getAuthenticatedHttpClient = jest.fn(() => ({ + get: jest.fn(() => new Promise(() => {})), + })); + render(); const spinner = screen.getByTestId('spinnerContainer'); expect(spinner.textContent).toEqual('Loading...'); - await waitForElementToBeRemoved(spinner); }); it('show connection error message when we suffer server side error', async () => { @@ -395,7 +407,7 @@ describe('ProctoredExamSettings connection states tests', () => { }, })); - await act(async () => render()); + await act(async () => render(intlWrapper())); const connectionError = screen.getByTestId('connectionError'); expect(connectionError.textContent).toEqual( expect.stringContaining('We encountered a technical error'), @@ -414,7 +426,7 @@ describe('ProctoredExamSettings connection states tests', () => { }, })); - await act(async () => render()); + await act(async () => render(intlWrapper())); const connectionError = screen.getByTestId('permissionError'); expect(connectionError.textContent).toEqual( expect.stringContaining('You are not authorized to view this page'), @@ -453,7 +465,7 @@ describe('ProctoredExamSettings save settings tests', () => { it('Show spinner while saving', async () => { const mockedFunctions = mockAPI(mockGetData, { data: 'success' }); - await act(async () => render()); + await act(async () => render(intlWrapper())); const submitButton = screen.getByTestId('submissionButton'); expect(screen.queryByTestId('saveInProgress')).toBeFalsy(); fireEvent.click(submitButton); @@ -466,7 +478,7 @@ describe('ProctoredExamSettings save settings tests', () => { it('Makes API call successfully with proctoring_escalation_email if proctortrack', async () => { const mockedFunctions = mockAPI(mockGetData, { data: 'success' }); - await act(async () => render()); + await act(async () => render(intlWrapper())); // Make a change to the provider to proctortrack and set the email const selectElement = screen.getByDisplayValue('mockproc'); await act(async () => { @@ -532,7 +544,7 @@ describe('ProctoredExamSettings save settings tests', () => { it('Makes API call generated error', async () => { const mockedFunctions = mockAPI(mockGetData, false); - await act(async () => render()); + await act(async () => render(intlWrapper())); // Make a change to the provider to proctortrack and set the email const submitButton = screen.getByTestId('submissionButton'); await act(async () => {