diff --git a/package.json b/package.json index 948c50c354..79f643dde1 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,12 @@ "*.dic.js" ], "rules": { + "@typescript-eslint/explicit-function-return-type": [ + "warn", + { + "allowExpressions": true + } + ], "@typescript-eslint/no-empty-interface": "warn", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-inferrable-types": "warn", diff --git a/src/backend/index.ts b/src/backend/index.ts index d81f53ae64..c3266860a5 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -109,11 +109,12 @@ const wordApi = new Api.WordApi(config, BASE_PATH, axiosInstance); // Backend controllers receiving a file via a "[FromForm] FileUpload fileUpload" param // have the internal fields expanded by openapi-generator as params in our Api. +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type function fileUpload(file: File) { return { file, filePath: "", name: "" }; } -function defaultOptions() { +function defaultOptions(): object { return { headers: authHeader() }; } diff --git a/src/backend/tests/localStorage.test.tsx b/src/backend/tests/localStorage.test.tsx index 6bfe8e2f75..31b02a86b1 100644 --- a/src/backend/tests/localStorage.test.tsx +++ b/src/backend/tests/localStorage.test.tsx @@ -32,7 +32,7 @@ afterAll(() => { } }); -function expectAllEmpty() { +function expectAllEmpty(): void { expect(LocalStorage.getAvatar()).toEqual(""); expect(LocalStorage.getClosedBanner()).toEqual(""); expect(LocalStorage.getCurrentUser()).toEqual(undefined); diff --git a/src/components/AnnouncementBanner/AnnouncementBanner.tsx b/src/components/AnnouncementBanner/AnnouncementBanner.tsx index 4a6d838af8..5db0fffa96 100644 --- a/src/components/AnnouncementBanner/AnnouncementBanner.tsx +++ b/src/components/AnnouncementBanner/AnnouncementBanner.tsx @@ -3,6 +3,7 @@ import { Box, IconButton, Toolbar, Typography } from "@mui/material"; import { CSSProperties, Fragment, + ReactElement, useCallback, useEffect, useState, @@ -17,7 +18,7 @@ import { useAppSelector } from "types/hooks"; import { Path } from "types/path"; import theme, { themeColors } from "types/theme"; -export default function AnnouncementBanner() { +export default function AnnouncementBanner(): ReactElement { const [banner, setBanner] = useState(""); const [margins, setMargins] = useState({}); @@ -40,7 +41,7 @@ export default function AnnouncementBanner() { }); }, [loc, calculateMargins]); - function closeBanner() { + function closeBanner(): void { setClosedBanner(banner); setBanner(""); } diff --git a/src/components/App/AppLoggedIn.tsx b/src/components/App/AppLoggedIn.tsx index 9b3ceee085..713866723c 100644 --- a/src/components/App/AppLoggedIn.tsx +++ b/src/components/App/AppLoggedIn.tsx @@ -57,7 +57,7 @@ export default function AppWithBar(): ReactElement { } }, [proj]); - const overrideThemeFont = (theme: Theme) => + const overrideThemeFont = (theme: Theme): Theme => styleOverrides ? createTheme({ ...theme, diff --git a/src/components/App/SignalRHub.tsx b/src/components/App/SignalRHub.tsx index 7b829f8c5d..0606481cda 100644 --- a/src/components/App/SignalRHub.tsx +++ b/src/components/App/SignalRHub.tsx @@ -3,7 +3,13 @@ import { HubConnectionBuilder, HubConnectionState, } from "@microsoft/signalr"; -import { Fragment, useCallback, useEffect, useState } from "react"; +import { + Fragment, + ReactElement, + useCallback, + useEffect, + useState, +} from "react"; import { baseURL } from "backend"; import { getUserId } from "backend/localStorage"; @@ -16,7 +22,7 @@ import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; /** A central hub for monitoring export status on SignalR */ -export default function SignalRHub() { +export default function SignalRHub(): ReactElement { const exportState = useAppSelector( (state: StoreState) => state.exportProjectState ); diff --git a/src/components/AppBar/tests/AppBarComponent.test.tsx b/src/components/AppBar/tests/AppBarComponent.test.tsx index b58fc64f49..b1f9796539 100644 --- a/src/components/AppBar/tests/AppBarComponent.test.tsx +++ b/src/components/AppBar/tests/AppBarComponent.test.tsx @@ -18,7 +18,7 @@ jest.mock("backend", () => ({ const mockStore = configureMockStore()(defaultState); -function setMockFunctions() { +function setMockFunctions(): void { mockGetUser.mockResolvedValue(mockUser); } diff --git a/src/components/AppBar/tests/ProjectButtons.test.tsx b/src/components/AppBar/tests/ProjectButtons.test.tsx index 186e422b3b..ed6060897c 100644 --- a/src/components/AppBar/tests/ProjectButtons.test.tsx +++ b/src/components/AppBar/tests/ProjectButtons.test.tsx @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -30,15 +30,15 @@ const mockProjectId = "proj-id"; const mockProjectRoles: { [key: string]: string } = {}; mockProjectRoles[mockProjectId] = "non-empty-string"; -let testRenderer: renderer.ReactTestRenderer; +let testRenderer: ReactTestRenderer; const mockStore = configureMockStore()({ currentProjectState: { project: { name: "" } }, }); -const renderProjectButtons = async (path = Path.Root) => { - await renderer.act(async () => { - testRenderer = renderer.create( +const renderProjectButtons = async (path = Path.Root): Promise => { + await act(async () => { + testRenderer = create( diff --git a/src/components/AppBar/tests/UserMenu.test.tsx b/src/components/AppBar/tests/UserMenu.test.tsx index 5a9c8189bf..3a34f8f562 100644 --- a/src/components/AppBar/tests/UserMenu.test.tsx +++ b/src/components/AppBar/tests/UserMenu.test.tsx @@ -1,6 +1,6 @@ import { Button, MenuItem } from "@mui/material"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { act, create, ReactTestRenderer } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -21,7 +21,7 @@ jest.mock("react-router-dom", () => ({ useNavigate: jest.fn(), })); -let testRenderer: renderer.ReactTestRenderer; +let testRenderer: ReactTestRenderer; const mockStore = configureMockStore()(); @@ -30,7 +30,7 @@ const mockGetUserId = jest.fn(); const mockUser = newUser(); const mockUserId = "mockUserId"; -function setMockFunctions() { +function setMockFunctions(): void { mockGetUser.mockResolvedValue(mockUser); mockGetUserId.mockReturnValue(mockUserId); } @@ -41,9 +41,9 @@ beforeEach(() => { }); describe("UserMenu", () => { - it("renders without crashing", () => { - renderer.act(() => { - testRenderer = renderer.create( + it("renders without crashing", async () => { + await act(async () => { + testRenderer = create( @@ -59,18 +59,18 @@ describe("UserMenu", () => { expect(await getIsAdmin()).toBeTruthy(); }); - it("UserMenuList has one more item for admins (Site Settings)", () => { - renderMenuList(); + it("UserMenuList has one more item for admins (Site Settings)", async () => { + await renderMenuList(); const normalMenuItems = testRenderer.root.findAllByType(MenuItem).length; - renderMenuList(true); + await renderMenuList(true); const adminMenuItems = testRenderer.root.findAllByType(MenuItem).length; expect(adminMenuItems).toBe(normalMenuItems + 1); }); }); -function renderMenuList(isAdmin = false) { - renderer.act(() => { - testRenderer = renderer.create( +async function renderMenuList(isAdmin = false): Promise { + await act(async () => { + testRenderer = create( diff --git a/src/components/Buttons/FileInputButton.tsx b/src/components/Buttons/FileInputButton.tsx index 3ca32fe9f3..75e75ed863 100644 --- a/src/components/Buttons/FileInputButton.tsx +++ b/src/components/Buttons/FileInputButton.tsx @@ -1,17 +1,17 @@ import { Button } from "@mui/material"; import { ButtonProps } from "@mui/material/Button"; -import React, { ReactElement } from "react"; +import { ReactElement, ReactNode } from "react"; interface BrowseProps { updateFile: (file: File) => void; accept?: string; - children?: React.ReactNode; + children?: ReactNode; buttonProps?: ButtonProps; } // This button links to a set of functions export default function FileInputButton(props: BrowseProps): ReactElement { - function updateFirstFile(files: FileList) { + function updateFirstFile(files: FileList): void { const file = files[0]; if (file) { props.updateFile(file); @@ -19,7 +19,7 @@ export default function FileInputButton(props: BrowseProps): ReactElement { } return ( - + <> {/* The actual file input element is hidden... */} - + ); } diff --git a/src/components/Buttons/PartOfSpeechButton.tsx b/src/components/Buttons/PartOfSpeechButton.tsx index 23feb1158b..05db4aa1fb 100644 --- a/src/components/Buttons/PartOfSpeechButton.tsx +++ b/src/components/Buttons/PartOfSpeechButton.tsx @@ -32,7 +32,7 @@ export default function PartOfSpeech(props: PartOfSpeechProps): ReactElement { catGroupText ); - const CatGroupButton = () => ( + const CatGroupButton = (): ReactElement => ( } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx index f69dba7441..5e6377311c 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx @@ -1,6 +1,6 @@ import { Delete } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; -import React, { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { CancelConfirmDialog } from "components/Dialogs"; @@ -18,11 +18,11 @@ interface DeleteEntryProps { /** * A delete button */ -export default function DeleteEntry(props: DeleteEntryProps) { +export default function DeleteEntry(props: DeleteEntryProps): ReactElement { const [open, setOpen] = useState(false); const { t } = useTranslation(); - function handleClick() { + function handleClick(): void { if (props.confirmId) { setOpen(true); } else { @@ -31,7 +31,7 @@ export default function DeleteEntry(props: DeleteEntryProps) { } return ( - + <> - + ); } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx index c80f1c2290..142a48e4d1 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx @@ -1,6 +1,6 @@ import { AddComment, Comment } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; -import React, { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { EditTextDialog } from "components/Dialogs"; @@ -14,12 +14,12 @@ interface EntryNoteProps { /** * A note adding/editing button */ -export default function EntryNote(props: EntryNoteProps) { +export default function EntryNote(props: EntryNoteProps): ReactElement { const [noteOpen, setNoteOpen] = useState(false); const { t } = useTranslation(); return ( - + <> - + ); } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx index bfe5e16b09..6f885f46a4 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx @@ -1,5 +1,10 @@ import { AddComment, Comment } from "@mui/icons-material"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import "tests/reactI18nextMock"; @@ -7,12 +12,12 @@ import EntryNote from "components/DataEntry/DataEntryTable/EntryCellComponents/E const mockText = "Test text"; -let testMaster: renderer.ReactTestRenderer; -let testHandle: renderer.ReactTestInstance; +let testMaster: ReactTestRenderer; +let testHandle: ReactTestInstance; -function renderWithText(text: string) { - renderer.act(() => { - testMaster = renderer.create( +async function renderWithText(text: string): Promise { + await act(async () => { + testMaster = create( ); }); @@ -20,14 +25,14 @@ function renderWithText(text: string) { } describe("DeleteEntry", () => { - it("renders without note", () => { - renderWithText(""); + it("renders without note", async () => { + await renderWithText(""); expect(testHandle.findAllByType(AddComment).length).toBe(1); expect(testHandle.findAllByType(Comment).length).toBe(0); }); - it("renders with note", () => { - renderWithText(mockText); + it("renders with note", async () => { + await renderWithText(mockText); expect(testHandle.findAllByType(AddComment).length).toBe(0); expect(testHandle.findAllByType(Comment).length).toBe(1); }); diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx index 19cbb1c910..11132c97f4 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx @@ -54,7 +54,7 @@ interface SenseListProps { analysisLang: string; } -export function SenseList(props: SenseListProps) { +export function SenseList(props: SenseListProps): ReactElement { const { t } = useTranslation(); const hasPartsOfSpeech = !!props.selectedWord.senses.find( diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index 82e08ae0d8..77e96dc084 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -54,7 +54,7 @@ interface VernListProps { analysisLang?: string; } -export function VernList(props: VernListProps) { +export function VernList(props: VernListProps): ReactElement { const { t } = useTranslation(); const hasPartsOfSpeech = !!props.vernacularWords.find((w) => diff --git a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx index 0cd97a629a..fc8525608b 100644 --- a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx @@ -1,6 +1,11 @@ import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -38,12 +43,12 @@ const mockUpdateGloss = jest.fn(); const mockUpdateNote = jest.fn(); const mockUpdateVern = jest.fn(); -let testMaster: renderer.ReactTestRenderer; -let testHandle: renderer.ReactTestInstance; +let testMaster: ReactTestRenderer; +let testHandle: ReactTestInstance; -function renderWithWord(word: Word) { - renderer.act(() => { - testMaster = renderer.create( +async function renderWithWord(word: Word): Promise { + await act(async () => { + testMaster = create( @@ -75,14 +80,14 @@ beforeEach(() => { describe("ExistingEntry", () => { describe("component", () => { - it("renders recorder and no players", () => { - renderWithWord(mockWord); + it("renders recorder and no players", async () => { + await renderWithWord(mockWord); expect(testHandle.findAllByType(AudioPlayer).length).toEqual(0); expect(testHandle.findAllByType(AudioRecorder).length).toEqual(1); }); - it("renders recorder and 3 players", () => { - renderWithWord({ ...mockWord, audio: ["a.wav", "b.wav", "c.wav"] }); + it("renders recorder and 3 players", async () => { + await renderWithWord({ ...mockWord, audio: ["a.wav", "b.wav", "c.wav"] }); expect(testHandle.findAllByType(AudioPlayer).length).toEqual(3); expect(testHandle.findAllByType(AudioRecorder).length).toEqual(1); }); @@ -90,10 +95,10 @@ describe("ExistingEntry", () => { describe("vernacular", () => { it("updates if changed", async () => { - renderWithWord(mockWord); + await renderWithWord(mockWord); testHandle = testHandle.findByType(VernWithSuggestions); - async function updateVernAndBlur(text: string) { - await renderer.act(async () => { + async function updateVernAndBlur(text: string): Promise { + await act(async () => { await testHandle.props.updateVernField(text); await testHandle.props.onBlur(); }); @@ -108,10 +113,10 @@ describe("ExistingEntry", () => { describe("gloss", () => { it("updates if changed", async () => { - renderWithWord(mockWord); + await renderWithWord(mockWord); testHandle = testHandle.findByType(GlossWithSuggestions); - async function updateGlossAndBlur(text: string) { - await renderer.act(async () => { + async function updateGlossAndBlur(text: string): Promise { + await act(async () => { await testHandle.props.updateGlossField(text); await testHandle.props.onBlur(); }); @@ -125,10 +130,10 @@ describe("ExistingEntry", () => { }); describe("note", () => { - it("updates text", () => { - renderWithWord(mockWord); + it("updates text", async () => { + await renderWithWord(mockWord); testHandle = testHandle.findByType(EntryNote).findByType(EditTextDialog); - renderer.act(() => { + await act(async () => { testHandle.props.updateText(mockText); }); expect(mockUpdateNote).toBeCalledWith(mockText); diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index c10380ebb4..e8e0e70342 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -387,7 +387,7 @@ describe("DataEntryTable", () => { const vern = "vern"; const glossDef = "gloss"; const noteText = "note"; - act(() => { + await act(async () => { testHandle.props.setNewVern(vern); testHandle.props.setNewGloss(glossDef); testHandle.props.setNewNote(noteText); diff --git a/src/components/Dialogs/ButtonConfirmation.tsx b/src/components/Dialogs/ButtonConfirmation.tsx index 0c3f0d798d..c348b05ef5 100644 --- a/src/components/Dialogs/ButtonConfirmation.tsx +++ b/src/components/Dialogs/ButtonConfirmation.tsx @@ -30,7 +30,7 @@ export default function ButtonConfirmation( const [loading, setLoading] = useState(false); const { t } = useTranslation(); - async function onConfirm() { + async function onConfirm(): Promise { setLoading(true); await props.onConfirm(); setLoading(false); diff --git a/src/components/Dialogs/DeleteEditTextDialog.tsx b/src/components/Dialogs/DeleteEditTextDialog.tsx index afb2faf7ec..afbc9c8a87 100644 --- a/src/components/Dialogs/DeleteEditTextDialog.tsx +++ b/src/components/Dialogs/DeleteEditTextDialog.tsx @@ -40,12 +40,12 @@ export default function DeleteEditTextDialog( const [text, setText] = useState(props.text); const { t } = useTranslation(); - function onCancel() { + function onCancel(): void { setText(props.text); props.close(); } - function onDelete() { + function onDelete(): void { setText(props.text); if (props.onDelete) { props.onDelete(); @@ -53,20 +53,23 @@ export default function DeleteEditTextDialog( props.close(); } - async function onSave() { + async function onSave(): Promise { setLoading(true); await props.updateText(text); setLoading(false); props.close(); } - function escapeClose(_: any, reason: "backdropClick" | "escapeKeyDown") { + function escapeClose( + _: any, + reason: "backdropClick" | "escapeKeyDown" + ): void { if (reason === "escapeKeyDown") { onCancel(); } } - function confirmIfEnter(event: React.KeyboardEvent) { + function confirmIfEnter(event: React.KeyboardEvent): void { if (event.key === Key.Enter) { onSave(); } diff --git a/src/components/Dialogs/EditTextDialog.tsx b/src/components/Dialogs/EditTextDialog.tsx index b891c283fc..976a3c4d88 100644 --- a/src/components/Dialogs/EditTextDialog.tsx +++ b/src/components/Dialogs/EditTextDialog.tsx @@ -35,25 +35,28 @@ export default function EditTextDialog( const [text, setText] = useState(props.text); const { t } = useTranslation(); - async function onConfirm() { + async function onConfirm(): Promise { props.close(); if (text !== props.text) { await props.updateText(text); } } - function onCancel() { + function onCancel(): void { setText(props.text); props.close(); } - function escapeClose(_: any, reason: "backdropClick" | "escapeKeyDown") { + function escapeClose( + _: any, + reason: "backdropClick" | "escapeKeyDown" + ): void { if (reason === "escapeKeyDown") { props.close(); } } - function confirmIfEnter(event: React.KeyboardEvent) { + function confirmIfEnter(event: React.KeyboardEvent): void { if (event.key === Key.Enter) { onConfirm(); } diff --git a/src/components/GoalTimeline/Redux/GoalActions.ts b/src/components/GoalTimeline/Redux/GoalActions.ts index dce3840252..66db597cc0 100644 --- a/src/components/GoalTimeline/Redux/GoalActions.ts +++ b/src/components/GoalTimeline/Redux/GoalActions.ts @@ -216,7 +216,7 @@ export async function loadGoalData(goalType: GoalType): Promise { } } -async function saveCurrentStep(goal: Goal) { +async function saveCurrentStep(goal: Goal): Promise { const userEditId = getUserEditId(); if (userEditId) { const step = goal.steps[goal.currentStep]; diff --git a/src/components/GoalTimeline/index.tsx b/src/components/GoalTimeline/index.tsx index 93c6074935..b354192fe7 100644 --- a/src/components/GoalTimeline/index.tsx +++ b/src/components/GoalTimeline/index.tsx @@ -68,7 +68,8 @@ export default function GoalTimeline(): ReactElement { (state: StoreState) => state.goalsState ); - const chooseGoal = (goal: Goal) => dispatch(asyncAddGoal(goal)); + const chooseGoal = (goal: Goal): Promise => + dispatch(asyncAddGoal(goal)); const [availableGoalTypes, setAvailableGoalTypes] = useState([]); const [suggestedGoalTypes, setSuggestedGoalTypes] = useState([]); @@ -86,7 +87,7 @@ export default function GoalTimeline(): ReactElement { dispatch(asyncGetUserEdits()); setLoaded(true); } - const updateHasGraylist = async () => + const updateHasGraylist = async (): Promise => setHasGraylist( await getGraylistEntries(1).then((res) => res.length !== 0) ); diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 5af9ca0bc0..b6d1d03ff1 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -61,7 +61,7 @@ const mockGetUser = jest.fn(); const mockGetUserEditById = jest.fn(); const mockNavigate = jest.fn(); const mockUpdateUser = jest.fn(); -function setMockFunctions() { +function setMockFunctions(): void { mockAddGoalToUserEdit.mockResolvedValue(0); mockAddStepToGoal.mockResolvedValue(0); mockCreateUserEdit.mockResolvedValue(mockUser); @@ -108,7 +108,7 @@ const mockUser = newUser("First Last", "username"); mockUser.id = mockUserId; mockUser.workedProjects[mockProjectId] = mockUserEditId; -function setupLocalStorage() { +function setupLocalStorage(): void { LocalStorage.setCurrentUser(mockUser); LocalStorage.setProjectId(mockProjectId); } diff --git a/src/components/Login/LoginPage/LoginComponent.tsx b/src/components/Login/LoginPage/LoginComponent.tsx index f9b02ef6be..3291d02cff 100644 --- a/src/components/Login/LoginPage/LoginComponent.tsx +++ b/src/components/Login/LoginPage/LoginComponent.tsx @@ -9,7 +9,7 @@ import { TextField, Typography, } from "@mui/material"; -import { Component } from "react"; +import { Component, ReactElement } from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import { BannerType } from "api/models"; @@ -71,7 +71,7 @@ export class Login extends Component { }; } - componentDidMount() { + componentDidMount(): void { this.props.reset(); getBannerText(BannerType.Login).then((loginBanner) => this.setState({ loginBanner }) @@ -84,13 +84,13 @@ export class Login extends Component { HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement >, field: K - ) { + ): void { const value = e.target.value; this.setState({ [field]: value } as Pick); } - login(e: React.FormEvent) { + login(e: React.FormEvent): void { e.preventDefault(); const username: string = this.state.username.trim(); @@ -105,7 +105,7 @@ export class Login extends Component { } } - render() { + render(): ReactElement { return ( diff --git a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx index 2fbfd09627..45ee12c976 100644 --- a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx +++ b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx @@ -60,7 +60,7 @@ function testLogin( password: string, goodUsername: boolean, goodPassword: boolean -) { +): void { loginHandle.instance.setState({ username, password }); loginHandle.instance.login(MOCK_EVENT); expect(loginHandle.instance.state.error).toEqual({ diff --git a/src/components/Login/SignUpPage/SignUpComponent.tsx b/src/components/Login/SignUpPage/SignUpComponent.tsx index 5a7903f5ab..d06f888c6e 100644 --- a/src/components/Login/SignUpPage/SignUpComponent.tsx +++ b/src/components/Login/SignUpPage/SignUpComponent.tsx @@ -7,7 +7,7 @@ import { TextField, Typography, } from "@mui/material"; -import { Component } from "react"; +import { Component, ReactElement } from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import router from "browserRouter"; @@ -86,7 +86,7 @@ export class SignUp extends Component { }; } - componentDidMount() { + componentDidMount(): void { const search = window.location.search; const email = new URLSearchParams(search).get("email"); if (email) { @@ -101,7 +101,7 @@ export class SignUp extends Component { HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement >, field: K - ) { + ): void { const value = e.target.value; this.setState({ [field]: value } as Pick); this.setState((prevState) => ({ @@ -109,7 +109,7 @@ export class SignUp extends Component { })); } - async checkUsername() { + async checkUsername(): Promise { if (!meetsUsernameRequirements(this.state.username)) { this.setState((prevState) => ({ error: { ...prevState.error, username: true }, @@ -117,7 +117,7 @@ export class SignUp extends Component { } } - async signUp(e: React.FormEvent) { + async signUp(e: React.FormEvent): Promise { e.preventDefault(); const name = this.state.name.trim(); const username = this.state.username.trim(); @@ -146,7 +146,7 @@ export class SignUp extends Component { } } - render() { + render(): ReactElement { return ( diff --git a/src/components/Login/SignUpPage/index.ts b/src/components/Login/SignUpPage/index.ts index 598a46ac06..f3bbe065e6 100644 --- a/src/components/Login/SignUpPage/index.ts +++ b/src/components/Login/SignUpPage/index.ts @@ -16,6 +16,7 @@ function mapStateToProps(state: StoreState): SignUpStateProps { }; } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type function mapDispatchToProps(dispatch: StoreStateDispatch) { return { signUp: ( diff --git a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx b/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx index cb092f7566..84df8b4651 100644 --- a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx +++ b/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx @@ -98,7 +98,7 @@ async function testSignUp( error_email: boolean, error_password: boolean, error_confirmPassword: boolean -) { +): Promise { signUpHandle.instance.setState({ name, username, diff --git a/src/components/Login/tests/LoginActions.test.tsx b/src/components/Login/tests/LoginActions.test.tsx index 0c53288cac..587f4762a2 100644 --- a/src/components/Login/tests/LoginActions.test.tsx +++ b/src/components/Login/tests/LoginActions.test.tsx @@ -175,7 +175,7 @@ describe("LoginAction", () => { function testActionCreatorAgainst( LoginAction: (name: string) => UserAction, type: LoginType -) { +): void { expect(LoginAction(mockUser.username)).toEqual({ type: type, payload: { username: mockUser.username }, diff --git a/src/components/PageNotFound/component.tsx b/src/components/PageNotFound/component.tsx index 12b178ac1f..b09d68ff75 100644 --- a/src/components/PageNotFound/component.tsx +++ b/src/components/PageNotFound/component.tsx @@ -1,5 +1,5 @@ import { Typography } from "@mui/material"; -import React from "react"; +import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -10,12 +10,12 @@ import { Path } from "types/path"; * A custom 404 page that should be displayed anytime the user tries to navigate * to a nonexistent route. */ -export default function PageNotFound() { +export default function PageNotFound(): ReactElement { const { t } = useTranslation(); const navigate = useNavigate(); return ( - + <> {t("generic.404Title")} @@ -30,6 +30,6 @@ export default function PageNotFound() { {t("generic.404Text")} - + ); } diff --git a/src/components/ProjectExport/DownloadButton.tsx b/src/components/ProjectExport/DownloadButton.tsx index d7e4ae27ba..ce844d3a90 100644 --- a/src/components/ProjectExport/DownloadButton.tsx +++ b/src/components/ProjectExport/DownloadButton.tsx @@ -21,7 +21,7 @@ import { useAppDispatch, useAppSelector } from "types/hooks"; import { themeColors } from "types/theme"; import { getNowDateTimeString } from "utilities/utilities"; -function makeExportName(projectName: string) { +function makeExportName(projectName: string): string { return `${projectName}_${getNowDateTimeString()}.zip`; } @@ -104,7 +104,7 @@ export default function DownloadButton( } } - function iconColor() { + function iconColor(): `#${string}` { return exportState.status === ExportStatus.Failure ? themeColors.error : props.colorSecondary diff --git a/src/components/ProjectExport/ExportButton.tsx b/src/components/ProjectExport/ExportButton.tsx index edfd0ab8d8..4b17e033f0 100644 --- a/src/components/ProjectExport/ExportButton.tsx +++ b/src/components/ProjectExport/ExportButton.tsx @@ -1,5 +1,6 @@ import { ButtonProps } from "@mui/material/Button"; import { enqueueSnackbar } from "notistack"; +import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { isFrontierNonempty } from "backend"; @@ -15,11 +16,11 @@ interface ExportButtonProps { } /** A button for exporting project to Lift file */ -export default function ExportButton(props: ExportButtonProps) { +export default function ExportButton(props: ExportButtonProps): ReactElement { const dispatch = useAppDispatch(); const { t } = useTranslation(); - async function exportProj() { + async function exportProj(): Promise { await isFrontierNonempty(props.projectId).then(async (isNonempty) => { if (isNonempty) { await dispatch(asyncExportProject(props.projectId)); diff --git a/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx b/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx index bec78a8c57..b39d3c42d2 100644 --- a/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx +++ b/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx @@ -1,6 +1,11 @@ import { LanguagePicker } from "mui-language-picker"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -30,18 +35,22 @@ const mockState = { }; const mockStore = createMockStore(mockState); -const mockEvent = (value = "") => ({ - preventDefault: jest.fn(), +const mockChangeEvent = ( + value: string +): { target: Partial } => ({ target: { value }, }); +const mockSubmitEvent = (): Partial> => ({ + preventDefault: jest.fn(), +}); -let projectMaster: renderer.ReactTestRenderer; -let projectHandle: renderer.ReactTestInstance; +let projectMaster: ReactTestRenderer; +let projectHandle: ReactTestInstance; 4; beforeAll(async () => { - await renderer.act(async () => { - projectMaster = renderer.create( + await act(async () => { + projectMaster = create( @@ -59,22 +68,26 @@ describe("CreateProject", () => { const nameField = projectHandle.findByProps({ id: fieldIdName }); expect(nameField.props.error).toBeFalsy(); - await renderer.act(async () => { - projectHandle.findByProps({ id: formId }).props.onSubmit(mockEvent()); + await act(async () => { + projectHandle + .findByProps({ id: formId }) + .props.onSubmit(mockSubmitEvent()); }); expect(nameField.props.error).toBeTruthy(); }); it("errors on taken name", async () => { const nameField = projectHandle.findByProps({ id: fieldIdName }); - await renderer.act(async () => { - nameField.props.onChange(mockEvent("non-empty")); + await act(async () => { + nameField.props.onChange(mockChangeEvent("non-empty-value")); }); expect(nameField.props.error).toBeFalsy(); mockProjectDuplicateCheck.mockResolvedValueOnce(true); - await renderer.act(async () => { - projectHandle.findByProps({ id: formId }).props.onSubmit(mockEvent()); + await act(async () => { + projectHandle + .findByProps({ id: formId }) + .props.onSubmit(mockSubmitEvent()); }); expect(nameField.props.error).toBeTruthy(); }); @@ -85,7 +98,7 @@ describe("CreateProject", () => { const langPickers = projectHandle.findAllByType(LanguagePicker); expect(langPickers).toHaveLength(2); - await renderer.act(async () => { + await act(async () => { langPickers[0].props.setCode("non-empty"); }); expect(button.props.disabled).toBeFalsy(); @@ -97,7 +110,7 @@ describe("CreateProject", () => { // File with no writing systems only disables analysis lang picker. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([]); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "")); }); expect(projectHandle.findAllByType(LanguagePicker)).toHaveLength(1); @@ -106,7 +119,7 @@ describe("CreateProject", () => { mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([ newWritingSystem(), ]); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "oneLang")); }); expect(projectHandle.findAllByType(LanguagePicker)).toHaveLength(0); @@ -116,7 +129,7 @@ describe("CreateProject", () => { const button = projectHandle.findByType(FileInputButton); const langs = [newWritingSystem("redLang"), newWritingSystem("blueLang")]; mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce(langs); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "twoLang")); }); const vernSelect = projectHandle.findByProps({ id: selectIdVern }); diff --git a/src/components/ProjectSettings/ProjectArchive.tsx b/src/components/ProjectSettings/ProjectArchive.tsx index 4dc16374a3..b52b461676 100644 --- a/src/components/ProjectSettings/ProjectArchive.tsx +++ b/src/components/ProjectSettings/ProjectArchive.tsx @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; import { ButtonProps } from "@mui/material/Button"; -import { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { archiveProject, restoreProject } from "backend"; @@ -17,25 +17,20 @@ interface ProjectArchiveProps extends ButtonProps { /** * Button for archiving/restoring project (changing isActive) */ -export default function ProjectArchive(props: ProjectArchiveProps) { +export default function ProjectArchive( + props: ProjectArchiveProps +): ReactElement { const [open, setOpen] = useState(false); const { t } = useTranslation(); - async function updateProj() { + async function updateProj(): Promise { if (props.archive) { await archiveProject(props.projectId); } else { await restoreProject(props.projectId); } - handleClose(); - await props.updateParent(); - } - - function handleOpen() { - setOpen(true); - } - function handleClose() { setOpen(false); + await props.updateParent(); } return ( @@ -43,7 +38,7 @@ export default function ProjectArchive(props: ProjectArchiveProps) {