Просмотр исходного кода

refactor: Frontend tests (#2959)

* Refactor and clean

* Refactor, cleanup, and pass skipped test

* Refactor

* Refactor and cleanup

* Cleanup

* Refactor and cleanup

* Remove unused mock

* Refactor and cleanup

* Refactor

* Remove unused hooks

* Refactor
sp.wack 1 год назад
Родитель
Сommit
1fd2e511f8

+ 6 - 5
frontend/src/components/Browser.test.tsx

@@ -1,10 +1,11 @@
 import React from "react";
+import { screen } from "@testing-library/react";
 import Browser from "./Browser";
 import { renderWithProviders } from "../../test-utils";
 
 describe("Browser", () => {
   it("renders a message if no screenshotSrc is provided", () => {
-    const { getByText } = renderWithProviders(<Browser />, {
+    renderWithProviders(<Browser />, {
       preloadedState: {
         browser: {
           url: "https://example.com",
@@ -14,11 +15,11 @@ describe("Browser", () => {
     });
 
     // i18n empty message key
-    expect(getByText(/BROWSER\$EMPTY_MESSAGE/i)).toBeInTheDocument();
+    expect(screen.getByText("BROWSER$EMPTY_MESSAGE")).toBeInTheDocument();
   });
 
   it("renders the url and a screenshot", () => {
-    const { getByText, getByAltText } = renderWithProviders(<Browser />, {
+    renderWithProviders(<Browser />, {
       preloadedState: {
         browser: {
           url: "https://example.com",
@@ -28,7 +29,7 @@ describe("Browser", () => {
       },
     });
 
-    expect(getByText("https://example.com")).toBeInTheDocument();
-    expect(getByAltText(/browser screenshot/i)).toBeInTheDocument();
+    expect(screen.getByText("https://example.com")).toBeInTheDocument();
+    expect(screen.getByAltText(/browser screenshot/i)).toBeInTheDocument();
   });
 });

+ 0 - 3
frontend/src/components/chat/Chat.test.tsx

@@ -10,14 +10,11 @@ const MESSAGES: Message[] = [
   { sender: "assistant", content: "How can I help you today?" },
 ];
 
-HTMLElement.prototype.scrollTo = vi.fn(() => {});
-
 describe("Chat", () => {
   it("should render chat messages", () => {
     renderWithProviders(<Chat messages={MESSAGES} />);
 
     const messages = screen.getAllByTestId("message");
-
     expect(messages).toHaveLength(MESSAGES.length);
   });
 });

+ 52 - 57
frontend/src/components/chat/ChatInput.test.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import userEvent from "@testing-library/user-event";
-import { act, render, fireEvent } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
 import ChatInput from "./ChatInput";
 
 describe("ChatInput", () => {
@@ -11,109 +11,104 @@ describe("ChatInput", () => {
   const onSendMessage = vi.fn();
 
   it("should render a textarea", () => {
-    const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
-    const textarea = getByRole("textbox");
-    expect(textarea).toBeInTheDocument();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    expect(screen.getByRole("textbox")).toBeInTheDocument();
   });
 
-  it("should be able to be set as disabled", () => {
-    const { getByRole } = render(
-      <ChatInput disabled onSendMessage={onSendMessage} />,
-    );
-    const textarea = getByRole("textbox");
-    const button = getByRole("button");
+  it("should be able to be set as disabled", async () => {
+    const user = userEvent.setup();
+    render(<ChatInput disabled onSendMessage={onSendMessage} />);
+
+    const textarea = screen.getByRole("textbox");
+    const button = screen.getByRole("button");
 
     expect(textarea).not.toBeDisabled(); // user can still type
     expect(button).toBeDisabled(); // user cannot submit
 
-    act(() => {
-      userEvent.type(textarea, "Hello, world!{enter}");
-    });
+    await user.type(textarea, "Hello, world!");
+    await user.keyboard("{Enter}");
 
     expect(onSendMessage).not.toHaveBeenCalled();
   });
 
   it("should render with a placeholder", () => {
-    const { getByPlaceholderText } = render(
-      <ChatInput onSendMessage={onSendMessage} />,
+    render(<ChatInput onSendMessage={onSendMessage} />);
+
+    const textarea = screen.getByPlaceholderText(
+      /CHAT_INTERFACE\$INPUT_PLACEHOLDER/i,
     );
-    const textarea = getByPlaceholderText(/CHAT_INTERFACE\$INPUT_PLACEHOLDER/i);
     expect(textarea).toBeInTheDocument();
   });
 
   it("should render a send button", () => {
-    const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
-    const button = getByRole("button");
-    expect(button).toBeInTheDocument();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    expect(screen.getByRole("button")).toBeInTheDocument();
   });
 
   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");
+    const user = userEvent.setup();
+    render(<ChatInput onSendMessage={onSendMessage} />);
 
-    fireEvent.change(textarea, { target: { value: "Hello, world!" } });
+    const textarea = screen.getByRole("textbox");
+    const button = screen.getByRole("button");
 
-    await act(async () => {
-      await userEvent.click(button);
-    });
+    await user.type(textarea, "Hello, world!");
+    await user.click(button);
 
     expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
-
-    // Additionally, check if the callback is called exactly once
+    // Additionally, check if it was 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");
+  it("should be able to send a message when the enter key is pressed", async () => {
+    const user = userEvent.setup();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    const textarea = screen.getByRole("textbox");
 
-    fireEvent.change(textarea, { target: { value: "Hello, world!" } });
-    fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", charCode: 13 });
+    await user.type(textarea, "Hello, world!");
+    await user.keyboard("{Enter}");
 
     expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
   });
 
-  it("should NOT send a message when shift + enter is pressed", () => {
-    const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
-    const textarea = getByRole("textbox");
+  it("should NOT send a message when shift + enter is pressed", async () => {
+    const user = userEvent.setup();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    const textarea = screen.getByRole("textbox");
 
-    act(() => {
-      userEvent.type(textarea, "Hello, world!{shift}{enter}");
-    });
+    await user.type(textarea, "Hello, world!");
+    await user.keyboard("{Shift>} {Enter}"); // Shift + Enter
 
     expect(onSendMessage).not.toHaveBeenCalled();
   });
 
-  it("should NOT send an empty message", () => {
-    const { getByRole } = render(<ChatInput onSendMessage={onSendMessage} />);
-    const textarea = getByRole("textbox");
-    const button = getByRole("button");
+  it("should NOT send an empty message", async () => {
+    const user = userEvent.setup();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    const textarea = screen.getByRole("textbox");
+    const button = screen.getByRole("button");
 
-    act(() => {
-      userEvent.type(textarea, " {enter}"); // Only whitespace
-    });
+    await user.type(textarea, " ");
 
+    // with enter key
+    await user.keyboard("{Enter}");
     expect(onSendMessage).not.toHaveBeenCalled();
 
-    act(() => {
-      userEvent.click(button);
-    });
-
+    // with button click
+    await user.click(button);
     expect(onSendMessage).not.toHaveBeenCalled();
   });
 
   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");
-
-    fireEvent.change(textarea, { target: { value: "Hello, world!" } });
+    const user = userEvent.setup();
+    render(<ChatInput onSendMessage={onSendMessage} />);
+    const textarea = screen.getByRole("textbox");
+    const button = screen.getByRole("button");
 
+    await user.type(textarea, "Hello, world!");
     expect(textarea).toHaveValue("Hello, world!");
 
-    fireEvent.click(button);
-
+    await user.click(button);
     expect(textarea).toHaveValue("");
   });
 

+ 14 - 19
frontend/src/components/chat/ChatInterface.test.tsx

@@ -1,27 +1,20 @@
 import React from "react";
-import { screen, act, fireEvent } from "@testing-library/react";
+import { screen, act } from "@testing-library/react";
 import { describe, expect, it } from "vitest";
 import userEvent from "@testing-library/user-event";
 import { renderWithProviders } from "test-utils";
-import { useTranslation } from "react-i18next";
 import ChatInterface from "./ChatInterface";
 import Session from "#/services/session";
 import ActionType from "#/types/ActionType";
 import { addAssistantMessage } from "#/state/chatSlice";
 import AgentState from "#/types/AgentState";
-import { I18nKey } from "#/i18n/declaration";
-
-// avoid typing side-effect
-vi.mock("#/hooks/useTyping", () => ({
-  useTyping: vi.fn((text: string) => text),
-}));
 
 const sessionSpy = vi.spyOn(Session, "send");
-vi.spyOn(Session, "isConnected").mockImplementation(() => true);
+vi.spyOn(Session, "isConnected").mockReturnValue(true);
 
 // This is for the scrollview ref in Chat.tsx
 // TODO: Move this into test setup
-HTMLElement.prototype.scrollTo = vi.fn(() => {});
+HTMLElement.prototype.scrollTo = vi.fn().mockImplementation(() => {});
 
 describe("ChatInterface", () => {
   afterEach(() => {
@@ -33,7 +26,8 @@ describe("ChatInterface", () => {
     expect(screen.queryAllByTestId("message")).toHaveLength(0);
   });
 
-  it("should render the new message the user has typed", () => {
+  it("should render the new message the user has typed", async () => {
+    const user = userEvent.setup();
     renderWithProviders(<ChatInterface />, {
       preloadedState: {
         agent: {
@@ -43,7 +37,7 @@ describe("ChatInterface", () => {
     });
 
     const input = screen.getByRole("textbox");
-    fireEvent.change(input, { target: { value: "my message" } });
+    await user.type(input, "my message");
     expect(input).toHaveValue("my message");
   });
 
@@ -67,7 +61,8 @@ describe("ChatInterface", () => {
     expect(screen.getByText("Hello to you!")).toBeInTheDocument();
   });
 
-  it("should send a start event to the Session", () => {
+  it("should send a start event to the Session", async () => {
+    const user = userEvent.setup();
     renderWithProviders(<ChatInterface />, {
       preloadedState: {
         agent: {
@@ -77,8 +72,8 @@ describe("ChatInterface", () => {
     });
 
     const input = screen.getByRole("textbox");
-    fireEvent.change(input, { target: { value: "my message" } });
-    fireEvent.keyDown(input, { key: "Enter", code: "Enter", charCode: 13 });
+    await user.type(input, "my message");
+    await user.keyboard("{Enter}");
 
     const event = {
       action: ActionType.MESSAGE,
@@ -88,6 +83,7 @@ describe("ChatInterface", () => {
   });
 
   it("should send a user message event to the Session", async () => {
+    const user = userEvent.setup();
     renderWithProviders(<ChatInterface />, {
       preloadedState: {
         agent: {
@@ -97,7 +93,8 @@ describe("ChatInterface", () => {
     });
 
     const input = screen.getByRole("textbox");
-    await userEvent.type(input, "my message{enter}");
+    await user.type(input, "my message");
+    await user.keyboard("{Enter}");
 
     const event = {
       action: ActionType.MESSAGE,
@@ -115,10 +112,8 @@ describe("ChatInterface", () => {
       },
     });
 
-    const { t } = useTranslation();
-
     const submitButton = screen.getByLabelText(
-      t(I18nKey.CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE),
+      "CHAT_INTERFACE$TOOLTIP_SEND_MESSAGE",
     );
 
     expect(submitButton).toBeDisabled();

+ 0 - 5
frontend/src/components/chat/ChatMessage.test.tsx

@@ -3,11 +3,6 @@ import { describe, it, expect } from "vitest";
 import React from "react";
 import ChatMessage from "./ChatMessage";
 
-// avoid typing side-effect
-vi.mock("#/hooks/useTyping", () => ({
-  useTyping: vi.fn((text: string) => text),
-}));
-
 describe("Message", () => {
   it("should render a user message", () => {
     render(

+ 7 - 8
frontend/src/components/file-explorer/ExplorerTree.test.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import { screen } from "@testing-library/react";
 import { renderWithProviders } from "test-utils";
 import ExplorerTree from "./ExplorerTree";
 
@@ -10,20 +11,18 @@ describe("ExplorerTree", () => {
   });
 
   it("should render the explorer", () => {
-    const { getByText } = renderWithProviders(
-      <ExplorerTree files={FILES} defaultOpen />,
-    );
+    renderWithProviders(<ExplorerTree files={FILES} defaultOpen />);
 
-    expect(getByText("file-1-1.ts")).toBeInTheDocument();
-    expect(getByText("folder-1-2")).toBeInTheDocument();
+    expect(screen.getByText("file-1-1.ts")).toBeInTheDocument();
+    expect(screen.getByText("folder-1-2")).toBeInTheDocument();
     // TODO: make sure children render
   });
 
   it("should render the explorer given the defaultExpanded prop", () => {
-    const { queryByText } = renderWithProviders(<ExplorerTree files={FILES} />);
+    renderWithProviders(<ExplorerTree files={FILES} />);
 
-    expect(queryByText("file-1-1.ts")).toBeInTheDocument();
-    expect(queryByText("folder-1-2")).toBeInTheDocument();
+    expect(screen.queryByText("file-1-1.ts")).toBeInTheDocument();
+    expect(screen.queryByText("folder-1-2")).toBeInTheDocument();
     // TODO: make sure children don't render
   });
 

+ 50 - 83
frontend/src/components/file-explorer/FileExplorer.test.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { waitFor, act } from "@testing-library/react";
+import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { renderWithProviders } from "test-utils";
 import { describe, it, expect, vi, Mock } from "vitest";
@@ -24,112 +24,79 @@ vi.mock("../../services/fileService", async () => ({
   uploadFiles: vi.fn(),
 }));
 
+const renderFileExplorerWithRunningAgentState = () =>
+  renderWithProviders(<FileExplorer />, {
+    preloadedState: {
+      agent: {
+        curAgentState: AgentState.RUNNING,
+      },
+    },
+  });
+
 describe("FileExplorer", () => {
   afterEach(() => {
     vi.clearAllMocks();
   });
 
   it("should get the workspace directory", async () => {
-    const { getByText } = renderWithProviders(<FileExplorer />, {
-      preloadedState: {
-        agent: {
-          curAgentState: AgentState.RUNNING,
-        },
-      },
-    });
+    renderFileExplorerWithRunningAgentState();
 
-    await waitFor(() => {
-      expect(getByText("folder1")).toBeInTheDocument();
-      expect(getByText("file1.ts")).toBeInTheDocument();
-    });
+    expect(await screen.findByText("folder1")).toBeInTheDocument();
+    expect(await screen.findByText("file1.ts")).toBeInTheDocument();
     expect(listFiles).toHaveBeenCalledTimes(1); // once for root
   });
 
   it.todo("should render an empty workspace");
 
   it("should refetch the workspace when clicking the refresh button", async () => {
-    const { getByText, getByTestId } = renderWithProviders(<FileExplorer />, {
-      preloadedState: {
-        agent: {
-          curAgentState: AgentState.RUNNING,
-        },
-      },
-    });
-    await waitFor(() => {
-      expect(getByText("folder1")).toBeInTheDocument();
-      expect(getByText("file1.ts")).toBeInTheDocument();
-    });
+    const user = userEvent.setup();
+    renderFileExplorerWithRunningAgentState();
+
+    expect(await screen.findByText("folder1")).toBeInTheDocument();
+    expect(await screen.findByText("file1.ts")).toBeInTheDocument();
     expect(listFiles).toHaveBeenCalledTimes(1); // once for root
 
-    await act(async () => {
-      await userEvent.click(getByTestId("refresh"));
-    });
+    const refreshButton = screen.getByTestId("refresh");
+    await user.click(refreshButton);
 
-    await waitFor(() => {
-      expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for refresh button
-    });
+    expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for refresh button
   });
 
-  it("should toggle the explorer visibility when clicking the close button", async () => {
-    const { getByTestId, getByText, queryByText } = renderWithProviders(
-      <FileExplorer />,
-      {
-        preloadedState: {
-          agent: {
-            curAgentState: AgentState.RUNNING,
-          },
-        },
-      },
-    );
+  it("should toggle the explorer visibility when clicking the toggle button", async () => {
+    const user = userEvent.setup();
+    renderFileExplorerWithRunningAgentState();
 
-    await waitFor(() => {
-      expect(getByText("folder1")).toBeInTheDocument();
-    });
+    const folder1 = await screen.findByText("folder1");
+    expect(folder1).toBeInTheDocument();
 
-    await act(async () => {
-      await userEvent.click(getByTestId("toggle"));
-    });
+    const toggleButton = screen.getByTestId("toggle");
+    await user.click(toggleButton);
 
-    expect(queryByText("folder1")).toBeInTheDocument();
-    expect(queryByText("folder1")).not.toBeVisible();
+    expect(folder1).toBeInTheDocument();
+    expect(folder1).not.toBeVisible();
   });
 
   it("should upload files", async () => {
-    // TODO: Improve this test by passing expected argument to `uploadFiles`
-    const { findByTestId } = renderWithProviders(<FileExplorer />, {
-      preloadedState: {
-        agent: {
-          curAgentState: AgentState.RUNNING,
-        },
-      },
-    });
+    const user = userEvent.setup();
+    renderFileExplorerWithRunningAgentState();
 
     const file = new File([""], "file-name");
-    const file2 = new File([""], "file-name-2");
-
-    const uploadFileInput = await findByTestId("file-input");
-
-    await act(async () => {
-      await userEvent.upload(uploadFileInput, file);
-    });
+    const uploadFileInput = await screen.findByTestId("file-input");
+    await user.upload(uploadFileInput, file);
 
+    // TODO: Improve this test by passing expected argument to `uploadFiles`
     expect(uploadFiles).toHaveBeenCalledOnce();
     expect(listFiles).toHaveBeenCalled();
 
-    const uploadDirInput = await findByTestId("file-input");
-
-    // The 'await' keyword is required here to avoid a warning during test runs
-    await act(async () => {
-      await userEvent.upload(uploadDirInput, [file, file2]);
-    });
+    const file2 = new File([""], "file-name-2");
+    const uploadDirInput = await screen.findByTestId("file-input");
+    await user.upload(uploadDirInput, [file, file2]);
 
-    await waitFor(() => {
-      expect(uploadFiles).toHaveBeenCalledTimes(2);
-      expect(listFiles).toHaveBeenCalled();
-    });
+    expect(uploadFiles).toHaveBeenCalledTimes(2);
+    expect(listFiles).toHaveBeenCalled();
   });
 
-  it.skip("should upload files when dragging them to the explorer", () => {
+  it.todo("should upload files when dragging them to the explorer", () => {
     // It will require too much work to mock drag logic, especially for our case
     // https://github.com/testing-library/user-event/issues/440#issuecomment-685010755
     // TODO: should be tested in an e2e environment such as Cypress/Playwright
@@ -137,20 +104,20 @@ describe("FileExplorer", () => {
 
   it.todo("should download a file");
 
-  it.todo("should display an error toast if file upload fails", async () => {
+  it("should display an error toast if file upload fails", async () => {
     (uploadFiles as Mock).mockRejectedValue(new Error());
+    const user = userEvent.setup();
+    renderFileExplorerWithRunningAgentState();
 
-    const { getByTestId } = renderWithProviders(<FileExplorer />);
-
-    const uploadFileInput = getByTestId("file-input");
+    const uploadFileInput = await screen.findByTestId("file-input");
     const file = new File([""], "test");
 
-    await act(async () => {
-      await userEvent.upload(uploadFileInput, file);
-    });
+    await user.upload(uploadFileInput, file);
 
     expect(uploadFiles).rejects.toThrow();
-    // TODO: figure out why spy isn't called to pass test
-    expect(toastSpy).toHaveBeenCalledWith("ws", "Error uploading file");
+    expect(toastSpy).toHaveBeenCalledWith(
+      expect.stringContaining("upload-error"),
+      expect.any(String),
+    );
   });
 });

+ 51 - 64
frontend/src/components/file-explorer/TreeNode.test.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { waitFor, act } from "@testing-library/react";
+import { screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { renderWithProviders } from "test-utils";
 import TreeNode from "./TreeNode";
@@ -25,106 +25,93 @@ describe("TreeNode", () => {
   });
 
   it("should render a file if property has no children", () => {
-    const { getByText } = renderWithProviders(
-      <TreeNode path="/file.ts" defaultOpen />,
-    );
-
-    expect(getByText("file.ts")).toBeInTheDocument();
+    renderWithProviders(<TreeNode path="/file.ts" defaultOpen />);
+    expect(screen.getByText("file.ts")).toBeInTheDocument();
   });
 
   it("should render a folder if it's in a subdir", async () => {
-    const { findByText } = renderWithProviders(
-      <TreeNode path="/folder1/" defaultOpen />,
-    );
+    renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
     expect(listFiles).toHaveBeenCalledWith("/folder1/");
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(await findByText("file2.ts")).toBeInTheDocument();
+    expect(await screen.findByText("folder1")).toBeInTheDocument();
+    expect(await screen.findByText("file2.ts")).toBeInTheDocument();
   });
 
   it("should close a folder when clicking on it", async () => {
-    const { findByText, queryByText } = renderWithProviders(
-      <TreeNode path="/folder1/" defaultOpen />,
-    );
+    const user = userEvent.setup();
+    renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
+
+    const folder1 = await screen.findByText("folder1");
+    const file2 = await screen.findByText("file2.ts");
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(await findByText("file2.ts")).toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(file2).toBeInTheDocument();
 
-    await act(async () => {
-      await userEvent.click(await findByText("folder1"));
-    });
+    await user.click(folder1);
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(queryByText("file2.ts")).not.toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
   });
 
   it("should open a folder when clicking on it", async () => {
-    const { getByText, findByText, queryByText } = renderWithProviders(
-      <TreeNode path="/folder1/" />,
-    );
+    const user = userEvent.setup();
+    renderWithProviders(<TreeNode path="/folder1/" />);
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(queryByText("file2.ts")).not.toBeInTheDocument();
+    const folder1 = await screen.findByText("folder1");
 
-    await act(async () => {
-      await userEvent.click(getByText("folder1"));
-    });
+    expect(folder1).toBeInTheDocument();
+    expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
+
+    await user.click(folder1);
     expect(listFiles).toHaveBeenCalledWith("/folder1/");
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(await findByText("file2.ts")).toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(await screen.findByText("file2.ts")).toBeInTheDocument();
   });
 
-  it("should call a fn and return the full path of a file when clicking on it", async () => {
-    const { getByText } = renderWithProviders(
-      <TreeNode path="/folder1/file2.ts" defaultOpen />,
-    );
+  it("should call `selectFile` and return the full path of a file when clicking on a file", async () => {
+    const user = userEvent.setup();
+    renderWithProviders(<TreeNode path="/folder1/file2.ts" defaultOpen />);
 
-    await act(async () => {
-      await userEvent.click(getByText("file2.ts"));
-    });
+    const file2 = screen.getByText("file2.ts");
+    await user.click(file2);
 
-    await waitFor(() => {
-      expect(selectFile).toHaveBeenCalledWith("/folder1/file2.ts");
-    });
+    expect(selectFile).toHaveBeenCalledWith("/folder1/file2.ts");
   });
 
-  it("should render the explorer given the defaultOpen prop", async () => {
-    const { getByText, findByText, queryByText } = renderWithProviders(
-      <TreeNode path="/" defaultOpen />,
-    );
+  it("should render the full explorer given the defaultOpen prop", async () => {
+    const user = userEvent.setup();
+    renderWithProviders(<TreeNode path="/" defaultOpen />);
 
     expect(listFiles).toHaveBeenCalledWith("/");
 
-    expect(await findByText("file1.ts")).toBeInTheDocument();
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(queryByText("file2.ts")).not.toBeInTheDocument();
+    const file1 = await screen.findByText("file1.ts");
+    const folder1 = await screen.findByText("folder1");
 
-    await act(async () => {
-      await userEvent.click(getByText("folder1"));
-    });
+    expect(file1).toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
 
+    await user.click(folder1);
     expect(listFiles).toHaveBeenCalledWith("folder1/");
 
-    expect(await findByText("file1.ts")).toBeInTheDocument();
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(await findByText("file2.ts")).toBeInTheDocument();
+    expect(file1).toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(await screen.findByText("file2.ts")).toBeInTheDocument();
   });
 
   it("should render all children as collapsed when defaultOpen is false", async () => {
-    const { findByText, getByText, queryByText } = renderWithProviders(
-      <TreeNode path="/folder1/" />,
-    );
+    renderWithProviders(<TreeNode path="/folder1/" defaultOpen={false} />);
+
+    const folder1 = await screen.findByText("folder1");
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(queryByText("file2.ts")).not.toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
 
-    await act(async () => {
-      await userEvent.click(getByText("folder1"));
-    });
+    await userEvent.click(folder1);
     expect(listFiles).toHaveBeenCalledWith("/folder1/");
 
-    expect(await findByText("folder1")).toBeInTheDocument();
-    expect(await findByText("file2.ts")).toBeInTheDocument();
+    expect(folder1).toBeInTheDocument();
+    expect(await screen.findByText("file2.ts")).toBeInTheDocument();
   });
 });

+ 24 - 29
frontend/src/components/terminal/Terminal.test.tsx

@@ -9,24 +9,19 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({
   disconnect: vi.fn(),
 }));
 
-const openMock = vi.fn();
-const writeMock = vi.fn();
-const writelnMock = vi.fn();
-const disposeMock = vi.fn();
-const onKeyMock = vi.fn();
-const attachCustomKeyEventHandlerMock = vi.fn();
+const mockTerminal = {
+  open: vi.fn(),
+  write: vi.fn(),
+  writeln: vi.fn(),
+  dispose: vi.fn(),
+  onKey: vi.fn(),
+  attachCustomKeyEventHandler: vi.fn(),
+  loadAddon: vi.fn(),
+};
 
 vi.mock("@xterm/xterm", async (importOriginal) => ({
   ...(await importOriginal<typeof import("@xterm/xterm")>()),
-  Terminal: vi.fn(() => ({
-    open: openMock,
-    write: writeMock,
-    writeln: writelnMock,
-    dispose: disposeMock,
-    onKey: onKeyMock,
-    attachCustomKeyEventHandler: attachCustomKeyEventHandlerMock,
-    loadAddon: vi.fn(),
-  })),
+  Terminal: vi.fn().mockImplementation(() => mockTerminal),
 }));
 
 const renderTerminal = (commands: Command[] = []) =>
@@ -47,9 +42,9 @@ describe("Terminal", () => {
     renderTerminal();
 
     expect(screen.getByText("Terminal")).toBeInTheDocument();
-    expect(openMock).toHaveBeenCalledTimes(1);
+    expect(mockTerminal.open).toHaveBeenCalledTimes(1);
 
-    expect(writeMock).toHaveBeenCalledWith("$ ");
+    expect(mockTerminal.write).toHaveBeenCalledWith("$ ");
   });
 
   it("should load commands to the terminal", () => {
@@ -58,8 +53,8 @@ describe("Terminal", () => {
       { type: "output", content: "OUTPUT" },
     ]);
 
-    expect(writelnMock).toHaveBeenNthCalledWith(1, "INPUT");
-    expect(writelnMock).toHaveBeenNthCalledWith(2, "OUTPUT");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "INPUT");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "OUTPUT");
   });
 
   it("should write commands to the terminal", () => {
@@ -70,14 +65,14 @@ describe("Terminal", () => {
       store.dispatch(appendOutput("Hello"));
     });
 
-    expect(writelnMock).toHaveBeenNthCalledWith(1, "echo Hello");
-    expect(writelnMock).toHaveBeenNthCalledWith(2, "Hello");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo Hello");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello");
 
     act(() => {
       store.dispatch(appendInput("echo World"));
     });
 
-    expect(writelnMock).toHaveBeenNthCalledWith(3, "echo World");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo World");
   });
 
   it("should load and write commands to the terminal", () => {
@@ -86,14 +81,14 @@ describe("Terminal", () => {
       { type: "output", content: "Hello" },
     ]);
 
-    expect(writelnMock).toHaveBeenNthCalledWith(1, "echo Hello");
-    expect(writelnMock).toHaveBeenNthCalledWith(2, "Hello");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo Hello");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello");
 
     act(() => {
       store.dispatch(appendInput("echo Hello"));
     });
 
-    expect(writelnMock).toHaveBeenNthCalledWith(3, "echo Hello");
+    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo Hello");
   });
 
   it("should end the line with a dollar sign after writing a command", () => {
@@ -103,18 +98,18 @@ describe("Terminal", () => {
       store.dispatch(appendInput("echo Hello"));
     });
 
-    expect(writelnMock).toHaveBeenCalledWith("echo Hello");
-    expect(writeMock).toHaveBeenCalledWith("$ ");
+    expect(mockTerminal.writeln).toHaveBeenCalledWith("echo Hello");
+    expect(mockTerminal.write).toHaveBeenCalledWith("$ ");
   });
 
   // This test fails because it expects `disposeMock` to have been called before the component is unmounted.
   it.skip("should dispose the terminal on unmount", () => {
     const { unmount } = renderWithProviders(<Terminal />);
 
-    expect(disposeMock).not.toHaveBeenCalled();
+    expect(mockTerminal.dispose).not.toHaveBeenCalled();
 
     unmount();
 
-    expect(disposeMock).toHaveBeenCalledTimes(1);
+    expect(mockTerminal.dispose).toHaveBeenCalledTimes(1);
   });
 });

+ 0 - 41
frontend/src/hooks/useTyping.test.ts

@@ -1,41 +0,0 @@
-import { act, renderHook } from "@testing-library/react";
-import { describe, it, vi } from "vitest";
-import { useTyping } from "./useTyping";
-
-vi.useFakeTimers();
-
-describe("useTyping", () => {
-  it("should 'type' a given message", () => {
-    const text = "Hello, World!";
-    const typingSpeed = 10;
-
-    const { result } = renderHook(() => useTyping(text));
-    expect(result.current).toBe("H");
-
-    act(() => {
-      vi.advanceTimersByTime(typingSpeed);
-    });
-
-    expect(result.current).toBe("He");
-
-    act(() => {
-      vi.advanceTimersByTime(typingSpeed);
-    });
-
-    expect(result.current).toBe("Hel");
-
-    for (let i = 3; i < text.length; i += 1) {
-      act(() => {
-        vi.advanceTimersByTime(typingSpeed);
-      });
-    }
-
-    expect(result.current).toBe("Hello, World!");
-
-    act(() => {
-      vi.advanceTimersByTime(typingSpeed);
-    });
-
-    expect(result.current).toBe("Hello, World!");
-  });
-});

+ 0 - 23
frontend/src/hooks/useTyping.ts

@@ -1,23 +0,0 @@
-import React from "react";
-
-export const useTyping = (text: string) => {
-  const [message, setMessage] = React.useState(text[0]);
-
-  const advance = () =>
-    setTimeout(() => {
-      if (message.length < text.length) {
-        setMessage(text.slice(0, message.length + 1));
-      }
-    }, 10);
-
-  React.useEffect(() => {
-    const timeout = advance();
-
-    return () => {
-      clearTimeout(timeout);
-    };
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [message]);
-
-  return message;
-};

+ 3 - 2
frontend/src/services/auth.test.ts

@@ -10,9 +10,10 @@ describe("Auth Service", () => {
   });
 
   describe("getToken", () => {
-    it("should fetch and return a token", async () => {
+    it("should fetch and return a token", () => {
       (Storage.prototype.getItem as Mock).mockReturnValue("newToken");
-      const data = await getToken();
+
+      const data = getToken();
       expect(localStorage.getItem).toHaveBeenCalledWith("token"); // Used to set Authorization header
       expect(data).toEqual("newToken");
     });

+ 5 - 8
frontend/src/services/session.test.ts

@@ -1,17 +1,14 @@
 import { describe, expect, it, vi } from "vitest";
-
 import ActionType from "#/types/ActionType";
 import { Settings, saveSettings } from "./settings";
 import Session from "./session";
 
 const sendSpy = vi.spyOn(Session, "send");
-const setupSpy = vi
-  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
-  .spyOn(Session as any, "_setupSocket")
-  .mockImplementation(() => {
-    /* eslint-disable-next-line @typescript-eslint/dot-notation */
-    Session["_initializeAgent"](); // use key syntax to fix complaint about private fn
-  });
+// @ts-expect-error - spying on private function
+const setupSpy = vi.spyOn(Session, "_setupSocket").mockImplementation(() => {
+  // @ts-expect-error - calling a private function
+  Session._initializeAgent();
+});
 
 describe("startNewSession", () => {
   it("Should start a new session with the current settings", () => {