diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.test.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.test.tsx new file mode 100644 index 0000000000..a319fad46d --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.test.tsx @@ -0,0 +1,66 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { ClaudeCodeModal } from "./ClaudeCodeModal"; + +describe("ClaudeCodeModal", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should render nothing when isOpen is false", () => { + renderWithProviders( + + ); + expect(screen.queryByText(/Help us improve your experience/i)).not.toBeInTheDocument(); + }); + + it("should render the feedback modal content when isOpen is true", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Help us improve your experience/i)).toBeInTheDocument(); + }); + + it("should show the survey description text", () => { + renderWithProviders( + + ); + expect(screen.getByText(/your experience using LiteLLM with Claude Code/i)).toBeInTheDocument(); + }); + + it("should open the Google Form and call onComplete when the feedback button is clicked", async () => { + const onComplete = vi.fn(); + const openSpy = vi.spyOn(window, "open").mockImplementation(() => null); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Open Feedback Form/i })); + + expect(openSpy).toHaveBeenCalledWith( + "https://forms.gle/LZeJQ3XytBakckYa9", + "_blank", + "noopener,noreferrer" + ); + expect(onComplete).toHaveBeenCalled(); + }); + + it("should call onClose when the close button is clicked", async () => { + const onClose = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + // The X close button is the first button; the "Open Feedback Form" button is the second + const buttons = screen.getAllByRole("button"); + await user.click(buttons[0]); + + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.test.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.test.tsx new file mode 100644 index 0000000000..71e1a8d07b --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.test.tsx @@ -0,0 +1,82 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { ClaudeCodePrompt } from "./ClaudeCodePrompt"; + +vi.mock("./NudgePrompt", () => ({ + NudgePrompt: ({ + title, + description, + buttonText, + onOpen, + onDismiss, + isVisible, + }: { + title: string; + description: string; + buttonText: string; + onOpen: () => void; + onDismiss: () => void; + isVisible: boolean; + }) => { + if (!isVisible) return null; + return ( +
+ {title} + {description} + + +
+ ); + }, +})); + +describe("ClaudeCodePrompt", () => { + it("should render with the Claude Code Feedback title when visible", () => { + renderWithProviders( + + ); + expect(screen.getByText("Claude Code Feedback")).toBeInTheDocument(); + }); + + it("should render the correct description text", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Help us improve your Claude Code experience/i)).toBeInTheDocument(); + }); + + it("should call onOpen when the share feedback button is clicked", async () => { + const onOpen = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Share feedback/i })); + + expect(onOpen).toHaveBeenCalled(); + }); + + it("should call onDismiss when the dismiss button is clicked", async () => { + const onDismiss = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Dismiss/i })); + + expect(onDismiss).toHaveBeenCalled(); + }); + + it("should not render when isVisible is false", () => { + renderWithProviders( + + ); + expect(screen.queryByText("Claude Code Feedback")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/survey/SurveyModal.test.tsx b/ui/litellm-dashboard/src/components/survey/SurveyModal.test.tsx new file mode 100644 index 0000000000..33af6d28c4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/SurveyModal.test.tsx @@ -0,0 +1,200 @@ +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { SurveyModal } from "./SurveyModal"; + +describe("SurveyModal", () => { + beforeEach(() => { + vi.spyOn(global, "fetch").mockResolvedValue(new Response()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should render nothing when isOpen is false", () => { + renderWithProviders( + + ); + expect( + screen.queryByText(/Are you using LiteLLM at your company\?/i) + ).not.toBeInTheDocument(); + }); + + it("should render step 1 when the modal is opened", () => { + renderWithProviders( + + ); + expect( + screen.getByText(/Are you using LiteLLM at your company\?/i) + ).toBeInTheDocument(); + }); + + it("should disable the Next button until a step 1 choice is made", () => { + renderWithProviders( + + ); + expect(screen.getByRole("button", { name: /Next/i })).toBeDisabled(); + }); + + it("should enable the Next button after selecting Yes", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /We use it for work/i })); + + expect(screen.getByRole("button", { name: /Next/i })).not.toBeDisabled(); + }); + + it("should navigate to the company name step when Yes is selected and Next is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /We use it for work/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + + expect( + screen.getByText(/What company are you using LiteLLM at\?/i) + ).toBeInTheDocument(); + }); + + it("should skip the company name step when No is selected and go straight to step 3", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Personal project/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + + expect(screen.getByText(/When did you start using LiteLLM\?/i)).toBeInTheDocument(); + }); + + it("should show 5 total steps when using at a company", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /We use it for work/i })); + + expect(screen.getByText(/Step 1 of 5/i)).toBeInTheDocument(); + }); + + it("should show 4 total steps when not using at a company", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Personal project/i })); + + expect(screen.getByText(/Step 1 of 4/i)).toBeInTheDocument(); + }); + + it("should navigate back to step 1 from step 3 when No was previously selected", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Personal project/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + await user.click(screen.getByRole("button", { name: /Back/i })); + + expect( + screen.getByText(/Are you using LiteLLM at your company\?/i) + ).toBeInTheDocument(); + }); + + describe("when step 4 (reasons) is reached", () => { + async function navigateToStep4(user: ReturnType) { + // No path: step 1 → 3 → 4 + await user.click(screen.getByRole("button", { name: /Personal project/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + await user.click(screen.getByRole("radio", { name: /Less than a month ago/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + } + + it("should show a text input when the Other reason is selected", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await navigateToStep4(user); + await user.click(screen.getByRole("button", { name: /Something else not listed above/i })); + + expect(screen.getByPlaceholderText(/Please specify/i)).toBeInTheDocument(); + }); + + it("should keep the Next button disabled when Other is selected but the text field is empty", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await navigateToStep4(user); + await user.click(screen.getByRole("button", { name: /Something else not listed above/i })); + + expect(screen.getByRole("button", { name: /Next/i })).toBeDisabled(); + }); + + it("should enable Next when a standard reason is selected", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + + await navigateToStep4(user); + await user.click( + screen.getByRole("button", { name: /Stars, contributors, forks, community support/i }) + ); + + expect(screen.getByRole("button", { name: /Next/i })).not.toBeDisabled(); + }); + }); + + it("should call onComplete after successfully submitting the form", async () => { + const onComplete = vi.fn(); + const user = userEvent.setup(); + renderWithProviders( + + ); + + // Navigate through the No path: step 1 → 3 → 4 → 5 → submit + await user.click(screen.getByRole("button", { name: /Personal project/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + await user.click(screen.getByRole("radio", { name: /Less than a month ago/i })); + await user.click(screen.getByRole("button", { name: /Next/i })); + await user.click( + screen.getByRole("button", { name: /Stars, contributors, forks, community support/i }) + ); + await user.click(screen.getByRole("button", { name: /Next/i })); + // Step 5: email is optional + await user.click(screen.getByRole("button", { name: /Submit/i })); + + await waitFor(() => { + expect(onComplete).toHaveBeenCalled(); + }); + }); + + it("should call onClose when the close button is clicked", async () => { + const onClose = vi.fn(); + const user = userEvent.setup(); + renderWithProviders( + + ); + + // X close button is the first button in the modal header + const buttons = screen.getAllByRole("button"); + await user.click(buttons[0]); + + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/survey/SurveyPrompt.test.tsx b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.test.tsx new file mode 100644 index 0000000000..ae5bc1a9c6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.test.tsx @@ -0,0 +1,82 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { SurveyPrompt } from "./SurveyPrompt"; + +vi.mock("./NudgePrompt", () => ({ + NudgePrompt: ({ + title, + description, + buttonText, + onOpen, + onDismiss, + isVisible, + }: { + title: string; + description: string; + buttonText: string; + onOpen: () => void; + onDismiss: () => void; + isVisible: boolean; + }) => { + if (!isVisible) return null; + return ( +
+ {title} + {description} + + +
+ ); + }, +})); + +describe("SurveyPrompt", () => { + it("should render with the Quick feedback title when visible", () => { + renderWithProviders( + + ); + expect(screen.getByText("Quick feedback")).toBeInTheDocument(); + }); + + it("should render the correct description text", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Help us improve LiteLLM/i)).toBeInTheDocument(); + }); + + it("should call onOpen when the share feedback button is clicked", async () => { + const onOpen = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Share feedback/i })); + + expect(onOpen).toHaveBeenCalled(); + }); + + it("should call onDismiss when the dismiss button is clicked", async () => { + const onDismiss = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + + ); + + await user.click(screen.getByRole("button", { name: /Dismiss/i })); + + expect(onDismiss).toHaveBeenCalled(); + }); + + it("should not render when isVisible is false", () => { + renderWithProviders( + + ); + expect(screen.queryByText("Quick feedback")).not.toBeInTheDocument(); + }); +});