Download __tests__/components/features/launch/plugin-launch-modal.test.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/components/features/launch/plugin-launch-modal.test.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/__tests__/components/features/launch/plugin-launch-modal.test.tsx
-
curl -L -o plugin-launch-modal.test.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/components/features/launch/plugin-launch-modal.test.tsx
14.1 kB
| import { render, screen, waitFor } from "@testing-library/react"; | |
| import { describe, it, expect, vi, beforeEach } from "vitest"; | |
| import userEvent from "@testing-library/user-event"; | |
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| import { PluginLaunchModal } from "#/components/features/launch/plugin-launch-modal"; | |
| import { PluginSpec } from "#/api/conversation-service/agent-server-conversation-service.types"; | |
| const mockOnStartConversation = vi.fn(); | |
| const mockOnClose = vi.fn(); | |
| function renderModal( | |
| plugins: PluginSpec[], | |
| props: Partial<{ | |
| message: string; | |
| isLoading: boolean; | |
| }> = {}, | |
| ) { | |
| const queryClient = new QueryClient({ | |
| defaultOptions: { queries: { retry: false } }, | |
| }); | |
| return render( | |
| <QueryClientProvider client={queryClient}> | |
| <PluginLaunchModal | |
| plugins={plugins} | |
| message={props.message} | |
| isLoading={props.isLoading ?? false} | |
| onStartConversation={mockOnStartConversation} | |
| onClose={mockOnClose} | |
| /> | |
| </QueryClientProvider>, | |
| ); | |
| } | |
| describe("PluginLaunchModal", () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| describe("Plugin Display Name Extraction", () => { | |
| it("should extract plugin name from repo_path when provided", () => { | |
| renderModal([{ source: "github:owner/repo", repo_path: "plugins/my-plugin" }]); | |
| // Plugin name should be "my-plugin" from the path | |
| expect(screen.getByText("my-plugin")).toBeInTheDocument(); | |
| }); | |
| it("should show repo path when no repo_path (repo IS the plugin)", () => { | |
| renderModal([{ source: "github:owner/my-plugin" }]); | |
| // When no repo_path, the whole repo is the plugin, show "owner/my-plugin" | |
| const elements = screen.getAllByText("owner/my-plugin"); | |
| expect(elements.length).toBeGreaterThan(0); | |
| }); | |
| it("should extract name from git URL", () => { | |
| renderModal([ | |
| { source: "https://github.com/owner/repo-name.git" }, | |
| ]); | |
| const elements = screen.getAllByText("repo-name"); | |
| expect(elements.length).toBeGreaterThan(0); | |
| }); | |
| it("should display full source when no special format", () => { | |
| renderModal([{ source: "local-plugin" }]); | |
| const elements = screen.getAllByText("local-plugin"); | |
| expect(elements.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| describe("Modal Title", () => { | |
| it("should show plugin name in title for single plugin", () => { | |
| renderModal([{ source: "github:owner/awesome-plugin" }]); | |
| // Title should include the plugin name - use getAllBy since text appears in multiple places | |
| const elements = screen.getAllByText(/owner\/awesome-plugin/); | |
| expect(elements.length).toBeGreaterThan(0); | |
| }); | |
| it("should show generic title for multiple plugins", () => { | |
| renderModal([ | |
| { source: "github:owner/plugin1" }, | |
| { source: "github:owner/plugin2" }, | |
| ]); | |
| // The h2 title contains both LAUNCH$MODAL_TITLE and LAUNCH$MODAL_TITLE_GENERIC | |
| const title = screen.getByRole("heading", { level: 2 }); | |
| expect(title.textContent).toContain("LAUNCH$MODAL_TITLE_GENERIC"); | |
| }); | |
| }); | |
| describe("Message Display", () => { | |
| it("should display message when provided", () => { | |
| renderModal([{ source: "github:owner/repo" }], { | |
| message: "This is a custom message", | |
| }); | |
| expect(screen.getByText("This is a custom message")).toBeInTheDocument(); | |
| }); | |
| it("should not render message element when not provided", () => { | |
| renderModal([{ source: "github:owner/repo" }]); | |
| // No message should be present | |
| const modal = screen.getByTestId("plugin-launch-modal"); | |
| expect(modal.querySelector("p.text-neutral-400")).not.toBeInTheDocument(); | |
| }); | |
| }); | |
| describe("Expandable Sections", () => { | |
| it("should expand plugin section by default when it has parameters", () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { apiKey: "test-key" }, | |
| }, | |
| ]); | |
| // Parameter input should be visible (section is expanded) | |
| expect(screen.getByTestId("plugin-0-param-apiKey")).toBeInTheDocument(); | |
| }); | |
| it("should collapse/expand section when clicking header", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { apiKey: "test-key" }, | |
| }, | |
| ]); | |
| // Initially expanded - parameter visible | |
| expect(screen.getByTestId("plugin-0-param-apiKey")).toBeInTheDocument(); | |
| // Click to collapse | |
| await user.click(screen.getByTestId("plugin-section-0")); | |
| // Parameter should be hidden | |
| await waitFor(() => { | |
| expect( | |
| screen.queryByTestId("plugin-0-param-apiKey"), | |
| ).not.toBeInTheDocument(); | |
| }); | |
| // Click to expand again | |
| await user.click(screen.getByTestId("plugin-section-0")); | |
| // Parameter should be visible again | |
| await waitFor(() => { | |
| expect(screen.getByTestId("plugin-0-param-apiKey")).toBeInTheDocument(); | |
| }); | |
| }); | |
| }); | |
| describe("Parameter Inputs", () => { | |
| it("should render text input for string parameters", () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { name: "default-name" }, | |
| }, | |
| ]); | |
| const input = screen.getByTestId("plugin-0-param-name"); | |
| expect(input).toHaveAttribute("type", "text"); | |
| expect(input).toHaveValue("default-name"); | |
| }); | |
| it("should render number input for number parameters", () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { count: 42 }, | |
| }, | |
| ]); | |
| const input = screen.getByTestId("plugin-0-param-count"); | |
| expect(input).toHaveAttribute("type", "number"); | |
| expect(input).toHaveValue(42); | |
| }); | |
| it("should render checkbox for boolean parameters", () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { enabled: true }, | |
| }, | |
| ]); | |
| const checkbox = screen.getByTestId("plugin-0-param-enabled"); | |
| expect(checkbox).toHaveAttribute("type", "checkbox"); | |
| expect(checkbox).toBeChecked(); | |
| }); | |
| it("should update string parameter value when typing", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { apiKey: "initial" }, | |
| }, | |
| ]); | |
| const input = screen.getByTestId("plugin-0-param-apiKey"); | |
| await user.clear(input); | |
| await user.type(input, "new-value"); | |
| expect(input).toHaveValue("new-value"); | |
| }); | |
| it("should update number parameter value when typing", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { count: 5 }, | |
| }, | |
| ]); | |
| const input = screen.getByTestId("plugin-0-param-count"); | |
| await user.clear(input); | |
| await user.type(input, "100"); | |
| expect(input).toHaveValue(100); | |
| }); | |
| it("should toggle boolean parameter when clicking checkbox", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| parameters: { debug: false }, | |
| }, | |
| ]); | |
| const checkbox = screen.getByTestId("plugin-0-param-debug"); | |
| expect(checkbox).not.toBeChecked(); | |
| await user.click(checkbox); | |
| expect(checkbox).toBeChecked(); | |
| }); | |
| }); | |
| describe("Ref and Path Display", () => { | |
| it("should display ref when provided", async () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| ref: "v1.2.3", | |
| parameters: { key: "value" }, | |
| }, | |
| ]); | |
| // Check the expanded section contains the ref value | |
| const modal = screen.getByTestId("plugin-launch-modal"); | |
| expect(modal.textContent).toContain("v1.2.3"); | |
| }); | |
| it("should display repo_path when provided", () => { | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| repo_path: "plugins/my-plugin", | |
| parameters: { key: "value" }, | |
| }, | |
| ]); | |
| // Check the expanded section contains the path value | |
| const modal = screen.getByTestId("plugin-launch-modal"); | |
| expect(modal.textContent).toContain("plugins/my-plugin"); | |
| }); | |
| }); | |
| describe("Plugins Without Parameters", () => { | |
| it("should show plugins list when all plugins have no parameters", () => { | |
| renderModal([ | |
| { source: "github:owner/plugin1" }, | |
| { source: "github:owner/plugin2" }, | |
| ]); | |
| expect(screen.getByText("LAUNCH$PLUGINS")).toBeInTheDocument(); | |
| // When no repo_path, the full repo path is shown (may appear multiple times) | |
| expect(screen.getAllByText("owner/plugin1").length).toBeGreaterThan(0); | |
| expect(screen.getAllByText("owner/plugin2").length).toBeGreaterThan(0); | |
| }); | |
| it("should show 'Additional Plugins' when mixing plugins with and without params", () => { | |
| renderModal([ | |
| { source: "github:owner/with-params", parameters: { key: "val" } }, | |
| { source: "github:owner/without-params" }, | |
| ]); | |
| expect(screen.getByText("LAUNCH$ADDITIONAL_PLUGINS")).toBeInTheDocument(); | |
| // When no repo_path, the full repo path is shown | |
| expect(screen.getAllByText("owner/without-params").length).toBeGreaterThan(0); | |
| }); | |
| it("should show ref in simple plugin list", () => { | |
| renderModal([ | |
| { source: "github:owner/plugin", ref: "main" }, | |
| ]); | |
| expect(screen.getByText("@ main")).toBeInTheDocument(); | |
| }); | |
| it("should show repo_path in simple plugin list", () => { | |
| renderModal([ | |
| { source: "github:owner/repo", repo_path: "plugins/city-weather" }, | |
| ]); | |
| // Should show the plugin name | |
| expect(screen.getByText("city-weather")).toBeInTheDocument(); | |
| // Should show the source info with path | |
| const modal = screen.getByTestId("plugin-launch-modal"); | |
| expect(modal.textContent).toContain("owner/repo"); | |
| expect(modal.textContent).toContain("plugins/city-weather"); | |
| }); | |
| }); | |
| describe("Action Buttons", () => { | |
| it("should call onClose when close button is clicked", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([{ source: "github:owner/repo" }]); | |
| await user.click(screen.getByTestId("close-button")); | |
| expect(mockOnClose).toHaveBeenCalledTimes(1); | |
| }); | |
| it("should call onStartConversation with updated plugins when start button is clicked", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { | |
| source: "github:owner/repo", | |
| ref: "main", | |
| parameters: { apiKey: "initial" }, | |
| }, | |
| ]); | |
| // Update the parameter | |
| const input = screen.getByTestId("plugin-0-param-apiKey"); | |
| await user.clear(input); | |
| await user.type(input, "updated-key"); | |
| // Check the trust checkbox first | |
| await user.click(screen.getByTestId("trust-checkbox")); | |
| // Click start | |
| await user.click(screen.getByTestId("start-conversation-button")); | |
| expect(mockOnStartConversation).toHaveBeenCalledTimes(1); | |
| const calledWithPlugins = mockOnStartConversation.mock.calls[0][0]; | |
| const calledWithMessage = mockOnStartConversation.mock.calls[0][1]; | |
| expect(calledWithPlugins[0].source).toBe("github:owner/repo"); | |
| expect(calledWithPlugins[0].ref).toBe("main"); | |
| expect(calledWithPlugins[0].parameters.apiKey).toBe("updated-key"); | |
| expect(calledWithMessage).toBeUndefined(); | |
| }); | |
| it("should call onStartConversation with message when provided", async () => { | |
| const user = userEvent.setup(); | |
| renderModal( | |
| [{ source: "github:owner/repo" }], | |
| { message: "/city-weather:now Tokyo" }, | |
| ); | |
| // Check the trust checkbox first | |
| await user.click(screen.getByTestId("trust-checkbox")); | |
| await user.click(screen.getByTestId("start-conversation-button")); | |
| expect(mockOnStartConversation).toHaveBeenCalledTimes(1); | |
| const calledWithPlugins = mockOnStartConversation.mock.calls[0][0]; | |
| const calledWithMessage = mockOnStartConversation.mock.calls[0][1]; | |
| expect(calledWithPlugins[0].source).toBe("github:owner/repo"); | |
| expect(calledWithMessage).toBe("/city-weather:now Tokyo"); | |
| }); | |
| it("should show 'Starting...' text when loading", () => { | |
| renderModal([{ source: "github:owner/repo" }], { isLoading: true }); | |
| expect(screen.getByText("LAUNCH$STARTING")).toBeInTheDocument(); | |
| }); | |
| it("should disable start button when loading", () => { | |
| renderModal([{ source: "github:owner/repo" }], { isLoading: true }); | |
| expect(screen.getByTestId("start-conversation-button")).toBeDisabled(); | |
| }); | |
| }); | |
| describe("Multiple Plugins with Parameters", () => { | |
| it("should render multiple expandable sections for plugins with parameters", () => { | |
| renderModal([ | |
| { source: "github:owner/plugin1", parameters: { key1: "val1" } }, | |
| { source: "github:owner/plugin2", parameters: { key2: "val2" } }, | |
| ]); | |
| expect(screen.getByTestId("plugin-section-0")).toBeInTheDocument(); | |
| expect(screen.getByTestId("plugin-section-1")).toBeInTheDocument(); | |
| }); | |
| it("should maintain separate state for each plugin's parameters", async () => { | |
| const user = userEvent.setup(); | |
| renderModal([ | |
| { source: "github:owner/plugin1", parameters: { key1: "val1" } }, | |
| { source: "github:owner/plugin2", parameters: { key2: "val2" } }, | |
| ]); | |
| // Update first plugin's parameter | |
| const input1 = screen.getByTestId("plugin-0-param-key1"); | |
| await user.clear(input1); | |
| await user.type(input1, "new-val1"); | |
| // Second plugin's parameter should be unchanged | |
| const input2 = screen.getByTestId("plugin-1-param-key2"); | |
| expect(input2).toHaveValue("val2"); | |
| // First plugin should have new value | |
| expect(input1).toHaveValue("new-val1"); | |
| }); | |
| }); | |
| }); | |