refactor: Migrate accessibilityPage variable in Redux store to React Query (#2760)

This commit is contained in:
Chris Chávez
2025-12-23 09:18:13 -05:00
committed by GitHub
parent f4d20eba45
commit ef6bd073c0
19 changed files with 92 additions and 178 deletions

View File

@@ -1,46 +0,0 @@
import {
render,
screen,
} from '@testing-library/react';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import initializeStore from '../../store';
import AccessibilityBody from './index';
let store;
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityBody
communityAccessibilityLink="http://example.com"
email="example@example.com"
/>
</AppProvider>
</IntlProvider>,
);
};
describe('<AccessibilityBody />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore({});
});
it('contains links', () => {
renderComponent();
expect(screen.getAllByTestId('email-element')).toHaveLength(2);
expect(screen.getAllByTestId('accessibility-page-link')).toHaveLength(1);
});
});
});

View File

@@ -0,0 +1,29 @@
import {
initializeMocks,
render,
screen,
} from '@src/testUtils';
import AccessibilityBody from './index';
const renderComponent = () => {
render(
<AccessibilityBody
communityAccessibilityLink="http://example.com"
email="example@example.com"
/>,
);
};
describe('<AccessibilityBody />', () => {
describe('renders', () => {
beforeEach(async () => {
initializeMocks();
});
it('contains links', () => {
renderComponent();
expect(screen.getAllByTestId('email-element')).toHaveLength(2);
expect(screen.getAllByTestId('accessibility-page-link')).toHaveLength(1);
});
});
});

View File

