Skip to content
Open
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
2 changes: 1 addition & 1 deletion frontend/src/pages/Chat/components/FeedbackModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function FeedbackModal({ messages, setOpenFeedback }: Props) {
border border-green-medium hover:border-green-dark
hover:bg-green-light`}
onClick={() => {
if (feedback.trim() === "") handleModalClose();
if (feedback.trim() === "") return handleModalClose();
setStatus("sending");
setTimeout(() => {
sendFeedback(messages, feedback, emailsToCC, wordsToRedact);
Expand Down
25 changes: 9 additions & 16 deletions frontend/src/tests/components/About.test.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";
import About from "../../About";
import { MemoryRouter } from "react-router-dom";

describe("About component", () => {
it("renders without crashing", () => {
beforeEach(() => {
render(
<MemoryRouter>
<About />
</MemoryRouter>,
);
expect(screen.getAllByText("About Tenant First Aid")).not.toBeNull();
});

it("renders without crashing", () => {
expect(screen.getByText("About Tenant First Aid")).toBeInTheDocument();
});

it("displays legal disclaimer section", () => {
render(
<MemoryRouter>
<About />
</MemoryRouter>,
);
expect(
screen.getAllByText("Legal Disclaimer & Privacy Notice"),
).not.toBeNull();
screen.getByText("Legal Disclaimer & Privacy Notice"),
).toBeInTheDocument();
});

it("displays contact information", () => {
render(
<MemoryRouter>
<About />
</MemoryRouter>,
);
expect(screen.getAllByText("michael@qiu-qiulaw.com")).not.toBeNull();
expect(screen.getByText("michael@qiu-qiulaw.com")).toBeInTheDocument();
});
});
28 changes: 28 additions & 0 deletions frontend/src/tests/components/AutoExpandText.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import AutoExpandText from "../../pages/Chat/components/AutoExpandText";

describe("AutoExpandText", () => {
it("renders children", () => {
render(<AutoExpandText isExpanded={true}>hello</AutoExpandText>);
expect(screen.getByText("hello")).toBeInTheDocument();
});

it("applies expanded classes when isExpanded is true", () => {
const { container } = render(
<AutoExpandText isExpanded={true}>content</AutoExpandText>,
);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("grid-rows-[1fr]");
expect(wrapper.className).not.toContain("opacity-0");
});

it("applies collapsed classes when isExpanded is false", () => {
const { container } = render(
<AutoExpandText isExpanded={false}>content</AutoExpandText>,
);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("grid-rows-[0fr]");
expect(wrapper.className).toContain("opacity-0");
});
});
14 changes: 4 additions & 10 deletions frontend/src/tests/components/ChatDisclaimer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cleanup, render, screen } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { describe, it, afterEach } from "vitest";
import { describe, it } from "vitest";

const renderChatDisclaimer = async (isOngoing: boolean) => {
const { default: ChatDisclaimer } =
Expand All @@ -17,21 +17,15 @@ const renderChatDisclaimer = async (isOngoing: boolean) => {
};

describe("ChatDisclaimer component", () => {
afterEach(() => {
cleanup();
});

it("renders About page link when onGoing is true", async () => {
const mockIsOngoing = true;
await renderChatDisclaimer(mockIsOngoing);
await renderChatDisclaimer(true);

const aboutLink = screen.getByRole("link", { name: "to about page" });
expect(aboutLink).toHaveAttribute("href", "/about");
});

it("does not render About page link when isOngoing is false", async () => {
const mockIsOngoing = false;
await renderChatDisclaimer(mockIsOngoing);
await renderChatDisclaimer(false);

const aboutLink = screen.queryByRole("link", { name: "to about page" });
expect(aboutLink).not.toBeInTheDocument();
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/tests/components/ExportMessagesButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { HumanMessage } from "@langchain/core/messages";
import ExportMessagesButton from "../../pages/Chat/components/ExportMessagesButton";

vi.mock("../../pages/Chat/utils/exportHelper", () => ({
default: vi.fn(),
}));

import exportMessages from "../../pages/Chat/utils/exportHelper";

describe("ExportMessagesButton", () => {
it("renders an Export button", () => {
render(<ExportMessagesButton messages={[]} />);
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
});

it("calls exportMessages with the provided messages on click", () => {
const messages = [new HumanMessage({ content: "hello", id: "1" })];
render(<ExportMessagesButton messages={messages} />);

fireEvent.click(screen.getByRole("button", { name: "Export" }));

expect(exportMessages).toHaveBeenCalledWith(messages);
});
});
86 changes: 86 additions & 0 deletions frontend/src/tests/components/FeedbackModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { render, screen, fireEvent, act } from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { HumanMessage } from "@langchain/core/messages";
import FeedbackModal from "../../pages/Chat/components/FeedbackModal";

vi.mock("../../pages/Chat/utils/feedbackHelper", () => ({
default: vi.fn(),
}));

import sendFeedback from "../../pages/Chat/utils/feedbackHelper";

describe("FeedbackModal", () => {
const messages = [new HumanMessage({ content: "hello", id: "1" })];

afterEach(() => {
vi.clearAllMocks();
});

it("renders the feedback form in idle state", () => {
render(<FeedbackModal messages={messages} setOpenFeedback={vi.fn()} />);

expect(
screen.getByPlaceholderText(/please enter your feedback/i),
).toBeInTheDocument();
expect(screen.getByPlaceholderText(/enter email/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/word.*to redact/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
});

it("calls setOpenFeedback(false) when Close is clicked", () => {
const setOpenFeedback = vi.fn();
render(
<FeedbackModal messages={messages} setOpenFeedback={setOpenFeedback} />,
);

fireEvent.click(screen.getByRole("button", { name: "Close" }));

expect(setOpenFeedback).toHaveBeenCalledWith(false);
});

it("accepts input in all fields and passes them to sendFeedback on submit", () => {
vi.useFakeTimers();
render(<FeedbackModal messages={messages} setOpenFeedback={vi.fn()} />);

fireEvent.change(
screen.getByPlaceholderText(/please enter your feedback/i),
{
target: { value: "great bot" },
},
);
fireEvent.change(screen.getByPlaceholderText(/enter email/i), {
target: { value: "user@example.com" },
});
fireEvent.change(screen.getByPlaceholderText(/word.*to redact/i), {
target: { value: "landlord" },
});
fireEvent.click(screen.getByRole("button", { name: "Send" }));

expect(screen.getByText("Feedback Sent!")).toBeInTheDocument();

act(() => {
vi.runAllTimers();
});
vi.useRealTimers();

expect(sendFeedback).toHaveBeenCalledWith(
messages,
"great bot",
"user@example.com",
"landlord",
);
});

it("closes immediately when Send is clicked with empty feedback", () => {
const setOpenFeedback = vi.fn();
render(
<FeedbackModal messages={messages} setOpenFeedback={setOpenFeedback} />,
);

fireEvent.click(screen.getByRole("button", { name: "Send" }));

expect(setOpenFeedback).toHaveBeenCalledWith(false);
expect(screen.queryByText("Feedback Sent!")).not.toBeInTheDocument();
});
});
30 changes: 28 additions & 2 deletions frontend/src/tests/components/HousingContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ function UpdateLocationButton() {
return (
<button
data-testid="update-loc"
onClick={() => handleHousingLocation?.({ city: "portland", state: "or" })}
onClick={() => handleHousingLocation({ city: "portland", state: "or" })}
>
update
</button>
);
}

function ResetButton() {
const { handleFormReset } = useHousingContext();
return (
<button data-testid="reset" onClick={handleFormReset}>
reset
</button>
);
}

describe("HousingContext", () => {
it("provides initial context values", () => {
render(
Expand All @@ -28,7 +37,7 @@ describe("HousingContext", () => {
</HousingContextProvider>,
);

const dump = screen.getByTestId("ctx").textContent || "";
const dump = screen.getByTestId("ctx").textContent ?? "";
expect(dump).toContain("housingLocation");
expect(dump).toContain("housingType");
expect(dump).toContain("tenantTopic");
Expand All @@ -51,6 +60,23 @@ describe("HousingContext", () => {
expect(screen.getByTestId("ctx").textContent).toContain('"or"');
});

it("resets housingLocation when handleFormReset is called", () => {
render(
<HousingContextProvider>
<ContextDump />
<UpdateLocationButton />
<ResetButton />
</HousingContextProvider>,
);

fireEvent.click(screen.getByTestId("update-loc"));
expect(screen.getByTestId("ctx").textContent).toContain("portland");

fireEvent.click(screen.getByTestId("reset"));

expect(screen.getByTestId("ctx").textContent).not.toContain("portland");
});

it("throws error when used outside provider", () => {
const renderOutside = () => render(<ContextDump />);
expect(renderOutside).toThrow();
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/tests/components/InitializationForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,9 @@ describe("InitializationForm", () => {

// Changes styling for generate button when other is selected
await waitFor(() => {
const genButton = screen.queryByRole("link", {
name: "generate letter",
});
if (genButton) {
expect(genButton).toHaveClass("opacity-50");
}
expect(screen.getByRole("link", { name: "generate letter" })).toHaveClass(
"opacity-50",
);
});
});
});
94 changes: 94 additions & 0 deletions frontend/src/tests/components/InputField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { createRef } from "react";
import InputField from "../../pages/Chat/components/InputField";
import HousingContextProvider from "../../contexts/HousingContext";

vi.mock("../../pages/Chat/utils/streamHelper", () => ({
streamText: vi.fn(),
}));

function renderInputField(
overrides: Partial<React.ComponentProps<typeof InputField>> = {},
) {
const inputRef = createRef<HTMLTextAreaElement>();
const props = {
addMessage: vi.fn(),
setMessages: vi.fn(),
isLoading: false,
setIsLoading: vi.fn(),
value: "",
inputRef,
onChange: vi.fn(),
...overrides,
};

render(
<HousingContextProvider>
<InputField {...props} />
</HousingContextProvider>,
);

return props;
}

describe("InputField", () => {
it("renders a textarea and Send button", () => {
renderInputField();

expect(screen.getByRole("textbox")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument();
});

it("disables the textarea and button when isLoading is true", () => {
renderInputField({ isLoading: true });

expect(screen.getByRole("textbox")).toBeDisabled();
expect(screen.getByRole("button", { name: "..." })).toBeDisabled();
});

it("disables the Send button when value is empty or whitespace", () => {
renderInputField({ value: " " });
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
});

it("enables the Send button when value has content", () => {
renderInputField({ value: "hello" });
expect(screen.getByRole("button", { name: "Send" })).not.toBeDisabled();
});

it("calls onChange when the textarea value changes", () => {
const onChange = vi.fn();
renderInputField({ onChange });

fireEvent.change(screen.getByRole("textbox"), {
target: { value: "new text" },
});

expect(onChange).toHaveBeenCalled();
});

it("clears input and adds user message when Send is clicked with content", async () => {
const setMessages = vi.fn();
const onChange = vi.fn();
renderInputField({ setMessages, onChange, value: "hello" });

fireEvent.click(screen.getByRole("button", { name: "Send" }));

await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ target: { value: "" } }),
);
expect(setMessages).toHaveBeenCalled();
});
});

it("does not send on Enter if value is empty", () => {
const setMessages = vi.fn();
renderInputField({ setMessages, value: "" });

fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" });

expect(setMessages).not.toHaveBeenCalled();
});
});
Loading
Loading