diff --git a/app/soapbox/actions/__tests__/alerts.test.ts b/app/soapbox/actions/__tests__/alerts.test.ts new file mode 100644 index 000000000..f2419893a --- /dev/null +++ b/app/soapbox/actions/__tests__/alerts.test.ts @@ -0,0 +1,149 @@ +import { AxiosError } from 'axios'; + +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { dismissAlert, showAlert, showAlertForError } from '../alerts'; + +const buildError = (message: string, status: number) => new AxiosError(message, String(status), null, null, { + data: { + error: message, + }, + statusText: String(status), + status, + headers: {}, + config: {}, +}); + +let store; + +beforeEach(() => { + const state = rootReducer(undefined, {}); + store = mockStore(state); +}); + +describe('dismissAlert()', () => { + it('dispatches the proper actions', async() => { + const alert = 'hello world'; + const expectedActions = [ + { type: 'ALERT_DISMISS', alert }, + ]; + await store.dispatch(dismissAlert(alert)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); + +describe('showAlert()', () => { + it('dispatches the proper actions', async() => { + const title = 'title'; + const message = 'msg'; + const severity = 'info'; + const expectedActions = [ + { type: 'ALERT_SHOW', title, message, severity }, + ]; + await store.dispatch(showAlert(title, message, severity)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); + +describe('showAlert()', () => { + describe('with a 502 status code', () => { + it('dispatches the proper actions', async() => { + const message = 'The server is down'; + const error = buildError(message, 502); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with a 404 status code', () => { + it('dispatches the proper actions', async() => { + const error = buildError('', 404); + + const expectedActions = []; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with a 410 status code', () => { + it('dispatches the proper actions', async() => { + const error = buildError('', 410); + + const expectedActions = []; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an accepted status code', () => { + describe('with a message from the server', () => { + it('dispatches the proper actions', async() => { + const message = 'custom message'; + const error = buildError(message, 200); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('without a message from the server', () => { + it('dispatches the proper actions', async() => { + const message = 'The request has been accepted for processing'; + const error = buildError(message, 202); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); + + describe('without a response', () => { + it('dispatches the proper actions', async() => { + const error = new AxiosError(); + + const expectedActions = [ + { + type: 'ALERT_SHOW', + title: { + defaultMessage: 'Oops!', + id: 'alert.unexpected.title', + }, + message: { + defaultMessage: 'An unexpected error occurred.', + id: 'alert.unexpected.message', + }, + severity: 'error', + }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); diff --git a/app/soapbox/actions/alerts.js b/app/soapbox/actions/alerts.js deleted file mode 100644 index c71ce3e87..000000000 --- a/app/soapbox/actions/alerts.js +++ /dev/null @@ -1,68 +0,0 @@ -import { defineMessages } from 'react-intl'; - -import { httpErrorMessages } from 'soapbox/utils/errors'; - -const messages = defineMessages({ - unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, - unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, -}); - -export const ALERT_SHOW = 'ALERT_SHOW'; -export const ALERT_DISMISS = 'ALERT_DISMISS'; -export const ALERT_CLEAR = 'ALERT_CLEAR'; - -const noOp = () => {}; - -export function dismissAlert(alert) { - return { - type: ALERT_DISMISS, - alert, - }; -} - -export function clearAlert() { - return { - type: ALERT_CLEAR, - }; -} - -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, severity = 'info') { - return { - type: ALERT_SHOW, - title, - message, - severity, - }; -} - -export function showAlertForError(error) { - return (dispatch, _getState) => { - if (error.response) { - const { data, status, statusText } = error.response; - - if (status === 502) { - return dispatch(showAlert('', 'The server is down', 'error')); - } - - if (status === 404 || status === 410) { - // Skip these errors as they are reflected in the UI - return dispatch(noOp); - } - - let message = statusText; - - if (data.error) { - message = data.error; - } - - if (!message) { - message = httpErrorMessages.find((httpError) => httpError.code === status)?.description; - } - - return dispatch(showAlert('', message, 'error')); - } else { - console.error(error); - return dispatch(showAlert(undefined, undefined, 'error')); - } - }; -} diff --git a/app/soapbox/actions/alerts.ts b/app/soapbox/actions/alerts.ts new file mode 100644 index 000000000..b0af2af35 --- /dev/null +++ b/app/soapbox/actions/alerts.ts @@ -0,0 +1,74 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { AxiosError } from 'axios'; +import { defineMessages, MessageDescriptor } from 'react-intl'; + +import { httpErrorMessages } from 'soapbox/utils/errors'; + +import { SnackbarActionSeverity } from './snackbar'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, +}); + +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; + +const noOp = () => { }; + +function dismissAlert(alert: any) { + return { + type: ALERT_DISMISS, + alert, + }; +} + +function showAlert( + title: MessageDescriptor | string = messages.unexpectedTitle, + message: MessageDescriptor | string = messages.unexpectedMessage, + severity: SnackbarActionSeverity = 'info', +) { + return { + type: ALERT_SHOW, + title, + message, + severity, + }; +} + +const showAlertForError = (error: AxiosError) => (dispatch: React.Dispatch, _getState: any) => { + if (error.response) { + const { data, status, statusText } = error.response; + + if (status === 502) { + return dispatch(showAlert('', 'The server is down', 'error')); + } + + if (status === 404 || status === 410) { + // Skip these errors as they are reflected in the UI + return dispatch(noOp as any); + } + + let message: string | undefined = statusText; + + if (data.error) { + message = data.error; + } + + if (!message) { + message = httpErrorMessages.find((httpError) => httpError.code === status)?.description; + } + + return dispatch(showAlert('', message, 'error')); + } else { + console.error(error); + return dispatch(showAlert(undefined, undefined, 'error')); + } +}; + +export { + dismissAlert, + showAlert, + showAlertForError, +}; diff --git a/app/soapbox/actions/snackbar.ts b/app/soapbox/actions/snackbar.ts index d1cda0d94..d4238cf33 100644 --- a/app/soapbox/actions/snackbar.ts +++ b/app/soapbox/actions/snackbar.ts @@ -2,7 +2,7 @@ import { ALERT_SHOW } from './alerts'; import type { MessageDescriptor } from 'react-intl'; -type SnackbarActionSeverity = 'info' | 'success' | 'error' +export type SnackbarActionSeverity = 'info' | 'success' | 'error' type SnackbarMessage = string | MessageDescriptor