Add Unit Tests to UI (#2015)

* Update testing framework

* Update action button test

* Add unit tests for language and authentication page

* Add unit tests for the custom selector

* Fix packages, add new testing plugin for eslint, fix issues

* Add unit tests for ChipInput

* Add coverage and test ui. Add more tests

* Fix formatting issues

* Try to fix the styling issues again

* Fix formatting issues
pull/2060/head
Liang Yi 2 years ago committed by GitHub
parent 3310f6aeb8
commit 0b7a1a90a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,5 +11,15 @@
"plugin:react-hooks/recommended", "plugin:react-hooks/recommended",
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
],
"plugins": ["testing-library"],
"overrides": [
{
"files": [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[jt]s?(x)"
],
"extends": ["plugin:testing-library/react"]
}
] ]
} }

@ -1,4 +1,4 @@
build build
dist dist
converage coverage
public public

File diff suppressed because it is too large Load Diff

@ -14,11 +14,11 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@mantine/core": "^5.6.0", "@mantine/core": "^5.6.0",
"@mantine/dropzone": "^5.6.0",
"@mantine/form": "^5.6.0", "@mantine/form": "^5.6.0",
"@mantine/hooks": "^5.6.0", "@mantine/hooks": "^5.6.0",
"@mantine/modals": "^5.6.0", "@mantine/modals": "^5.6.0",
"@mantine/notifications": "^5.6.0", "@mantine/notifications": "^5.6.0",
"@mantine/dropzone": "^5.6.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -34,7 +34,7 @@
"@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.0",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/lodash": "^4.14.0", "@types/lodash": "^4.14.0",
@ -43,10 +43,13 @@
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-table": "^7.7.0", "@types/react-table": "^7.7.0",
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^2.2.0",
"@vitest/coverage-c8": "^0.25.0",
"@vitest/ui": "^0.25.0",
"clsx": "^1.2.0", "clsx": "^1.2.0",
"eslint": "^8.26.0", "eslint": "^8.26.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.9.0",
"husky": "^8.0.2", "husky": "^8.0.2",
"jsdom": "^20.0.1", "jsdom": "^20.0.1",
"lodash": "^4.17.0", "lodash": "^4.17.0",
@ -60,7 +63,7 @@
"typescript": "^4", "typescript": "^4",
"vite": "^3.2.1", "vite": "^3.2.1",
"vite-plugin-checker": "^0.5.5", "vite-plugin-checker": "^0.5.5",
"vitest": "^0.24.3" "vitest": "^0.25.0"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -70,6 +73,8 @@
"check:ts": "tsc --noEmit --incremental false", "check:ts": "tsc --noEmit --incremental false",
"check:fmt": "prettier -c .", "check:fmt": "prettier -c .",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"format": "prettier -w .", "format": "prettier -w .",
"prepare": "cd .. && husky install frontend/.husky" "prepare": "cd .. && husky install frontend/.husky"
}, },

@ -0,0 +1,82 @@
import { render, screen } from "@testing-library/react";
import { describe, it } from "vitest";
import { Language } from ".";
describe("Language text", () => {
const testLanguage: Language.Info = {
code2: "en",
name: "English",
};
it("should show short text", () => {
render(<Language.Text value={testLanguage}></Language.Text>);
expect(screen.getByText(testLanguage.code2)).toBeDefined();
});
it("should show long text", () => {
render(<Language.Text value={testLanguage} long></Language.Text>);
expect(screen.getByText(testLanguage.name)).toBeDefined();
});
const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true };
it("should show short text with HI", () => {
render(<Language.Text value={testLanguageWithHi}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:HI`;
expect(screen.getByText(expectedText)).toBeDefined();
});
it("should show long text with HI", () => {
render(<Language.Text value={testLanguageWithHi} long></Language.Text>);
const expectedText = `${testLanguageWithHi.name} HI`;
expect(screen.getByText(expectedText)).toBeDefined();
});
const testLanguageWithForced: Language.Info = {
...testLanguage,
forced: true,
};
it("should show short text with Forced", () => {
render(<Language.Text value={testLanguageWithForced}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:Forced`;
expect(screen.getByText(expectedText)).toBeDefined();
});
it("should show long text with Forced", () => {
render(<Language.Text value={testLanguageWithForced} long></Language.Text>);
const expectedText = `${testLanguageWithHi.name} Forced`;
expect(screen.getByText(expectedText)).toBeDefined();
});
});
describe("Language list", () => {
const elements: Language.Info[] = [
{
code2: "en",
name: "English",
},
{
code2: "zh",
name: "Chinese",
},
];
it("should show all languages", () => {
render(<Language.List value={elements}></Language.List>);
elements.forEach((value) => {
expect(screen.getByText(value.name)).toBeDefined();
});
});
});

