From 6bf523d31eb820c361001fc4a0f64cf2833b886b Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 22:45:59 +0000 Subject: [PATCH] story-kit: merge 112_story_add_test_coverage_for_app_tsx --- frontend/src/App.test.tsx | 185 +++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index b0eb4f8..56443bf 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { api } from "./api/client"; @@ -159,4 +159,187 @@ describe("App", () => { expect(screen.getByTitle("/home/user/project2")).toBeInTheDocument(); }); }); + + it("shows error when path input is empty", async () => { + await renderApp(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(/\/path\/to\/project/i), + ).toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText( + /\/path\/to\/project/i, + ) as HTMLInputElement; + await userEvent.clear(input); + + const openButton = screen.getByRole("button", { name: /open project/i }); + await userEvent.click(openButton); + + await waitFor(() => { + expect( + screen.getByText(/Please enter a project path/i), + ).toBeInTheDocument(); + }); + }); + + it("calls forgetKnownProject and removes project from list", async () => { + mockedApi.getKnownProjects.mockResolvedValue(["/home/user/project1"]); + mockedApi.forgetKnownProject.mockResolvedValue(true); + + await renderApp(); + + await waitFor(() => { + expect(screen.getByTitle("/home/user/project1")).toBeInTheDocument(); + }); + + const forgetButton = screen.getByRole("button", { + name: /Forget project1/i, + }); + await userEvent.click(forgetButton); + + await waitFor(() => { + expect(mockedApi.forgetKnownProject).toHaveBeenCalledWith( + "/home/user/project1", + ); + expect( + screen.queryByTitle("/home/user/project1"), + ).not.toBeInTheDocument(); + }); + }); + + it("closes project and returns to selection screen", async () => { + mockedApi.openProject.mockResolvedValue("/home/user/myproject"); + mockedApi.closeProject.mockResolvedValue(true); + + await renderApp(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(/\/path\/to\/project/i), + ).toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText( + /\/path\/to\/project/i, + ) as HTMLInputElement; + await userEvent.clear(input); + await userEvent.type(input, "/home/user/myproject"); + + const openButton = screen.getByRole("button", { name: /open project/i }); + await userEvent.click(openButton); + + await waitFor(() => { + expect(mockedApi.openProject).toHaveBeenCalledWith( + "/home/user/myproject", + ); + }); + + // Chat view should appear with close button + const closeButton = await waitFor(() => screen.getByText("✕")); + await userEvent.click(closeButton); + + await waitFor(() => { + expect(mockedApi.closeProject).toHaveBeenCalled(); + expect( + screen.getByPlaceholderText(/\/path\/to\/project/i), + ).toBeInTheDocument(); + }); + }); + + it("handles ArrowDown and ArrowUp keyboard navigation when suggestions are visible", async () => { + mockedApi.listDirectoryAbsolute.mockResolvedValue([ + { name: "projects", kind: "dir" }, + { name: "documents", kind: "dir" }, + ]); + + await renderApp(); + + // Wait for suggestions to appear after debounce + await waitFor( + () => { + expect(screen.getByText(/projects\//)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + + const input = screen.getByPlaceholderText(/\/path\/to\/project/i); + + // ArrowDown with matchList present — moves selection forward + fireEvent.keyDown(input, { key: "ArrowDown" }); + + // ArrowUp with matchList present — moves selection backward + fireEvent.keyDown(input, { key: "ArrowUp" }); + }); + + it("handles Tab keyboard navigation to accept suggestion", async () => { + mockedApi.listDirectoryAbsolute.mockResolvedValue([ + { name: "myrepo", kind: "dir" }, + ]); + + await renderApp(); + + await waitFor( + () => { + expect(screen.getByText(/myrepo\//)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + + const input = screen.getByPlaceholderText(/\/path\/to\/project/i); + + // Tab with matchList present — accepts the selected match + fireEvent.keyDown(input, { key: "Tab" }); + }); + + it("handles Escape key to close suggestions", async () => { + mockedApi.listDirectoryAbsolute.mockResolvedValue([ + { name: "workspace", kind: "dir" }, + ]); + + await renderApp(); + + await waitFor( + () => { + expect(screen.getByText(/workspace\//)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + + const input = screen.getByPlaceholderText(/\/path\/to\/project/i); + + // Escape closes suggestions + fireEvent.keyDown(input, { key: "Escape" }); + + await waitFor(() => { + expect(screen.queryByText(/workspace\//)).not.toBeInTheDocument(); + }); + }); + + it("handles Enter key to trigger project open", async () => { + mockedApi.openProject.mockResolvedValue("/home/user/myproject"); + + await renderApp(); + + await waitFor(() => { + expect( + screen.getByPlaceholderText(/\/path\/to\/project/i), + ).toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText( + /\/path\/to\/project/i, + ) as HTMLInputElement; + await userEvent.clear(input); + await userEvent.type(input, "/home/user/myproject"); + + fireEvent.keyDown(input, { key: "Enter" }); + + await waitFor(() => { + expect(mockedApi.openProject).toHaveBeenCalledWith( + "/home/user/myproject", + ); + }); + }); });