@@ -1,5 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, MailtoLink, Stack } from '@openedx/paragon';
@@ -8,6 +6,9 @@ import messages from './messages';
const AccessibilityBody = ({
communityAccessibilityLink,
email,
}: {
communityAccessibilityLink: string,
email: string,
}) => (
<div className="mt-5">
<header>
@@ -90,9 +91,4 @@ const AccessibilityBody = ({
</div>
);
AccessibilityBody.propTypes = {
communityAccessibilityLink: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
};
export default AccessibilityBody;

View File

@@ -1,56 +1,31 @@
import {
initializeMocks,
render,
screen,
} from '@testing-library/react';
} from '@src/testUtils';
import userEvent from '@testing-library/user-event';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import initializeStore from '../../store';
import { RequestStatus } from '../../data/constants';
import AccessibilityForm from './index';
import { getZendeskrUrl } from '../data/api';
import messages from './messages';
let axiosMock;
let store;
const defaultProps = {
accessibilityEmail: 'accessibilityTest@test.com',
};
const initialState = {
accessibilityPage: {
savingStatus: '',
},
};
const renderComponent = () => {
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<AccessibilityForm {...defaultProps} />
</AppProvider>
</IntlProvider>,
<AccessibilityForm {...defaultProps} />,
);
};
describe('<AccessibilityPolicyForm />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
});
describe('renders', () => {
@@ -86,14 +61,23 @@ describe('<AccessibilityPolicyForm />', () => {
submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage);
});
it('renders in progress state', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(
() => new Promise(() => {
// always in pending
}),
);
await user.click(submitButton);
expect(screen.getByRole('button', { name: /submitting/i })).toBeInTheDocument();
});
it('shows correct success message', async () => {
axiosMock.onPost(getZendeskrUrl()).reply(200);
await user.click(submitButton);
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.SUCCESSFUL);
expect(screen.getAllByRole('alert')).toHaveLength(1);
expect(screen.getByText(messages.accessibilityPolicyFormSuccess.defaultMessage)).toBeVisible();
@@ -108,9 +92,6 @@ describe('<AccessibilityPolicyForm />', () => {
await user.click(submitButton);
const { savingStatus } = store.getState().accessibilityPage;
expect(savingStatus).toEqual(RequestStatus.FAILED);
expect(screen.getAllByRole('alert')).toHaveLength(1);
expect(screen.getByTestId('rate-limit-alert')).toBeVisible();

View File

@@ -1,5 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
FormattedMessage, FormattedDate, FormattedTime, useIntl,
} from '@edx/frontend-platform/i18n';
@@ -7,26 +5,22 @@ import {
ActionRow, Alert, Form, Stack, StatefulButton,
} from '@openedx/paragon';
import { RequestStatus } from '../../data/constants';
import { STATEFUL_BUTTON_STATES } from '../../constants';
import submitAccessibilityForm from '../data/thunks';
import { STATEFUL_BUTTON_STATES } from '@src/constants';
import useAccessibility from './hooks';
import messages from './messages';
const AccessibilityForm = ({
accessibilityEmail,
}) => {
const AccessibilityForm = ({ accessibilityEmail }: { accessibilityEmail: string }) => {
const intl = useIntl();
const {
errors,
values,
isFormFilled,
dispatch,
mutation,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
} = useAccessibility({ name: '', email: '', message: '' }, intl);
} = useAccessibility({ name: '', email: '', message: '' });
const formFields = [
{
@@ -55,7 +49,7 @@ const AccessibilityForm = ({
};
const handleSubmit = () => {
dispatch(submitAccessibilityForm(values));
mutation.mutateAsync(values).catch(() => {});
};
const start = new Date('Mon Jan 29 2018 13:00:00 GMT (UTC)');
@@ -66,7 +60,7 @@ const AccessibilityForm = ({
<h2 className="my-4">
<FormattedMessage {...messages.accessibilityPolicyFormHeader} />
</h2>
{savingStatus === RequestStatus.SUCCESSFUL && (
{savingStatus === 'success' && (
<Alert variant="success">
<Stack gap={2}>
<div className="mb-2">
@@ -86,7 +80,7 @@ const AccessibilityForm = ({
</Stack>
</Alert>
)}
{savingStatus === RequestStatus.FAILED && (
{savingStatus === 'error' && (
<Alert variant="danger">
<div data-testid="rate-limit-alert">
<FormattedMessage
@@ -125,7 +119,7 @@ const AccessibilityForm = ({
onClick={handleSubmit}
disabled={!isFormFilled}
state={
savingStatus === RequestStatus.IN_PROGRESS
savingStatus === 'pending'
? STATEFUL_BUTTON_STATES.pending
: STATEFUL_BUTTON_STATES.default
}
@@ -136,8 +130,4 @@ const AccessibilityForm = ({
);
};
AccessibilityForm.propTypes = {
accessibilityEmail: PropTypes.string.isRequired,
};
export default AccessibilityForm;

View File

@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
import { useSubmitAccessibilityForm } from '../data/apiHooks';
import { AccessibilityFormData } from '../data/api';
const useAccessibility = (initialValues, intl) => {
const dispatch = useDispatch();
const savingStatus = useSelector(state => state.accessibilityPage.savingStatus);
const useAccessibility = (initialValues: AccessibilityFormData) => {
const intl = useIntl();
const [isFormFilled, setFormFilled] = useState(false);
const validationSchema = Yup.object().shape({
name: Yup.string().required(
@@ -29,29 +29,27 @@ const useAccessibility = (initialValues, intl) => {
enableReinitialize: true,
validateOnBlur: false,
validationSchema,
/* istanbul ignore next */
onSubmit: () => {},
});
const mutation = useSubmitAccessibilityForm(handleReset);
useEffect(() => {
setFormFilled(Object.values(values).every((i) => i));
}, [values]);
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
handleReset();
}
}, [savingStatus]);
const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName];
return {
errors,
values,
isFormFilled,
dispatch,
mutation,
handleBlur,
handleChange,
hasErrorField,
savingStatus,
savingStatus: mutation.status,
};
};

View File

@@ -1,4 +1,3 @@
// @ts-check
import { initializeMocks, render, screen } from '../testUtils';
import AccessibilityPage from './index';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import { Helmet } from 'react-helmet';
@@ -38,6 +37,4 @@ const AccessibilityPage = () => {
);
};
AccessibilityPage.propTypes = {};
export default AccessibilityPage;

View File

@@ -8,12 +8,20 @@ ensureConfig([
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getZendeskrUrl = () => `${getApiBaseUrl()}/zendesk_proxy/v0`;
export interface AccessibilityFormData {
name: string;
email: string;
message: string;
}
/**
* Posts the form data to zendesk endpoint
* @param {string} courseId
* @returns {Promise<[{}]>}
*/
export async function postAccessibilityForm({ name, email, message }) {
export async function postAccessibilityForm({
name,
email,
message,
}: AccessibilityFormData) {
const data = {
name,
tags: ['studio_a11y'],

View File

@@ -0,0 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { AccessibilityFormData, postAccessibilityForm } from './api';
/**
* Mutation to submit accessibility form
*/
export const useSubmitAccessibilityForm = (handleSuccess: (e: any) => void) => (
useMutation({
mutationFn: (formData: AccessibilityFormData) => postAccessibilityForm(formData),
onSuccess: handleSuccess,
})
);

View File

@@ -1,23 +0,0 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
const slice = createSlice({
name: 'accessibilityPage',
initialState: {
savingStatus: '',
},
reducers: {
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
},
});
export const {
updateLoadingStatus,
updateSavingStatus,
} = slice.actions;
export const {
reducer,
} = slice;

View File

@@ -1,24 +0,0 @@
import { RequestStatus } from '../../data/constants';
import { postAccessibilityForm } from './api';
import { updateSavingStatus } from './slice';
function submitAccessibilityForm({ email, name, message }) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
await postAccessibilityForm({ email, name, message });
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
/* istanbul ignore else */
if (error.response && error.response.status === 429) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
} else {
/* istanbul ignore next */
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
}
}
};
}
export default submitAccessibilityForm;

View File

@@ -23,7 +23,6 @@ import { reducer as videosReducer } from './files-and-videos/videos-page/data/sl
import { reducer as courseOutlineReducer } from './course-outline/data/slice';
import { reducer as courseUnitReducer } from './course-unit/data/slice';
import { reducer as courseChecklistReducer } from './course-checklist/data/slice';
import { reducer as accessibilityPageReducer } from './accessibility-page/data/slice';
import { reducer as textbooksReducer } from './textbooks/data/slice';
import { reducer as certificatesReducer } from './certificates/data/slice';
import { reducer as groupConfigurationsReducer } from './group-configurations/data/slice';
@@ -55,7 +54,6 @@ export interface DeprecatedReduxState {
courseOutline: Record<string, any>;
courseUnit: Record<string, any>;
courseChecklist: Record<string, any>;
accessibilityPage: Record<string, any>;
certificates: Record<string, any>;
groupConfigurations: InferState<typeof groupConfigurationsReducer>;
textbooks: Record<string, any>;
@@ -84,7 +82,6 @@ export default function initializeStore(preloadedState: Partial<DeprecatedReduxS
courseOutline: courseOutlineReducer,
courseUnit: courseUnitReducer,
courseChecklist: courseChecklistReducer,
accessibilityPage: accessibilityPageReducer,
certificates: certificatesReducer,
groupConfigurations: groupConfigurationsReducer,
textbooks: textbooksReducer,