From a144dd00e360bedbba961085dee817e8fa015c4b Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Thu, 7 Jan 2021 00:15:35 +0300 Subject: [PATCH] Remove `any` from codebase --- frontend/.eslintrc.js | 2 ++ frontend/app/__mocks__/headers.ts | 2 +- frontend/app/common/fetcher.test.ts | 8 ++++---- frontend/app/common/local-storage.test.ts | 4 ++-- frontend/app/common/local-storage.ts | 11 ++++++----- .../components/auth-panel/auth-panel.test.tsx | 5 +++-- .../auth__email-login-form.test.tsx | 16 ++++++++++------ .../comment-form__subscribe-by-email.test.tsx | 7 +++---- .../comment-form/comment-form.test.tsx | 5 +++-- .../app/components/comment/comment.test.tsx | 6 +++--- frontend/app/hooks/useAction.ts | 1 + frontend/app/store/thread/reducers.test.ts | 7 ++++--- frontend/app/store/user/reducers.test.ts | 19 ++++++++++++------- frontend/app/utils/debounce.ts | 2 +- 14 files changed, 55 insertions(+), 40 deletions(-) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index af7c190e..d807e638 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -21,12 +21,14 @@ module.exports = { 'no-undef': 'off', 'no-redeclare': 'off', 'no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'error', }, }, { files: ['*.d.ts'], rules: { '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }, { diff --git a/frontend/app/__mocks__/headers.ts b/frontend/app/__mocks__/headers.ts index e89fbad5..0efddd1d 100644 --- a/frontend/app/__mocks__/headers.ts +++ b/frontend/app/__mocks__/headers.ts @@ -16,7 +16,7 @@ global.Headers = class HeadersMock extends Headers implements Headers { delete(key: string) { this.headers.delete(key); } - forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) { + forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown) { this.headers.forEach((value, key) => { callbackfn.call(thisArg || this, value, key, this); }); diff --git a/frontend/app/common/fetcher.test.ts b/frontend/app/common/fetcher.test.ts index 23ee9a7b..0da74117 100644 --- a/frontend/app/common/fetcher.test.ts +++ b/frontend/app/common/fetcher.test.ts @@ -28,9 +28,9 @@ describe('fetcher', () => { }); it('should throw special error object on 401 status', async () => { const response = 'unauthorized nginx response'; - (window.fetch as any) = jest.fn().mockImplementation(async () => ({ + window.fetch = jest.fn().mockImplementation(async () => ({ status: 401, - headers: new (window as any).Headers(), + headers: new Headers(), json: async () => { throw new Error('json parse error'); }, @@ -48,9 +48,9 @@ describe('fetcher', () => { }); }); it('should throw "Something went wrong." object on unknown status', async () => { - (jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({ + window.fetch = jest.fn().mockImplementation(async () => ({ status: 400, - headers: new (window as any).Headers(), + headers: new Headers(), async json() { throw new Error('json parse error'); }, diff --git a/frontend/app/common/local-storage.test.ts b/frontend/app/common/local-storage.test.ts index 68f73e15..5c633f46 100644 --- a/frontend/app/common/local-storage.test.ts +++ b/frontend/app/common/local-storage.test.ts @@ -12,7 +12,7 @@ describe('getJsonItem', () => { }); it('should update json in localStoeage', () => { - setJsonItem(LS_KEY, []); + setJsonItem(LS_KEY, []); expect(localStorage.getItem(LS_KEY)).toBe('[]'); }); }); @@ -88,7 +88,7 @@ describe('updateJsonItem', () => { it('should update data in localStorage with merge', () => { localStorage.setItem(LS_KEY, JSON.stringify([3, 4, 5])); - updateJsonItem(LS_KEY, data => [1, 2, ...data]); + updateJsonItem(LS_KEY, (data: unknown[]) => [1, 2, ...data]); expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify([1, 2, 3, 4, 5])); }); diff --git a/frontend/app/common/local-storage.ts b/frontend/app/common/local-storage.ts index 3367bb02..cf4a3313 100644 --- a/frontend/app/common/local-storage.ts +++ b/frontend/app/common/local-storage.ts @@ -21,7 +21,7 @@ export const removeItem = IS_STORAGE_AVAILABLE console.error(failMessage); // eslint-disable-line no-console }; -export function getJsonItem(key: string): T | null { +export function getJsonItem(key: string): T | null { try { const json = getItem(key); @@ -38,7 +38,7 @@ export function getJsonItem(key: string): T | null { } } -export function setJsonItem(key: string, data: T) { +export function setJsonItem(key: string, data: T) { try { setItem(key, JSON.stringify(data)); } catch (e) { @@ -48,10 +48,11 @@ export function setJsonItem(key: string, data: T) { export function updateJsonItem(key: string, value: (data: T) => T): void; export function updateJsonItem(key: string, value: T): void; -export function updateJsonItem>(key: string, value: T) { - const savedData = getJsonItem(key); +export function updateJsonItem(key: string, value: T): void; +export function updateJsonItem(key: string, value: T) { + const savedData = getJsonItem(key); - if (Array.isArray(value)) { + if (Array.isArray(value) && Array.isArray(savedData)) { setJsonItem(key, [...savedData, ...value]); return; } diff --git a/frontend/app/components/auth-panel/auth-panel.test.tsx b/frontend/app/components/auth-panel/auth-panel.test.tsx index 32df5c62..ef908534 100644 --- a/frontend/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/app/components/auth-panel/auth-panel.test.tsx @@ -5,6 +5,7 @@ import { Middleware } from 'redux'; import { Provider } from 'react-redux'; import { IntlProvider } from 'react-intl'; +import type { User } from 'common/types'; import enMessages from 'locales/en.json'; import AuthPanel, { Props } from './auth-panel'; @@ -61,7 +62,7 @@ describe('', () => { ...DefaultProps, user: null, postInfo: { ...DefaultProps.postInfo, read_only: true }, - hiddenUsers: { hidden_joe: {} as any }, + hiddenUsers: { hidden_joe: {} as User }, } as Props); const adminAction = element.find('.auth-panel__admin-action'); @@ -76,7 +77,7 @@ describe('', () => { ...DefaultProps, user: null, postInfo: { ...DefaultProps.postInfo, read_only: true }, - hiddenUsers: { hidden_joe: {} as any }, + hiddenUsers: { hidden_joe: {} as User }, } as Props); const firstCol = element.find('.auth-panel__column').first(); diff --git a/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx b/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx index 8f7595b0..e118d50c 100644 --- a/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx +++ b/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx @@ -20,22 +20,26 @@ jest.mock('utils/jwt', () => ({ jest.mock('common/api'); +const sendEmailVerificationRequestMock = sendEmailVerificationRequest as jest.Mock< + ReturnType +>; + function simulateInput(input: ReactWrapper, value: string) { input.getDOMNode().value = value; input.simulate('input'); } describe('EmailLoginForm', () => { - const testUser = ({} as any) as User; + const testUser = {} as User; const onSuccess = jest.fn(async () => undefined); const onSignIn = jest.fn(async () => testUser); beforeEach(() => { - (sendEmailVerificationRequest as any).mockReset(); + sendEmailVerificationRequestMock.mockReset(); }); it('works', async () => { - (sendEmailVerificationRequest as any).mockResolvedValueOnce({}); + sendEmailVerificationRequestMock.mockResolvedValueOnce(); const el = mount( @@ -45,7 +49,7 @@ describe('EmailLoginForm', () => { simulateInput(el.find(`input[name="username"]`), 'someone'); el.find('form').simulate('submit'); await sleep(100); - expect(sendEmailVerificationRequest).toBeCalledWith('someone', 'someone@example.com'); + expect(sendEmailVerificationRequestMock).toBeCalledWith('someone', 'someone@example.com'); el.update(); simulateInput(el.find(`textarea[name="token"]`), 'abcd'); @@ -58,7 +62,7 @@ describe('EmailLoginForm', () => { }); it('should send form by pasting token', async () => { - (sendEmailVerificationRequest as any).mockResolvedValueOnce({}); + sendEmailVerificationRequestMock.mockResolvedValueOnce(); const onSignIn = jest.fn(async () => testUser); const wrapper = mount( @@ -78,7 +82,7 @@ describe('EmailLoginForm', () => { }); it('should show error "Token is expired" on paste', async () => { - (sendEmailVerificationRequest as any).mockResolvedValueOnce({}); + sendEmailVerificationRequestMock.mockResolvedValueOnce(); const onSignIn = jest.fn(async () => testUser); const wrapper = mount( diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx index 26500709..7ab069f8 100644 --- a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx @@ -15,7 +15,6 @@ import { sleep } from 'utils/sleep'; import { Input } from 'components/input'; import { Button } from 'components/button'; import { Dropdown } from 'components/dropdown'; -import TextareaAutosize from 'components/comment-form/textarea-autosize'; import enMessages from 'locales/en.json'; import { LS_EMAIL_KEY } from 'common/constants'; @@ -161,10 +160,10 @@ describe('', () => { await sleep(0); wrapper.update(); - const textarea = wrapper.find(TextareaAutosize); - const onInputToken = textarea.prop('onInput') as (e: any) => void; + const textarea = wrapper.find('textarea'); - act(() => onInputToken(makeInputEvent(validToken))); + textarea.getDOMNode().value = validToken; + textarea.simulate('input'); await sleep(0); wrapper.update(); diff --git a/frontend/app/components/comment-form/comment-form.test.tsx b/frontend/app/components/comment-form/comment-form.test.tsx index 202d9365..c72dc70a 100644 --- a/frontend/app/components/comment-form/comment-form.test.tsx +++ b/frontend/app/components/comment-form/comment-form.test.tsx @@ -9,8 +9,9 @@ import * as localStorageModule from 'common/local-storage'; import { CommentForm, CommentFormProps, messages } from './comment-form'; import { SubscribeByEmail } from './__subscribe-by-email'; import TextareaAutosize from './textarea-autosize'; +import { IntlShape } from 'react-intl'; -function createEvent(type: string, value: T): E { +function createEvent(type: string, value: T): E { const event = new Event(type); Object.defineProperty(event, 'target', { value }); @@ -31,7 +32,7 @@ const intl = { formatMessage(message: { defaultMessage: string }) { return message.defaultMessage || ''; }, -} as any; +} as IntlShape; describe('', () => { it('should shallow without control panel, preview button, and rss links in "simple view" mode', () => { diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index ca467f39..6c0eef97 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -1,6 +1,6 @@ import { h } from 'preact'; import { mount as enzymeMount } from 'enzyme'; -import { IntlProvider } from 'react-intl'; +import { IntlProvider, IntlShape } from 'react-intl'; import enMessages from 'locales/en.json'; import type { User, Comment as CommentType, PostInfo } from 'common/types'; @@ -9,7 +9,7 @@ import { sleep } from 'utils/sleep'; import Comment, { CommentProps } from './comment'; -const mount = (component: any) => +const mount = (component: T) => enzymeMount( {component} @@ -20,7 +20,7 @@ const intl = { formatMessage(message: { defaultMessage: string }) { return message.defaultMessage || ''; }, -} as any; +} as IntlShape; const DefaultProps: Partial = { CommentForm: null, diff --git a/frontend/app/hooks/useAction.ts b/frontend/app/hooks/useAction.ts index 6014d6f6..0363773c 100644 --- a/frontend/app/hooks/useAction.ts +++ b/frontend/app/hooks/useAction.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useMemo } from 'preact/compat'; import { useDispatch } from 'react-redux'; import { BoundActionCreator, BoundActionCreators } from 'utils/actionBinder'; diff --git a/frontend/app/store/thread/reducers.test.ts b/frontend/app/store/thread/reducers.test.ts index ed673b2f..c463744c 100644 --- a/frontend/app/store/thread/reducers.test.ts +++ b/frontend/app/store/thread/reducers.test.ts @@ -1,4 +1,5 @@ import { Comment } from 'common/types'; +import { StoreState } from 'store'; import { setCollapse } from './actions'; import { THREAD_SET_COLLAPSE } from './types'; @@ -6,11 +7,11 @@ import { THREAD_SET_COLLAPSE } from './types'; describe('collapsedThreads', () => { it('should set collapsed to true', () => { const comment = { id: 'some-id' } as Comment; - const node = { comment, replies: [] }; - const state = { collapsedThreads: {}, comments: [node] }; + const state = { collapsedThreads: {} } as StoreState; const dispatch = jest.fn(); - const getState = jest.fn(() => state) as any; + const getState = jest.fn(() => state); + setCollapse(comment.id, true)(dispatch, getState, undefined); expect(dispatch).toBeCalledWith({ type: THREAD_SET_COLLAPSE, diff --git a/frontend/app/store/user/reducers.test.ts b/frontend/app/store/user/reducers.test.ts index a73390b3..409c77bc 100644 --- a/frontend/app/store/user/reducers.test.ts +++ b/frontend/app/store/user/reducers.test.ts @@ -1,12 +1,16 @@ -import * as api from 'common/api'; +import { getUser, logIn as logInApi, logOut } from 'common/api'; import { User } from 'common/types'; import { fetchUser, logIn, logout } from './actions'; import { user } from './reducers'; -import { USER_SET } from './types'; +import { USER_ACTIONS, USER_SET } from './types'; jest.mock('common/api'); +const getUserMock = (getUser as unknown) as jest.Mock>; +const logInMock = (logInApi as unknown) as jest.Mock>; +const logOutMock = (logOut as unknown) as jest.Mock>; + afterEach(() => { jest.resetModules(); }); @@ -14,12 +18,13 @@ afterEach(() => { describe('user', () => { it('should return null by default', () => { const action = { type: 'OTHER' }; - const newState = user(null, action as any); + const newState = user(null, action as USER_ACTIONS); + expect(newState).toEqual(null); }); it('should set state of user on fetchUser', async () => { - (api.getUser as any).mockImplementation( + getUserMock.mockImplementation( async (): Promise => ({ id: 'john', @@ -41,7 +46,7 @@ describe('user', () => { }); it('should set state of user on logIn', async () => { - (api.logIn as any).mockImplementation( + logInMock.mockImplementation( async (): Promise => ({ id: 'john', @@ -63,7 +68,7 @@ describe('user', () => { }); it('should NOT set state of user on failed logIn', async () => { - (api.logIn as any).mockImplementation( + logInMock.mockImplementation( async (): Promise => { throw new Error('Unauthorized'); } @@ -75,7 +80,7 @@ describe('user', () => { }); it('should unset user on logOut', async () => { - (api.logOut as any).mockImplementation(async (): Promise => undefined); + logOutMock.mockImplementation(async (): Promise => undefined); const dispatch = jest.fn(); const getState = jest.fn(); await logout()(dispatch, getState, undefined); diff --git a/frontend/app/utils/debounce.ts b/frontend/app/utils/debounce.ts index b1b622d3..ea2d9b1b 100644 --- a/frontend/app/utils/debounce.ts +++ b/frontend/app/utils/debounce.ts @@ -6,7 +6,7 @@ export default function debounce( ): (...args: Parameters>) => void { let timeout: number | undefined; - return function (this: any, ...args): void { + return function (this: unknown, ...args): void { const laterCall = (): unknown => fn.apply(this, args); window.clearTimeout(timeout); timeout = window.setTimeout(laterCall, wait);