From a335c06f1302f90b4d5ac6714aa1b46d5e7b1b56 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 26 May 2022 13:37:22 -0400 Subject: [PATCH] Convert blocks action to TypeScript --- app/soapbox/__fixtures__/blocks.json | 8 + app/soapbox/__mocks__/api.ts | 7 +- app/soapbox/actions/__tests__/blocks.test.ts | 183 +++++++++++++++++++ app/soapbox/actions/blocks.js | 95 ---------- app/soapbox/actions/blocks.ts | 109 +++++++++++ 5 files changed, 306 insertions(+), 96 deletions(-) create mode 100644 app/soapbox/__fixtures__/blocks.json create mode 100644 app/soapbox/actions/__tests__/blocks.test.ts delete mode 100644 app/soapbox/actions/blocks.js create mode 100644 app/soapbox/actions/blocks.ts diff --git a/app/soapbox/__fixtures__/blocks.json b/app/soapbox/__fixtures__/blocks.json new file mode 100644 index 000000000..42e8753c5 --- /dev/null +++ b/app/soapbox/__fixtures__/blocks.json @@ -0,0 +1,8 @@ +[ + { + "id": "22", + "username": "twoods", + "acct": "twoods", + "display_name": "Tiger Woods" + } +] diff --git a/app/soapbox/__mocks__/api.ts b/app/soapbox/__mocks__/api.ts index 99797009e..f81d4a3c4 100644 --- a/app/soapbox/__mocks__/api.ts +++ b/app/soapbox/__mocks__/api.ts @@ -1,6 +1,7 @@ import { jest } from '@jest/globals'; -import { AxiosInstance } from 'axios'; +import { AxiosInstance, AxiosResponse } from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import LinkHeader from 'http-link-header'; const api = jest.requireActual('../api') as Record; let mocks: Array = []; @@ -15,6 +16,10 @@ const setupMock = (axios: AxiosInstance) => { export const staticClient = api.staticClient; +export const getLinks = (response: AxiosResponse): LinkHeader => { + return new LinkHeader(response.headers?.link); +}; + export const baseClient = (...params: any[]) => { const axios = api.baseClient(...params); setupMock(axios); diff --git a/app/soapbox/actions/__tests__/blocks.test.ts b/app/soapbox/actions/__tests__/blocks.test.ts new file mode 100644 index 000000000..55055722a --- /dev/null +++ b/app/soapbox/actions/__tests__/blocks.test.ts @@ -0,0 +1,183 @@ +import { __stub } from 'soapbox/api'; +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { expandBlocks, fetchBlocks } from '../blocks'; + +const account = { + acct: 'twoods', + display_name: 'Tiger Woods', + id: '22', + username: 'twoods', +}; + +describe('fetchBlocks()', () => { + let store; + + describe('if logged out', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', null); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('if logged in', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', '1234'); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + beforeEach(() => { + const blocks = require('soapbox/__fixtures__/blocks.json'); + + __stub((mock) => { + mock.onGet('/api/v1/blocks').reply(200, blocks, { + link: '; rel=\'prev\'', + }); + }); + }); + + it('should fetch blocks from the API', async() => { + const expectedActions = [ + { type: 'BLOCKS_FETCH_REQUEST' }, + { type: 'ACCOUNTS_IMPORT', accounts: [account] }, + { type: 'BLOCKS_FETCH_SUCCESS', accounts: [account], next: null }, + { + type: 'RELATIONSHIPS_FETCH_REQUEST', + ids: ['22'], + skipLoading: true, + }, + ]; + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/blocks').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'BLOCKS_FETCH_REQUEST' }, + { type: 'BLOCKS_FETCH_FAIL', error: new Error('Network Error') }, + ]; + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); +}); + +describe('expandBlocks()', () => { + let store; + + describe('if logged out', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', null); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('if logged in', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', '1234'); + store = mockStore(state); + }); + + describe('without a url', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('user_lists', { blocks: { next: null } }); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('with a url', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('user_lists', { blocks: { next: 'example' } }); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + beforeEach(() => { + const blocks = require('soapbox/__fixtures__/blocks.json'); + + __stub((mock) => { + mock.onGet('example').reply(200, blocks, { + link: '; rel=\'prev\'', + }); + }); + }); + + it('should fetch blocks from the url', async() => { + const expectedActions = [ + { type: 'BLOCKS_EXPAND_REQUEST' }, + { type: 'ACCOUNTS_IMPORT', accounts: [account] }, + { type: 'BLOCKS_EXPAND_SUCCESS', accounts: [account], next: null }, + { + type: 'RELATIONSHIPS_FETCH_REQUEST', + ids: ['22'], + skipLoading: true, + }, + ]; + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('example').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'BLOCKS_EXPAND_REQUEST' }, + { type: 'BLOCKS_EXPAND_FAIL', error: new Error('Network Error') }, + ]; + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); + }); +}); diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js deleted file mode 100644 index 554446f2f..000000000 --- a/app/soapbox/actions/blocks.js +++ /dev/null @@ -1,95 +0,0 @@ -import { isLoggedIn } from 'soapbox/utils/auth'; -import { getNextLinkName } from 'soapbox/utils/quirks'; - -import api, { getLinks } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; -export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; -export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; - -export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; -export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; -export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; - -export function fetchBlocks() { - return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - const nextLinkName = getNextLinkName(getState); - - dispatch(fetchBlocksRequest()); - - api(getState).get('/api/v1/blocks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(fetchBlocksFail(error))); - }; -} - -export function fetchBlocksRequest() { - return { - type: BLOCKS_FETCH_REQUEST, - }; -} - -export function fetchBlocksSuccess(accounts, next) { - return { - type: BLOCKS_FETCH_SUCCESS, - accounts, - next, - }; -} - -export function fetchBlocksFail(error) { - return { - type: BLOCKS_FETCH_FAIL, - error, - }; -} - -export function expandBlocks() { - return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - const nextLinkName = getNextLinkName(getState); - - const url = getState().getIn(['user_lists', 'blocks', 'next']); - - if (url === null) { - return; - } - - dispatch(expandBlocksRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandBlocksFail(error))); - }; -} - -export function expandBlocksRequest() { - return { - type: BLOCKS_EXPAND_REQUEST, - }; -} - -export function expandBlocksSuccess(accounts, next) { - return { - type: BLOCKS_EXPAND_SUCCESS, - accounts, - next, - }; -} - -export function expandBlocksFail(error) { - return { - type: BLOCKS_EXPAND_FAIL, - error, - }; -} diff --git a/app/soapbox/actions/blocks.ts b/app/soapbox/actions/blocks.ts new file mode 100644 index 000000000..af68b7b83 --- /dev/null +++ b/app/soapbox/actions/blocks.ts @@ -0,0 +1,109 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { AxiosError } from 'axios'; + +import { isLoggedIn } from 'soapbox/utils/auth'; +import { getNextLinkName } from 'soapbox/utils/quirks'; + +import api, { getLinks } from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; +const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; +const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; + +const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; +const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; +const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; + +const fetchBlocks = () => (dispatch: React.Dispatch, getState: any) => { + if (!isLoggedIn(getState)) return null; + const nextLinkName = getNextLinkName(getState); + + dispatch(fetchBlocksRequest()); + + return api(getState) + .get('/api/v1/blocks') + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(fetchBlocksFail(error))); +}; + +function fetchBlocksRequest() { + return { type: BLOCKS_FETCH_REQUEST }; +} + +function fetchBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_FETCH_SUCCESS, + accounts, + next, + }; +} + +function fetchBlocksFail(error: AxiosError) { + return { + type: BLOCKS_FETCH_FAIL, + error, + }; +} + +const expandBlocks = () => (dispatch: React.Dispatch, getState: any) => { + if (!isLoggedIn(getState)) return null; + const nextLinkName = getNextLinkName(getState); + + const url = getState().getIn(['user_lists', 'blocks', 'next']); + + if (url === null) { + return null; + } + + dispatch(expandBlocksRequest()); + + return api(getState) + .get(url) + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + dispatch(importFetchedAccounts(response.data)); + dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(expandBlocksFail(error))); +}; + +function expandBlocksRequest() { + return { + type: BLOCKS_EXPAND_REQUEST, + }; +} + +function expandBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_EXPAND_SUCCESS, + accounts, + next, + }; +} + +function expandBlocksFail(error: AxiosError) { + return { + type: BLOCKS_EXPAND_FAIL, + error, + }; +} + +export { + fetchBlocks, + expandBlocks, + BLOCKS_FETCH_REQUEST, + BLOCKS_FETCH_SUCCESS, + BLOCKS_FETCH_FAIL, + BLOCKS_EXPAND_REQUEST, + BLOCKS_EXPAND_SUCCESS, + BLOCKS_EXPAND_FAIL, +};