@ -0,0 +1,38 @@
import { faStickyNote } from "@fortawesome/free-regular-svg-icons";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
import Action from "./Action";
const testLabel = "Test Label";
const testIcon = faStickyNote;
describe("Action button", () => {
it("should be a button", () => {
render(<Action icon={testIcon} label={testLabel}></Action>);
const element = screen.getByRole("button", { name: testLabel });
expect(element.getAttribute("type")).toEqual("button");
expect(element.getAttribute("aria-label")).toEqual(testLabel);
});
it("should show icon", () => {
render(<Action icon={testIcon} label={testLabel}></Action>);
// TODO: use getBy...
const element = screen.getByRole("img", { hidden: true });
expect(element.getAttribute("data-prefix")).toEqual(testIcon.prefix);
expect(element.getAttribute("data-icon")).toEqual(testIcon.iconName);
});
it("should call on-click event when clicked", async () => {
const onClickFn = vitest.fn();
render(
<Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>
);
await userEvent.click(screen.getByRole("button", { name: testLabel }));
expect(onClickFn).toHaveBeenCalled();
});
});

@ -0,0 +1,46 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
import ChipInput from "./ChipInput";
describe("ChipInput", () => {
const existedValues = ["value_1", "value_2"];
// TODO: Support default value
it.skip("should works with default value", () => {
render(<ChipInput defaultValue={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
});
});
it("should works with value", () => {
render(<ChipInput value={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
});
});
it.skip("should allow user creates new value", async () => {
const typedValue = "value_3";
const mockedFn = vitest.fn((values: string[]) => {
expect(values).toContain(typedValue);
});
render(<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>);
const element = screen.getByRole("searchbox");
await userEvent.type(element, typedValue);
expect(element).toHaveValue(typedValue);
const createBtn = screen.getByText(`Add "${typedValue}"`);
await userEvent.click(createBtn);
expect(mockedFn).toBeCalledTimes(1);
});
});

@ -0,0 +1,147 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
import { Selector, SelectorOption } from "./Selector";
const selectorName = "Test Selections";
const testOptions: SelectorOption<string>[] = [
{
label: "Option 1",
value: "option_1",
},
{
label: "Option 2",
value: "option_2",
},
];
describe("Selector", () => {
describe("options", () => {
it("should work with the SelectorOption", () => {
render(<Selector name={selectorName} options={testOptions}></Selector>);
// TODO: selectorName
expect(screen.getByRole("searchbox")).toBeDefined();
});
it("should display when clicked", async () => {
render(<Selector name={selectorName} options={testOptions}></Selector>);
const element = screen.getByRole("searchbox");
await userEvent.click(element);
expect(screen.queryAllByRole("option")).toHaveLength(testOptions.length);
testOptions.forEach((option) => {
expect(screen.getByText(option.label)).toBeDefined();
});
});
it("shouldn't show default value", async () => {
const option = testOptions[0];
render(
<Selector
name={selectorName}
options={testOptions}
defaultValue={option.value}
></Selector>
);
expect(screen.getByDisplayValue(option.label)).toBeDefined();
});
it("shouldn't show value", async () => {
const option = testOptions[0];
render(
<Selector
name={selectorName}
options={testOptions}
value={option.value}
></Selector>
);
expect(screen.getByDisplayValue(option.label)).toBeDefined();
});
});
describe("event", () => {
it("should fire on-change event when clicking option", async () => {
const clickedOption = testOptions[0];
const mockedFn = vitest.fn((value: string | null) => {
expect(value).toEqual(clickedOption.value);
});
render(
<Selector
name={selectorName}
options={testOptions}
onChange={mockedFn}
></Selector>
);
const element = screen.getByRole("searchbox");
await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label));
expect(mockedFn).toBeCalled();
});
});
describe("with object options", () => {
const objectOptions: SelectorOption<{ name: string }>[] = [
{
label: "Option 1",
value: {
name: "option_1",
},
},
{
label: "Option 2",
value: {
name: "option_2",
},
},
];
it("should fire on-change event with payload", async () => {
const clickedOption = objectOptions[0];
const mockedFn = vitest.fn((value: { name: string } | null) => {
expect(value).toEqual(clickedOption.value);
});
render(
<Selector
name={selectorName}
options={objectOptions}
onChange={mockedFn}
getkey={(v) => v.name}
></Selector>
);
const element = screen.getByRole("searchbox");
await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label));
expect(mockedFn).toBeCalled();
});
});
describe("placeholder", () => {
it("should show when no selection", () => {
const placeholder = "Empty Selection";
render(
<Selector
name={selectorName}
options={testOptions}
placeholder={placeholder}
></Selector>
);
expect(screen.getByPlaceholderText(placeholder)).toBeDefined();
});
});
});

@ -1,12 +0,0 @@
import { describe, it } from "vitest";
import { StaticModals } from "./WithModal";
describe("modal tests", () => {
it.skip("no duplicated modals", () => {
const existedKeys = new Set<string>();
StaticModals.forEach(({ modalKey }) => {
expect(existedKeys.has(modalKey)).toBeFalsy();
existedKeys.add(modalKey);
});
});
});

