Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRMDR-600 UI feedback endpoint #264

Merged
merged 3 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ describe('Feedback Page', () => {
fillInForm(mockInputData);

cy.get('#submit-feedback').click();

// TODO: when backend call for sending email is implemented,
// intercept the call and check that payload data is the same as mockInputData
cy.intercept('POST', '/Feedback*', {
statusCode: 200,
}).as('feedback');

cy.get('.app-homepage-content h1', { timeout: 5000 }).should(
'have.text',
Expand All @@ -104,7 +104,9 @@ describe('Feedback Page', () => {
fillInForm(mockInputData);

cy.get('#submit-feedback').click();

cy.intercept('POST', '/Feedback*', {
statusCode: 200,
}).as('feedback');
cy.get('.app-homepage-content h1', { timeout: 5000 }).should(
'have.text',
'We’ve received your feedback',
Expand Down
86 changes: 76 additions & 10 deletions app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import { SATISFACTION_CHOICES, SUBMISSION_STAGE } from '../../../types/pages/feedbackPage/types';
import userEvent from '@testing-library/user-event';
import sendEmail from '../../../helpers/requests/sendEmail';
import FeedbackForm, { Props } from './FeedbackForm';
import { fillInForm } from '../../../helpers/test/formUtils';
import axios from 'axios';
import useBaseAPIUrl from '../../../helpers/hooks/useBaseAPIUrl';
import { routes } from '../../../types/generic/routes';

jest.mock('../../../helpers/requests/sendEmail');
jest.mock('axios');
jest.mock('../../../helpers/hooks/useBaseAPIUrl');
jest.mock('../../../helpers/hooks/useBaseAPIHeaders');
const mockedUseNavigate = jest.fn();
const mockSendEmail = sendEmail as jest.Mock;
const mockedAxios = axios as jest.Mocked<typeof axios>;
const mockedBaseURL = useBaseAPIUrl as jest.Mock;
const baseURL = 'http://test';
jest.mock('moment', () => {
return () => jest.requireActual('moment')('2020-01-01T00:00:00.000Z');
});
const mockSetStage = jest.fn();
jest.mock('react-router', () => ({
useNavigate: () => mockedUseNavigate,
}));

const clickSubmitButton = () => {
userEvent.click(screen.getByRole('button', { name: 'Send feedback' }));
};
Expand All @@ -29,7 +39,7 @@ const renderComponent = (override: Partial<Props> = {}) => {
describe('<FeedbackForm />', () => {
beforeEach(() => {
process.env.REACT_APP_ENVIRONMENT = 'jest';
mockSendEmail.mockReturnValueOnce(Promise.resolve());
mockedBaseURL.mockReturnValue(baseURL);
});
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -84,6 +94,10 @@ describe('<FeedbackForm />', () => {

describe('User interactions', () => {
it('on submit, call sendEmail() with the data that user had filled in', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ status: 200, data: 'Success' }),
);

const mockInputData = {
feedbackContent: 'Mock feedback content',
howSatisfied: SATISFACTION_CHOICES.VerySatisfied,
Expand All @@ -98,7 +112,11 @@ describe('<FeedbackForm />', () => {
clickSubmitButton();
});

await waitFor(() => expect(mockSendEmail).toBeCalledWith(mockInputData));
await waitFor(() =>
expect(mockedAxios.post).toBeCalledWith(baseURL + '/Feedback', mockInputData, {
headers: {},
}),
);
expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting);
});

Expand All @@ -119,7 +137,7 @@ describe('<FeedbackForm />', () => {
await waitFor(() => {
expect(screen.getByText('Please enter your feedback')).toBeInTheDocument();
});
expect(mockSendEmail).not.toBeCalled();
expect(mockedAxios).not.toBeCalled();
expect(mockSetStage).not.toBeCalled();
});

Expand All @@ -140,11 +158,11 @@ describe('<FeedbackForm />', () => {
await waitFor(() => {
expect(screen.getByText('Please select an option')).toBeInTheDocument();
});
expect(mockSendEmail).not.toBeCalled();
expect(mockedAxios).not.toBeCalled();
expect(mockSetStage).not.toBeCalled();
});

it("on submit, if user filled in an invalid email address, display an error message and don't send email", async () => {
it("on submit, if user filled in an invalid email address, display an error message and don't send email", async () => {
const mockInputData = {
feedbackContent: 'Mock feedback content',
howSatisfied: SATISFACTION_CHOICES.VerySatisfied,
Expand All @@ -162,11 +180,14 @@ describe('<FeedbackForm />', () => {
await waitFor(() => {
expect(screen.getByText('Enter a valid email address')).toBeInTheDocument();
});
expect(mockSendEmail).not.toBeCalled();
expect(mockedAxios).not.toBeCalled();
expect(mockSetStage).not.toBeCalled();
});

it('on submit, allows the respondent name and email to be blank', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ status: 200, data: 'Success' }),
);
const mockInputData = {
feedbackContent: 'Mock feedback content',
howSatisfied: SATISFACTION_CHOICES.VeryDissatisfied,
Expand All @@ -184,7 +205,15 @@ describe('<FeedbackForm />', () => {
clickSubmitButton();
});

await waitFor(() => expect(mockSendEmail).toBeCalledWith(expectedEmailContent));
await waitFor(() =>
expect(mockedAxios.post).toBeCalledWith(
baseURL + '/Feedback',
expectedEmailContent,
{
headers: {},
},
),
);
expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting);
});

Expand All @@ -195,4 +224,41 @@ describe('<FeedbackForm />', () => {
expect(screen.getByRole('SpinnerButton')).toHaveAttribute('disabled');
});
});
describe('Navigation', () => {
it('navigates to Error page when call to feedback endpoint return 500', async () => {
const errorResponse = {
response: {
status: 500,
data: { message: 'An error occurred', err_code: 'SP_1001' },
},
};
mockedAxios.post.mockImplementation(() => Promise.reject(errorResponse));
const mockInputData = {
feedbackContent: 'Mock feedback content',
howSatisfied: SATISFACTION_CHOICES.VerySatisfied,
respondentName: 'Jane Smith',
respondentEmail: 'jane_smith@testing.com',
};

renderComponent();

act(() => {
fillInForm(mockInputData);
clickSubmitButton();
});

await waitFor(() =>
expect(mockedAxios.post).toBeCalledWith(baseURL + '/Feedback', mockInputData, {
headers: {},
}),
);
expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting);

await waitFor(() => {
expect(mockedUseNavigate).toHaveBeenCalledWith(
routes.SERVER_ERROR + '?encodedError=WyJTUF8xMDAxIiwiMTU3NzgzNjgwMCJd',
);
});
});
});
});
8 changes: 6 additions & 2 deletions app/src/components/blocks/feedbackForm/FeedbackForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ import SpinnerButton from '../../generic/spinnerButton/SpinnerButton';
import { routes } from '../../../types/generic/routes';
import { useNavigate } from 'react-router-dom';
import { errorToParams } from '../../../helpers/utils/errorToParams';
import { AxiosError } from 'axios/index';
import { AxiosError } from 'axios';
import useBaseAPIUrl from '../../../helpers/hooks/useBaseAPIUrl';
import useBaseAPIHeaders from '../../../helpers/hooks/useBaseAPIHeaders';

export type Props = {
stage: SUBMISSION_STAGE;
setStage: Dispatch<SUBMISSION_STAGE>;
};

function FeedbackForm({ stage, setStage }: Props) {
const baseUrl = useBaseAPIUrl();
const baseHeaders = useBaseAPIHeaders();
const {
handleSubmit,
register,
Expand All @@ -35,7 +39,7 @@ function FeedbackForm({ stage, setStage }: Props) {
setStage(SUBMISSION_STAGE.Submitting);
// add tests for failing and passing cases when real email service is implemented
try {
await sendEmail(formData);
await sendEmail({ formData, baseUrl, baseHeaders });
setStage(SUBMISSION_STAGE.Successful);
} catch (e) {
const error = e as AxiosError;
Expand Down
29 changes: 18 additions & 11 deletions app/src/helpers/requests/sendEmail.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { FormData } from '../../types/pages/feedbackPage/types';
import axios, { AxiosError } from 'axios';
import { AuthHeaders } from '../../types/blocks/authHeaders';
import { endpoints } from '../../types/generic/endpoints';
type Args = {
formData: FormData;
baseUrl: string;
baseHeaders: AuthHeaders;
};
const sendEmail = async ({ formData, baseUrl, baseHeaders }: Args) => {
const gatewayUrl = baseUrl + endpoints.FEEDBACK;

const sendEmail = async (formData: FormData) => {
// using console.log as a placeholder until we got the send email solution in place
/* eslint-disable-next-line no-console */
console.log(`sending feedback from user by email: ${JSON.stringify(formData)}}`);
try {
await new Promise((resolve, reject) =>
setTimeout(() => {
resolve({});
}, 1000),
);
return { status: 200 };
const { data } = await axios.post(gatewayUrl, formData, {
headers: {
...baseHeaders,
},
});
return data;
} catch (e) {
throw e;
const error = e as AxiosError;
throw error;
}
};

Expand Down
2 changes: 2 additions & 0 deletions app/src/helpers/utils/errorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const errorCodes: { [key: string]: string } = {
OUT_5001: 'Error logging user out',
ENV_5001: 'An error occurred due to missing environment variable',
GWY_5001: 'Failed to utilise AWS client/resource',
SFB_5001: 'Error occur when sending email by SES',
SFB_5002: 'Failed to fetch parameters for sending email from SSM param store',
};

export default errorCodes;
1 change: 1 addition & 0 deletions app/src/pages/feedbackPage/FeedbackPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SATISFACTION_CHOICES } from '../../types/pages/feedbackPage/types';
import FeedbackPage from './FeedbackPage';
import sendEmail from '../../helpers/requests/sendEmail';
import { fillInForm } from '../../helpers/test/formUtils';
jest.mock('../../helpers/hooks/useBaseAPIHeaders');

jest.mock('../../helpers/requests/sendEmail');
const mockSendEmail = sendEmail as jest.Mock;
Expand Down
1 change: 1 addition & 0 deletions app/src/types/generic/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum endpoints {
DOCUMENT_PRESIGN = '/DocumentManifest',

LLOYDGEORGE_STITCH = '/LloydGeorgeStitch',
FEEDBACK = '/Feedback',
}
Loading