diff --git a/app/soapbox/actions/__tests__/carousels.test.ts b/app/soapbox/actions/__tests__/carousels.test.ts new file mode 100644 index 000000000..f3d0c4248 --- /dev/null +++ b/app/soapbox/actions/__tests__/carousels.test.ts @@ -0,0 +1,58 @@ +import { __stub } from 'soapbox/api'; +import { mockStore, rootState } from 'soapbox/jest/test-helpers'; + +import { fetchCarouselAvatars } from '../carousels'; + +describe('fetchCarouselAvatars()', () => { + let store; + + beforeEach(() => { + store = mockStore(rootState); + }); + + describe('with a successful API request', () => { + let avatars; + + beforeEach(() => { + avatars = [ + { account_id: '1', username: 'jl', account_avatar: 'https://example.com/some.jpg' }, + ]; + + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/avatars').reply(200, avatars); + }); + }); + + it('should fetch the users from the API', async() => { + const expectedActions = [ + { type: 'CAROUSEL_AVATAR_REQUEST' }, + { type: 'CAROUSEL_AVATAR_SUCCESS', payload: avatars }, + ]; + + await store.dispatch(fetchCarouselAvatars()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/avatars').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'CAROUSEL_AVATAR_REQUEST' }, + { type: 'CAROUSEL_AVATAR_FAIL' }, + ]; + + await store.dispatch(fetchCarouselAvatars()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); diff --git a/app/soapbox/actions/carousels.ts b/app/soapbox/actions/carousels.ts new file mode 100644 index 000000000..7935536c4 --- /dev/null +++ b/app/soapbox/actions/carousels.ts @@ -0,0 +1,25 @@ +import { AxiosResponse } from 'axios'; + +import { AppDispatch, RootState } from 'soapbox/store'; + +import api from '../api'; + +const CAROUSEL_AVATAR_REQUEST = 'CAROUSEL_AVATAR_REQUEST'; +const CAROUSEL_AVATAR_SUCCESS = 'CAROUSEL_AVATAR_SUCCESS'; +const CAROUSEL_AVATAR_FAIL = 'CAROUSEL_AVATAR_FAIL'; + +const fetchCarouselAvatars = () => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CAROUSEL_AVATAR_REQUEST }); + + return api(getState) + .get('/api/v1/truth/carousels/avatars') + .then((response: AxiosResponse) => dispatch({ type: CAROUSEL_AVATAR_SUCCESS, payload: response.data })) + .catch(() => dispatch({ type: CAROUSEL_AVATAR_FAIL })); +}; + +export { + CAROUSEL_AVATAR_REQUEST, + CAROUSEL_AVATAR_SUCCESS, + CAROUSEL_AVATAR_FAIL, + fetchCarouselAvatars, +}; diff --git a/app/soapbox/reducers/__tests__/carousels.test.ts b/app/soapbox/reducers/__tests__/carousels.test.ts new file mode 100644 index 000000000..394a2c037 --- /dev/null +++ b/app/soapbox/reducers/__tests__/carousels.test.ts @@ -0,0 +1,45 @@ +import { AnyAction } from 'redux'; + +import { + CAROUSEL_AVATAR_REQUEST, + CAROUSEL_AVATAR_SUCCESS, + CAROUSEL_AVATAR_FAIL, +} from 'soapbox/actions/carousels'; + +import reducer from '../carousels'; + +describe('carousels reducer', () => { + it('should return the initial state', () => { + expect(reducer(undefined, {} as AnyAction)).toEqual({ + avatars: [], + isLoading: false, + }); + }); + + describe('CAROUSEL_AVATAR_REQUEST', () => { + it('sets "isLoading" to "true"', () => { + const initialState = { isLoading: false, avatars: [] }; + const action = { type: CAROUSEL_AVATAR_REQUEST }; + expect(reducer(initialState, action).isLoading).toEqual(true); + }); + }); + + describe('CAROUSEL_AVATAR_SUCCESS', () => { + it('sets the next state', () => { + const initialState = { isLoading: true, avatars: [] }; + const action = { type: CAROUSEL_AVATAR_SUCCESS, payload: [45] }; + const result = reducer(initialState, action); + + expect(result.isLoading).toEqual(false); + expect(result.avatars).toEqual([45]); + }); + }); + + describe('CAROUSEL_AVATAR_FAIL', () => { + it('sets "isLoading" to "true"', () => { + const initialState = { isLoading: true, avatars: [] }; + const action = { type: CAROUSEL_AVATAR_FAIL }; + expect(reducer(initialState, action).isLoading).toEqual(false); + }); + }); +}); diff --git a/app/soapbox/reducers/carousels.ts b/app/soapbox/reducers/carousels.ts new file mode 100644 index 000000000..47a6c7576 --- /dev/null +++ b/app/soapbox/reducers/carousels.ts @@ -0,0 +1,36 @@ +import { AnyAction } from 'redux'; + +import { + CAROUSEL_AVATAR_REQUEST, + CAROUSEL_AVATAR_SUCCESS, + CAROUSEL_AVATAR_FAIL, +} from '../actions/carousels'; + +type Avatar = { + account_id: string + account_avatar: string + username: string +} + +type CarouselsState = { + avatars: Avatar[], + isLoading: boolean +} + +const initialState: CarouselsState = { + avatars: [], + isLoading: false, +}; + +export default function rules(state: CarouselsState = initialState, action: AnyAction): CarouselsState { + switch (action.type) { + case CAROUSEL_AVATAR_REQUEST: + return { ...state, isLoading: true }; + case CAROUSEL_AVATAR_SUCCESS: + return { ...state, isLoading: false, avatars: action.payload }; + case CAROUSEL_AVATAR_FAIL: + return { ...state, isLoading: false }; + default: + return state; + } +} diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index b0d3ee03f..7f33dad1c 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -14,6 +14,7 @@ import alerts from './alerts'; import aliases from './aliases'; import auth from './auth'; import backups from './backups'; +import carousels from './carousels'; import chat_message_lists from './chat_message_lists'; import chat_messages from './chat_messages'; import chats from './chats'; @@ -122,6 +123,7 @@ const reducers = { onboarding, rules, history, + carousels, }; // Build a default state from all reducers: it has the key and `undefined`