@ -0,0 +1,20 @@
import { render, screen } from "@testing-library/react";
import { QueryClientProvider } from "react-query";
import { describe, it } from "vitest";
import Authentication from "./Authentication";
import queryClient from "@/apis/queries";
describe("Authentication", () => {
it("should render without crash", () => {
render(
<QueryClientProvider client={queryClient}>
<Authentication></Authentication>
</QueryClientProvider>
);
expect(screen.getByPlaceholderText("Username")).toBeDefined();
expect(screen.getByPlaceholderText("Password")).toBeDefined();
expect(screen.getByRole("button", { name: "Login" })).toBeDefined();
});
});

@ -40,11 +40,13 @@ const Authentication: FunctionComponent = () => {
> >
<Stack> <Stack>
<TextInput <TextInput
name="Username"
placeholder="Username" placeholder="Username"
required required
{...form.getInputProps("username")} {...form.getInputProps("username")}
></TextInput> ></TextInput>
<PasswordInput <PasswordInput
name="Password"
required required
placeholder="Password" placeholder="Password"
{...form.getInputProps("password")} {...form.getInputProps("password")}

@ -0,0 +1,31 @@
import queryClient from "@/apis/queries";
import { Text } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { QueryClientProvider } from "react-query";
import { BrowserRouter } from "react-router-dom";
import { describe, it } from "vitest";
import Layout from "./Layout";
const renderLayout = () => {
render(
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<Layout name="Test Settings">
<Text>Value</Text>
</Layout>
</QueryClientProvider>
</BrowserRouter>
);
};
describe("Settings layout", () => {
it.concurrent("should be able to render without issues", () => {
renderLayout();
});
it.concurrent("save button should be disabled by default", () => {
renderLayout();
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
});
});

@ -0,0 +1,38 @@
import { Text } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { describe, it } from "vitest";
import { Section } from "./Section";
describe("Settings section", () => {
const header = "Section Header";
it("should show header", () => {
render(<Section header="Section Header"></Section>);
expect(screen.getByText(header)).toBeDefined();
expect(screen.getByRole("separator")).toBeDefined();
});
it("should show children", () => {
const text = "Section Child";
render(
<Section header="Section Header">
<Text>{text}</Text>
</Section>
);
expect(screen.getByText(header)).toBeDefined();
expect(screen.getByText(text)).toBeDefined();
});
it("should work with hidden", () => {
const text = "Section Child";
render(
<Section header="Section Header" hidden>
<Text>{text}</Text>
</Section>
);
expect(screen.getByText(header)).not.toBeVisible();
expect(screen.getByText(text)).not.toBeVisible();
});
});

@ -0,0 +1,42 @@
import { useForm } from "@mantine/form";
import { render, screen } from "@testing-library/react";
import { FunctionComponent } from "react";
import { describe, it } from "vitest";
import { FormContext, FormValues } from "../utilities/FormValues";
import { Number, Text } from "./forms";
const FormSupport: FunctionComponent = ({ children }) => {
const form = useForm<FormValues>({
initialValues: {
settings: {},
hooks: {},
},
});
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
};
describe("Settings form", () => {
describe("number component", () => {
it("should be able to render", () => {
render(
<FormSupport>
<Number settingKey="test-numberValue"></Number>
</FormSupport>
);
expect(screen.getByRole("textbox")).toBeDefined();
});
});
describe("text component", () => {
it("should be able to render", () => {
render(
<FormSupport>
<Text settingKey="test-textValue"></Text>
</FormSupport>
);
expect(screen.getByRole("textbox")).toBeDefined();
});
});
});

@ -45,9 +45,8 @@ declare module "react-table" {
interface CustomTableProps<D extends Record<string, unknown>> interface CustomTableProps<D extends Record<string, unknown>>
extends useSelectionProps<D> {} extends useSelectionProps<D> {}
export interface TableOptions< export interface TableOptions<D extends Record<string, unknown>>
D extends Record<string, unknown> extends UseExpandedOptions<D>,
> extends UseExpandedOptions<D>,
// UseFiltersOptions<D>, // UseFiltersOptions<D>,
// UseGlobalFiltersOptions<D>, // UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>, UseGroupByOptions<D>,

@ -1,27 +1,10 @@
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { describe, it, vitest } from "vitest"; import { describe, it } from "vitest";
import { Main } from "../src/main"; import { Main } from "../src/main";
describe("render test", () => { describe("App", () => {
beforeAll(() => { it("should render without crash", () => {
// From https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vitest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vitest.fn(), // Deprecated
removeListener: vitest.fn(), // Deprecated
addEventListener: vitest.fn(),
removeEventListener: vitest.fn(),
dispatchEvent: vitest.fn(),
})),
});
});
it("render without crashing", () => {
render( render(
<StrictMode> <StrictMode>
<Main /> <Main />

@ -1 +1,28 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { vitest } from "vitest";
// From https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vitest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vitest.fn(), // Deprecated
removeListener: vitest.fn(), // Deprecated
addEventListener: vitest.fn(),
removeEventListener: vitest.fn(),
dispatchEvent: vitest.fn(),
})),
});
// From https://github.com/mantinedev/mantine/blob/master/configuration/jest/jsdom.mocks.js
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;

Loading…
Cancel
Save