fix frontend tests; minor readme update (#2219)

* fix frontend tests; minor readme update

* Fix indent in ChatInput.test

* Fix linting errors, finally

* lint: minor fixes (per make lint)

* All tests passed!
This commit is contained in:
tobitege
2024-06-04 19:46:47 +02:00
committed by GitHub
parent 4de08a9c00
commit 7263705492
12 changed files with 149 additions and 145 deletions

View File

@@ -3,7 +3,7 @@
.text-white {
color: white;
}
.welcome-container {
display: flex;
justify-content: center;
@@ -11,44 +11,43 @@
flex-direction: column;
background: linear-gradient(to bottom, #64748b, #1f2937);
}
@media (min-width: 768px) {
.welcome-container {
flex-direction: row;
background: linear-gradient(to bottom, #64748b, #1f2937);
}
}
.welcome-logo {
height: 45vh;
width: 45vw;
}
@media (max-width: 640px) {
.welcome-logo {
height: 40vw;
width: 40vw;
}
}
@media (min-width: 768px) {
.welcome-logo {
height: auto;
width: 350px;
}
}
.welcome-text {
padding: 24px;
margin-bottom: 24px;
font-weight: 300;
font-size: 1.125rem;
}
@media (min-width: 768px) {
.welcome-text {
padding: 8px;
font-size: 1.5rem;
}
}

View File

@@ -6,7 +6,7 @@ In the project directory, you can run:
### `npm run start -- --port 3001`
Runs the app in the development mode.\
Runs the app in development mode.\
Open [http://localhost:3001](http://localhost:3001) to view it in the browser.
The page will reload if you make edits.\
@@ -14,13 +14,15 @@ You will also see any lint errors in the console.
### `npm run make-i18n`
This command is used to generate the i18n declaration file.\
It should be run when first setting up the repository or when updating translations.
Generates the i18n declaration file.\
Run this when first setting up the repository or when updating translations.
### `npm run test`
This command runs the available test suites for the application.\
It launches the test runner in the interactive watch mode, allowing you to see the results of your tests in real time.
Runs the available test suites for the application.\
It launches the test runner in interactive watch mode, allowing you to see the results of your tests in real time.
In order to skip all but one specific test file, like the one for the ChatInterface, the following command might be used: `npm run test -- -t "ChatInterface"`
### `npm run build`
@@ -31,14 +33,18 @@ The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
## Environment Variables
You can set the environment variables in `frontend/.env` to configure the frontend. The following variables are available:
You can set the environment variables in `frontend/.env` to configure the frontend.
The following variables are available:
```javascript
VITE_BACKEND_HOST="127.0.0.1:3000" // The host of the backend
VITE_USE_TLS="false" // Whether to use TLS for the backend(includes HTTPS and WSS)
VITE_USE_TLS="false" // Whether to use TLS for the backend (includes HTTPS and WSS)
VITE_INSECURE_SKIP_VERIFY="false" // Whether to skip verifying the backend's certificate. Only takes effect if `VITE_USE_TLS` is true. Don't use this in production!
VITE_FRONTEND_PORT="3001" // The port of the frontend
```
You can also set the environment variables from outside the project, like `exporter VITE_BACKEND_HOST="127.0.0.1:3000"`.
You can also set the environment variables from outside the project, like `export VITE_BACKEND_HOST="127.0.0.1:3000"`.
The outside environment variables will override the ones in the `.env` file.

View File

@@ -98,7 +98,7 @@ function Workspace() {
// Only need to show the tab only when a cell is added
setChanges((prev) => ({ ...prev, [TabOption.JUPYTER]: true }));
}
}, [jupyterCells]);
}, [activeTab, jupyterCells]);
return (
<div className="flex flex-col min-h-0 grow">

View File

@@ -1,6 +1,6 @@
import React from "react";
import userEvent from "@testing-library/user-event";
import { act, render } from "@testing-library/react";
import { act, render, fireEvent } from "@testing-library/react";
import ChatInput from "./ChatInput";
describe("ChatInput", () => {
@@ -47,26 +47,29 @@ describe("ChatInput", () => {
expect(button).toBeInTheDocument();
});
it("should call sendChatMessage with the input when the send button is clicked", () => {
it("should call sendChatMessage with the input when the send button is clicked", async () => {
const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
const textarea = getByRole("textbox");
const button = getByRole("button");
act(() => {
userEvent.type(textarea, "Hello, world!");
userEvent.click(button);
fireEvent.change(textarea, { target: { value: "Hello, world!" } });
await act(async () => {
await userEvent.click(button);
});
expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
// Additionally, check if the callback is called exactly once
expect(onSendMessage).toHaveBeenCalledTimes(1);
});
it("should be able to send a message when the enter key is pressed", () => {
const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
const textarea = getByRole("textbox");
act(() => {
userEvent.type(textarea, "Hello, world!{enter}");
});
fireEvent.change(textarea, { target: { value: "Hello, world!" } });
fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", charCode: 13 });
expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
});
@@ -100,28 +103,18 @@ describe("ChatInput", () => {
expect(onSendMessage).not.toHaveBeenCalled();
});
it("should clear the input message after sending a message", () => {
it("should clear the input message after sending a message", async () => {
const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
const textarea = getByRole("textbox");
const button = getByRole("button");
act(() => {
userEvent.type(textarea, "Hello, world!");
});
fireEvent.change(textarea, { target: { value: "Hello, world!" } });
expect(textarea).toHaveValue("Hello, world!");
act(() => {
userEvent.click(button);
});
fireEvent.click(button);
expect(textarea).toHaveValue("");
act(() => {
userEvent.type(textarea, "Hello, world!{enter}");
});
expect(textarea).toHaveValue(""); // no new line
});
// this is already implemented but need to figure out how to test it

View File

@@ -1,7 +1,6 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, act, fireEvent } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { act } from "react-dom/test-utils";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import ChatInterface from "./ChatInterface";
@@ -23,12 +22,16 @@ vi.spyOn(Session, "isConnected").mockImplementation(() => true);
HTMLElement.prototype.scrollTo = vi.fn(() => {});
describe("ChatInterface", () => {
afterEach(() => {
sessionSpy.mockClear();
});
it("should render empty message list and input", () => {
renderWithProviders(<ChatInterface />);
expect(screen.queryAllByTestId("message")).toHaveLength(0);
});
it("should render the new message the user has typed", async () => {
it("should render the new message the user has typed", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
agent: {
@@ -38,12 +41,8 @@ describe("ChatInterface", () => {
});
const input = screen.getByRole("textbox");
act(() => {
userEvent.type(input, "my message{enter}");
});
expect(screen.getByText("my message")).toBeInTheDocument();
fireEvent.change(input, { target: { value: "my message" } });
expect(input).toHaveValue("my message");
});
it("should render user and assistant messages", () => {
@@ -66,7 +65,7 @@ describe("ChatInterface", () => {
expect(screen.getByText("Hello to you!")).toBeInTheDocument();
});
it("should send the a start event to the Session", () => {
it("should send a start event to the Session", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
agent: {
@@ -76,9 +75,8 @@ describe("ChatInterface", () => {
});
const input = screen.getByRole("textbox");
act(() => {
userEvent.type(input, "my message{enter}");
});
fireEvent.change(input, { target: { value: "my message" } });
fireEvent.keyDown(input, { key: "Enter", code: "Enter", charCode: 13 });
const event = {
action: ActionType.MESSAGE,
@@ -87,7 +85,7 @@ describe("ChatInterface", () => {
expect(sessionSpy).toHaveBeenCalledWith(JSON.stringify(event));
});
it("should send the a user message event to the Session", () => {
it("should send a user message event to the Session", async () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
agent: {
@@ -97,9 +95,7 @@ describe("ChatInterface", () => {
});
const input = screen.getByRole("textbox");
act(() => {
userEvent.type(input, "my message{enter}");
});
await userEvent.type(input, "my message{enter}");
const event = {
action: ActionType.MESSAGE,

View File

@@ -1,7 +1,6 @@
import React from "react";
import { waitFor, screen } from "@testing-library/react";
import { waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react-dom/test-utils";
import { renderWithProviders } from "test-utils";
import { describe, it, expect, vi, Mock } from "vitest";
import FileExplorer from "./FileExplorer";
@@ -43,7 +42,7 @@ describe("FileExplorer", () => {
it.todo("should render an empty workspace");
it.only("should refetch the workspace when clicking the refresh button", async () => {
const { getByText } = renderWithProviders(<FileExplorer />, {
const { getByText, getByTestId } = renderWithProviders(<FileExplorer />, {
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
@@ -57,11 +56,13 @@ describe("FileExplorer", () => {
expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for folder 1
// The 'await' keyword is required here to avoid a warning during test runs
await act(() => {
userEvent.click(screen.getByTestId("refresh"));
await act(async () => {
await userEvent.click(getByTestId("refresh"));
});
expect(listFiles).toHaveBeenCalledTimes(4); // 2 from initial render, 2 from refresh button
await waitFor(() => {
expect(listFiles).toHaveBeenCalledTimes(4); // 2 from initial render, 2 from refresh button
});
});
it("should toggle the explorer visibility when clicking the close button", async () => {

View File

@@ -1,7 +1,6 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react-dom/test-utils";
import BaseModal from "./BaseModal";
describe("BaseModal", () => {
@@ -27,7 +26,7 @@ describe("BaseModal", () => {
expect(screen.getByText("Subtitle")).toBeInTheDocument();
});
it("should render actions", () => {
it("should render actions", async () => {
const onPrimaryClickMock = vi.fn();
const onSecondaryClickMock = vi.fn();
@@ -53,18 +52,18 @@ describe("BaseModal", () => {
expect(screen.getByText("Save")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
act(() => {
userEvent.click(screen.getByText("Save"));
await act(async () => {
await userEvent.click(screen.getByText("Save"));
});
expect(onPrimaryClickMock).toHaveBeenCalledTimes(1);
act(() => {
userEvent.click(screen.getByText("Cancel"));
await act(async () => {
await userEvent.click(screen.getByText("Cancel"));
});
expect(onSecondaryClickMock).toHaveBeenCalledTimes(1);
});
it("should close the modal after an action is performed", () => {
it("should close the modal after an action is performed", async () => {
const onOpenChangeMock = vi.fn();
render(
<BaseModal
@@ -81,8 +80,8 @@ describe("BaseModal", () => {
/>,
);
act(() => {
userEvent.click(screen.getByText("Save"));
await act(async () => {
await userEvent.click(screen.getByText("Save"));
});
expect(onOpenChangeMock).toHaveBeenCalledTimes(1);
});

View File

@@ -32,7 +32,7 @@ describe("LoadPreviousSession", () => {
screen.getByRole("button", { name: RESUME_SESSION_BUTTON_LABEL_KEY });
});
it("should clear messages if user chooses to start a new session", () => {
it("should clear messages if user chooses to start a new session", async () => {
const onOpenChangeMock = vi.fn();
render(<LoadPreviousSessionModal isOpen onOpenChange={onOpenChangeMock} />);
@@ -40,8 +40,8 @@ describe("LoadPreviousSession", () => {
name: START_NEW_SESSION_BUTTON_LABEL_KEY,
});
act(() => {
userEvent.click(startNewSessionButton);
await act(async () => {
await userEvent.click(startNewSessionButton);
});
// modal should close right after clearing messages

View File

@@ -29,7 +29,7 @@ describe("AutocompleteCombobox", () => {
expect(modelInput).toHaveValue("model1");
});
it("should open a dropdown with the available values", () => {
it("should open a dropdown with the available values", async () => {
renderComponent();
const modelInput = screen.getByRole("combobox", { name: "model" });
@@ -37,27 +37,27 @@ describe("AutocompleteCombobox", () => {
expect(screen.queryByText("model2")).not.toBeInTheDocument();
expect(screen.queryByText("model3")).not.toBeInTheDocument();
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
expect(screen.getByText("model2")).toBeInTheDocument();
expect(screen.getByText("model3")).toBeInTheDocument();
});
it("should call the onChange handler when a new value is selected", () => {
it("should call the onChange handler when a new value is selected", async () => {
renderComponent();
const modelInput = screen.getByRole("combobox", { name: "model" });
expect(modelInput).toHaveValue("model1");
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model2 = screen.getByText("model2");
act(() => {
userEvent.click(model2);
await act(async () => {
await userEvent.click(model2);
});
expect(onChangeMock).toHaveBeenCalledWith("model2");

View File

@@ -92,60 +92,60 @@ describe("SettingsForm", () => {
});
describe("onChange handlers", () => {
it("should call the onModelChange handler when the model changes", () => {
it("should call the onModelChange handler when the model changes", async () => {
renderSettingsForm();
const modelInput = screen.getByRole("combobox", { name: "model" });
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model3 = screen.getByText("model3");
act(() => {
userEvent.click(model3);
await act(async () => {
await userEvent.click(model3);
});
expect(onModelChangeMock).toHaveBeenCalledWith("model3");
});
it("should call the onAgentChange handler when the agent changes", () => {
it("should call the onAgentChange handler when the agent changes", async () => {
renderSettingsForm();
const agentInput = screen.getByRole("combobox", { name: "agent" });
act(() => {
userEvent.click(agentInput);
await act(async () => {
await userEvent.click(agentInput);
});
const agent3 = screen.getByText("agent3");
act(() => {
userEvent.click(agent3);
await act(async () => {
await userEvent.click(agent3);
});
expect(onAgentChangeMock).toHaveBeenCalledWith("agent3");
});
it("should call the onLanguageChange handler when the language changes", () => {
it("should call the onLanguageChange handler when the language changes", async () => {
renderSettingsForm();
const languageInput = screen.getByRole("combobox", { name: "language" });
act(() => {
userEvent.click(languageInput);
await act(async () => {
await userEvent.click(languageInput);
});
const french = screen.getByText("Français");
act(() => {
userEvent.click(french);
await act(async () => {
await userEvent.click(french);
});
expect(onLanguageChangeMock).toHaveBeenCalledWith("Français");
});
it("should call the onAPIKeyChange handler when the API key changes", () => {
it("should call the onAPIKeyChange handler when the API key changes", async () => {
renderSettingsForm();
const apiKeyInput = screen.getByTestId("apikey");
act(() => {
userEvent.type(apiKeyInput, "x");
await act(async () => {
await userEvent.type(apiKeyInput, "x");
});
expect(onAPIKeyChangeMock).toHaveBeenCalledWith("sk-...x");

View File

@@ -1,4 +1,4 @@
import { act, screen, waitFor } from "@testing-library/react";
import { screen, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import i18next from "i18next";
import React from "react";
@@ -47,6 +47,14 @@ vi.mock("#/services/options", async (importOriginal) => ({
.mockResolvedValue(Promise.resolve(["agent1", "agent2", "agent3"])),
}));
// Helper function to assert that fetchModels was called
async function assertModelsAndAgentsFetched() {
await waitFor(() => {
expect(fetchAgents).toHaveBeenCalledTimes(1);
expect(fetchModels).toHaveBeenCalledTimes(1);
});
}
describe("SettingsModal", () => {
afterEach(() => {
vi.clearAllMocks();
@@ -55,10 +63,7 @@ describe("SettingsModal", () => {
it("should fetch existing agents and models from the API", async () => {
renderWithProviders(<SettingsModal isOpen onOpenChange={vi.fn()} />);
await waitFor(() => {
expect(fetchModels).toHaveBeenCalledTimes(1);
expect(fetchAgents).toHaveBeenCalledTimes(1);
});
assertModelsAndAgentsFetched();
});
it("should close the modal when the close button is clicked", async () => {
@@ -71,8 +76,8 @@ describe("SettingsModal", () => {
name: /MODAL_CLOSE_BUTTON_LABEL/i, // i18n key
});
act(() => {
userEvent.click(cancelButton);
await act(async () => {
await userEvent.click(cancelButton);
});
expect(onOpenChange).toHaveBeenCalledWith(false);
@@ -111,21 +116,24 @@ describe("SettingsModal", () => {
),
);
// Use the helper function to assert models were fetched
await assertModelsAndAgentsFetched();
const saveButton = screen.getByRole("button", { name: /save/i });
const modelInput = screen.getByRole("combobox", { name: "model" });
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model3 = screen.getByText("model3");
act(() => {
userEvent.click(model3);
await act(async () => {
await userEvent.click(model3);
});
act(() => {
userEvent.click(saveButton);
await act(async () => {
await userEvent.click(saveButton);
});
expect(saveSettings).toHaveBeenCalledWith({
@@ -146,18 +154,18 @@ describe("SettingsModal", () => {
const saveButton = screen.getByRole("button", { name: /save/i });
const modelInput = screen.getByRole("combobox", { name: "model" });
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model3 = screen.getByText("model3");
act(() => {
userEvent.click(model3);
await act(async () => {
await userEvent.click(model3);
});
act(() => {
userEvent.click(saveButton);
await act(async () => {
await userEvent.click(saveButton);
});
expect(startNewSessionSpy).toHaveBeenCalled();
@@ -174,18 +182,18 @@ describe("SettingsModal", () => {
const saveButton = screen.getByRole("button", { name: /save/i });
const modelInput = screen.getByRole("combobox", { name: "model" });
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model3 = screen.getByText("model3");
act(() => {
userEvent.click(model3);
await act(async () => {
await userEvent.click(model3);
});
act(() => {
userEvent.click(saveButton);
await act(async () => {
await userEvent.click(saveButton);
});
expect(toastSpy).toHaveBeenCalledTimes(2);
@@ -202,18 +210,18 @@ describe("SettingsModal", () => {
const saveButton = screen.getByRole("button", { name: /save/i });
const languageInput = screen.getByRole("combobox", { name: "language" });
act(() => {
userEvent.click(languageInput);
await act(async () => {
await userEvent.click(languageInput);
});
const spanish = screen.getByText("Español");
act(() => {
userEvent.click(spanish);
await act(async () => {
await userEvent.click(spanish);
});
act(() => {
userEvent.click(saveButton);
await act(async () => {
await userEvent.click(saveButton);
});
expect(i18nSpy).toHaveBeenCalledWith("es");
@@ -227,21 +235,25 @@ describe("SettingsModal", () => {
),
);
await waitFor(() => {
expect(fetchModels).toHaveBeenCalledTimes(1);
});
const saveButton = screen.getByRole("button", { name: /save/i });
const modelInput = screen.getByRole("combobox", { name: "model" });
act(() => {
userEvent.click(modelInput);
await act(async () => {
await userEvent.click(modelInput);
});
const model3 = screen.getByText("model3");
act(() => {
userEvent.click(model3);
await act(async () => {
await userEvent.click(model3);
});
act(() => {
userEvent.click(saveButton);
await act(async () => {
await userEvent.click(saveButton);
});
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
@@ -261,17 +273,17 @@ describe("SettingsModal", () => {
});
const agentInput = screen.getByRole("combobox", { name: "agent" });
act(() => {
userEvent.click(agentInput);
await act(async () => {
await userEvent.click(agentInput);
});
const agent3 = screen.getByText("agent3");
act(() => {
userEvent.click(agent3);
await act(async () => {
await userEvent.click(agent3);
});
expect(agentInput).toHaveValue("agent3");
act(() => {
userEvent.click(resetButton);
await act(async () => {
await userEvent.click(resetButton);
});
expect(getDefaultSettings).toHaveBeenCalled();

View File

@@ -66,8 +66,6 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"