Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten TypeScript linting #2684

Merged
merged 5 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"piptools",
"Prenoun",
"Preverb",
"recaptcha",
"reportgenerator",
"sched",
"sillsdev",
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
},
"eslintConfig": {
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
Expand All @@ -142,8 +143,9 @@
],
"rules": {
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "warn",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/switch-exhaustiveness-check": "warn",
"import/first": "warn",
"import/newline-after-import": "warn",
Expand Down Expand Up @@ -173,9 +175,8 @@
],
"no-undef": "off",
"prefer-const": "warn",
"react/display-name": "off",
"react/jsx-boolean-value": "warn",
"unused-imports/no-unused-imports": "error"
"unused-imports/no-unused-imports": "warn"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
Expand All @@ -189,6 +190,7 @@
"react",
"unused-imports"
],
"root": true,
"settings": {
"react": {
"version": "detect"
Expand Down
18 changes: 11 additions & 7 deletions src/components/App/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "jest-canvas-mock";
import { Provider } from "react-redux";
import renderer from "react-test-renderer";
import { act, create } from "react-test-renderer";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";

Expand All @@ -9,9 +9,13 @@ import "tests/reactI18nextMock";
import { defaultState } from "components/App/DefaultState";
import App from "components/App/component";

jest.mock("@matt-block/react-recaptcha-v2", () => () => (
<div id="mockRecaptcha">Recaptcha</div>
));
jest.mock(
"@matt-block/react-recaptcha-v2",
() =>
function MockRecaptcha() {
return <div id="mockRecaptcha">Recaptcha</div>;
}
);
jest.mock("components/AnnouncementBanner/AnnouncementBanner", () => "div");

const createMockStore = configureMockStore([thunk]);
Expand All @@ -23,9 +27,9 @@ global.innerHeight = 100;
global.analytics = { track: jest.fn() } as any;

describe("App", () => {
it("renders without crashing", () => {
renderer.act(() => {
renderer.create(
it("renders without crashing", async () => {
await act(async () => {
create(
<Provider store={mockStore}>
<App />
</Provider>
Expand Down
11 changes: 1 addition & 10 deletions src/components/AppBar/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,12 @@ export default function UserMenu(props: TabProps): ReactElement {
open={Boolean(anchorElement)}
transformOrigin={{ horizontal: "right", vertical: "top" }}
>
<WrappedUserMenuList isAdmin={isAdmin} onSelect={handleClose} />
<UserMenuList isAdmin={isAdmin} onSelect={handleClose} />
</Menu>
</>
);
}

// <Menu> automatically applies a ref to its first child for anchoring. The
// following prevents a console warning: "Function components cannot be given refs.
// Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"
const WrappedUserMenuList = React.forwardRef(
(props: React.ComponentProps<typeof UserMenuList>, ref) => (
<UserMenuList {...props} forwardedRef={ref} />
)
);

interface UserMenuListProps {
isAdmin: boolean;
onSelect: () => void;
Expand Down
10 changes: 4 additions & 6 deletions src/components/AppBar/tests/AppBarComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom";
import renderer from "react-test-renderer";
import { act, create } from "react-test-renderer";
import configureMockStore from "redux-mock-store";

import "tests/reactI18nextMock";
Expand All @@ -18,8 +18,6 @@ jest.mock("backend", () => ({

const mockStore = configureMockStore()(defaultState);

let testRenderer: renderer.ReactTestRenderer;

function setMockFunctions() {
mockGetUser.mockResolvedValue(mockUser);
}
Expand All @@ -30,9 +28,9 @@ beforeAll(() => {
});

describe("AppBar", () => {
it("renders", () => {
renderer.act(() => {
testRenderer = renderer.create(
it("renders", async () => {
await act(async () => {
create(
<Provider store={mockStore}>
<MemoryRouter>
<AppBar />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface VernWithSuggestionsProps {
vernInput?: React.RefObject<HTMLInputElement>;
updateVernField: (newValue: string, openDialog?: boolean) => void;
onBlur: () => void;
onClose?: (e: React.ChangeEvent<{}>, reason: AutocompleteCloseReason) => void;
onClose?: (e: React.SyntheticEvent, reason: AutocompleteCloseReason) => void;
suggestedVerns?: string[];
handleEnter: () => void;
vernacularLang: WritingSystem;
Expand Down
50 changes: 27 additions & 23 deletions src/components/DataEntry/DataEntryTable/tests/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ReactElement } from "react";
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";
Expand All @@ -17,7 +22,6 @@ import DataEntryTable, {
updateEntryGloss,
} from "components/DataEntry/DataEntryTable";
import NewEntry from "components/DataEntry/DataEntryTable/NewEntry";
import { RecentEntryProps } from "components/DataEntry/DataEntryTable/RecentEntry";
import { newProject } from "types/project";
import {
newSemanticDomain,
Expand Down Expand Up @@ -57,10 +61,10 @@ jest.mock("utilities/utilities");

jest.spyOn(window, "alert").mockImplementation(() => {});

let testRenderer: renderer.ReactTestRenderer;
let testHandle: renderer.ReactTestInstance;
let testRenderer: ReactTestRenderer;
let testHandle: ReactTestInstance;

function MockRecentEntry(props: RecentEntryProps): ReactElement {
function MockRecentEntry(): ReactElement {
return <div />;
}

Expand Down Expand Up @@ -97,8 +101,8 @@ beforeEach(() => {
});

const renderTable = async (): Promise<void> => {
await renderer.act(async () => {
testRenderer = renderer.create(
await act(async () => {
testRenderer = create(
<Provider store={mockStore}>
<DataEntryTable
semanticDomain={mockTreeNode}
Expand All @@ -119,7 +123,7 @@ const addRecentEntry = async (word?: Word): Promise<string> => {
}
mockCreateWord.mockResolvedValueOnce(word);
mockGetWord.mockResolvedValueOnce(word);
await renderer.act(async () => {
await act(async () => {
await testRenderer.root.findByType(NewEntry).props.addNewEntry();
});
return word.id;
Expand All @@ -140,7 +144,7 @@ describe("DataEntryTable", () => {
it("hides questions", async () => {
expect(mockHideQuestions).not.toBeCalled();
testHandle = testRenderer.root.findByProps({ id: exitButtonId });
await renderer.act(async () => await testHandle.props.onClick());
await act(async () => await testHandle.props.onClick());
expect(mockHideQuestions).toBeCalled();
});

Expand All @@ -149,26 +153,26 @@ describe("DataEntryTable", () => {
testHandle = testRenderer.root.findByType(NewEntry);
expect(testHandle).not.toBeNull;
// Set newVern but not newGloss.
await renderer.act(async () => testHandle.props.setNewVern("hasVern"));
await act(async () => testHandle.props.setNewVern("hasVern"));
testHandle = testRenderer.root.findByProps({ id: exitButtonId });
await renderer.act(async () => await testHandle.props.onClick());
await act(async () => await testHandle.props.onClick());
expect(mockCreateWord).toBeCalledTimes(1);
});

it("doesn't create word when new entry has no vernacular", async () => {
testHandle = testRenderer.root.findByType(NewEntry);
expect(testHandle).not.toBeNull;
// Set newGloss but not newVern.
await renderer.act(async () => testHandle.props.setNewGloss("hasGloss"));
await act(async () => testHandle.props.setNewGloss("hasGloss"));
testHandle = testRenderer.root.findByProps({ id: exitButtonId });
await renderer.act(async () => await testHandle.props.onClick());
await act(async () => await testHandle.props.onClick());
expect(mockCreateWord).not.toBeCalled();
});

it("opens the domain tree", async () => {
expect(mockOpenTree).not.toBeCalled();
testHandle = testRenderer.root.findByProps({ id: exitButtonId });
await renderer.act(async () => await testHandle.props.onClick());
await act(async () => await testHandle.props.onClick());
expect(mockOpenTree).toBeCalledTimes(1);
});
});
Expand Down Expand Up @@ -293,7 +297,7 @@ describe("DataEntryTable", () => {
mockGetFrontierWords.mockResolvedValue([word]);
await renderTable();
testHandle = testRenderer.root.findByType(NewEntry);
await renderer.act(async () => {
await act(async () => {
await testHandle.props.setNewGloss(firstGlossText(word.senses[0]));
await testHandle.props.updateWordWithNewGloss(word.id);
});
Expand All @@ -312,7 +316,7 @@ describe("DataEntryTable", () => {

testHandle = testRenderer.root.findByType(NewEntry);
const glossText = firstGlossText(word.senses[senseIndex]);
await renderer.act(async () => {
await act(async () => {
await testHandle.props.setNewGloss(glossText);
await testHandle.props.updateWordWithNewGloss(word.id);
});
Expand All @@ -330,7 +334,7 @@ describe("DataEntryTable", () => {
it("updates word in backend if gloss doesn't exist", async () => {
await renderTable();
testHandle = testRenderer.root.findByType(NewEntry);
await renderer.act(async () => {
await act(async () => {
await testHandle.props.setNewGloss("differentGloss");
await testHandle.props.updateWordWithNewGloss(mockMultiWord.id);
});
Expand All @@ -344,7 +348,7 @@ describe("DataEntryTable", () => {
it("checks for duplicate and, if so, updates it", async () => {
testHandle = testRenderer.root.findByType(NewEntry);
mockGetDuplicateId.mockResolvedValueOnce(true);
await renderer.act(async () => {
await act(async () => {
await testHandle.props.addNewEntry();
});
expect(mockUpdateDuplicate).toBeCalledTimes(1);
Expand All @@ -368,7 +372,7 @@ describe("DataEntryTable", () => {

// Verify that the number of recent entries increases by the correct amount
expect(testRenderer.root.findAllByType(MockRecentEntry)).toHaveLength(0);
await renderer.act(async () => {
await act(async () => {
await testRenderer.root.findByType(NewEntry).props.addNewEntry();
});
expect(testRenderer.root.findAllByType(MockRecentEntry)).toHaveLength(
Expand All @@ -383,14 +387,14 @@ describe("DataEntryTable", () => {
const vern = "vern";
const glossDef = "gloss";
const noteText = "note";
renderer.act(() => {
act(() => {
testHandle.props.setNewVern(vern);
testHandle.props.setNewGloss(glossDef);
testHandle.props.setNewNote(noteText);
});

// Trigger the function to add a new entry
await renderer.act(async () => {
await act(async () => {
try {
await testHandle.props.addNewEntry();
} catch {
Expand Down Expand Up @@ -426,7 +430,7 @@ describe("DataEntryTable", () => {
it("removes a recent entry", async () => {
await addRecentEntry();
const recentEntry = testRenderer.root.findByType(MockRecentEntry);
await renderer.act(async () => {
await act(async () => {
await recentEntry.props.removeEntry();
});
expect(testRenderer.root.findAllByType(MockRecentEntry)).toHaveLength(0);
Expand All @@ -449,7 +453,7 @@ describe("DataEntryTable", () => {

// Update the vernacular
const newVern = "not the vern generated in addRecentEntry";
await renderer.act(async () => {
await act(async () => {
await recentEntry.props.updateVern(newVern);
});

Expand Down
4 changes: 2 additions & 2 deletions src/components/Login/LoginPage/LoginComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TextField,
Typography,
} from "@mui/material";
import React from "react";
import { Component } from "react";
import { withTranslation, WithTranslation } from "react-i18next";

import { BannerType } from "api/models";
Expand Down Expand Up @@ -57,7 +57,7 @@ interface LoginError {
}

/** The login page (also doubles as a logout page) */
export class Login extends React.Component<LoginProps, LoginState> {
export class Login extends Component<LoginProps, LoginState> {
constructor(props: LoginProps) {
super(props);
this.props.logout(); // Loading this page will reset the app, both store and localStorage
Expand Down
27 changes: 18 additions & 9 deletions src/components/Login/LoginPage/tests/LoginComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import renderer from "react-test-renderer";
import {
ReactTestInstance,
ReactTestRenderer,
act,
create,
} from "react-test-renderer";

import "tests/reactI18nextMock";

import Login from "components/Login/LoginPage/LoginComponent";

jest.mock("@matt-block/react-recaptcha-v2", () => () => (
<div id="mockRecaptcha">Recaptcha</div>
));
jest.mock(
"@matt-block/react-recaptcha-v2",
() =>
function MockRecaptcha() {
return <div id="mockRecaptcha">Recaptcha</div>;
}
);
jest.mock("backend", () => ({
getBannerText: () => Promise.resolve(""),
}));

jest.mock("browserRouter");
const LOGOUT = jest.fn();
let loginMaster: renderer.ReactTestRenderer;
let loginHandle: renderer.ReactTestInstance;
let loginMaster: ReactTestRenderer;
let loginHandle: ReactTestInstance;

const DATA = "stuff";
const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } };

describe("Testing login component", () => {
beforeEach(() => {
renderer.act(() => {
loginMaster = renderer.create(<Login logout={LOGOUT} reset={LOGOUT} />);
beforeEach(async () => {
await act(async () => {
loginMaster = create(<Login logout={LOGOUT} reset={LOGOUT} />);
});
loginHandle = loginMaster.root.findByType(Login);
LOGOUT.mockClear();
Expand Down
Loading