mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
319 lines
10 KiB
TypeScript
319 lines
10 KiB
TypeScript
import React from "react"
|
|
import { render, screen, act, cleanup, waitFor, within, fireEvent } from "@/utils/test-utils"
|
|
import { SearchableSelect, SearchableSelectOption } from "@/components/ui/searchable-select"
|
|
import userEvent from "@testing-library/user-event"
|
|
|
|
describe("SearchableSelect", () => {
|
|
const mockOptions: SearchableSelectOption[] = [
|
|
{ value: "option1", label: "Option 1" },
|
|
{ value: "option2", label: "Option 2" },
|
|
{ value: "option3", label: "Option 3", disabled: true },
|
|
]
|
|
|
|
const defaultProps = {
|
|
options: mockOptions,
|
|
placeholder: "Select an option",
|
|
searchPlaceholder: "Search options...",
|
|
emptyMessage: "No options found",
|
|
onValueChange: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it("renders with placeholder when no value is selected", () => {
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
expect(screen.getByText("Select an option")).toBeInTheDocument()
|
|
})
|
|
|
|
it("renders with selected option label when value is provided", () => {
|
|
render(<SearchableSelect {...defaultProps} value="option1" />)
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument()
|
|
})
|
|
|
|
it("opens dropdown when clicked", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
expect(screen.getByPlaceholderText("Search options...")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 2")).toBeInTheDocument()
|
|
})
|
|
|
|
it("filters options based on search input", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
// Verify all options are initially visible
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 2")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 3")).toBeInTheDocument()
|
|
})
|
|
|
|
const searchInput = screen.getByPlaceholderText("Search options...")
|
|
|
|
// Use fireEvent for cmdk input
|
|
fireEvent.change(searchInput, { target: { value: "1" } })
|
|
|
|
// Wait for the filtering to take effect
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument()
|
|
expect(screen.queryByText("Option 2")).not.toBeInTheDocument()
|
|
expect(screen.queryByText("Option 3")).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it("calls onValueChange when an option is selected", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
const option = screen.getByText("Option 2")
|
|
await user.click(option)
|
|
|
|
expect(defaultProps.onValueChange).toHaveBeenCalledWith("option2")
|
|
})
|
|
|
|
it("does not call onValueChange when a disabled option is clicked", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
const disabledOption = screen.getByText("Option 3")
|
|
await user.click(disabledOption)
|
|
|
|
expect(defaultProps.onValueChange).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("clears search value when dropdown is closed", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
const searchInput = screen.getByPlaceholderText("Search options...")
|
|
|
|
// Use fireEvent for cmdk input
|
|
fireEvent.change(searchInput, { target: { value: "test" } })
|
|
|
|
// Verify the search filters the options
|
|
await waitFor(() => {
|
|
expect(screen.queryByText("Option 1")).not.toBeInTheDocument()
|
|
expect(screen.queryByText("Option 2")).not.toBeInTheDocument()
|
|
})
|
|
|
|
// Close the dropdown by clicking outside
|
|
await user.click(document.body)
|
|
|
|
// Wait for dropdown to close
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
|
|
})
|
|
|
|
// Wait a bit for the timeout to clear search
|
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
|
|
// Open again to check if search was cleared
|
|
await user.click(trigger)
|
|
|
|
// All options should be visible again
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 2")).toBeInTheDocument()
|
|
expect(screen.getByText("Option 3")).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it("handles component unmounting without memory leaks", async () => {
|
|
vi.useFakeTimers()
|
|
const { unmount, rerender } = render(<SearchableSelect {...defaultProps} value="option1" />)
|
|
|
|
// Change the value prop to trigger the effect with timeout
|
|
rerender(<SearchableSelect {...defaultProps} value="option2" />)
|
|
|
|
// Immediately unmount the component before the timeout completes
|
|
act(() => {
|
|
unmount()
|
|
})
|
|
|
|
// This test ensures that no setState calls happen after unmount
|
|
// If there was a memory leak, this would throw an error
|
|
expect(() => {
|
|
// Wait for any pending timeouts
|
|
act(() => {
|
|
vi.runAllTimers()
|
|
})
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it("cleans up timeouts on unmount", () => {
|
|
vi.useFakeTimers()
|
|
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout")
|
|
const { unmount } = render(<SearchableSelect {...defaultProps} />)
|
|
|
|
act(() => {
|
|
unmount()
|
|
})
|
|
|
|
// Verify that clearTimeout was called during cleanup
|
|
expect(clearTimeoutSpy).toHaveBeenCalled()
|
|
clearTimeoutSpy.mockRestore()
|
|
})
|
|
|
|
it("resets search value when value prop changes", async () => {
|
|
const user = userEvent.setup()
|
|
const { rerender } = render(<SearchableSelect {...defaultProps} value="option1" />)
|
|
|
|
// Open dropdown and type something
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
const searchInput = screen.getByPlaceholderText("Search options...")
|
|
fireEvent.change(searchInput, { target: { value: "2" } })
|
|
|
|
// Verify search is working - use within to scope to dropdown
|
|
const dropdown = screen.getByRole("dialog")
|
|
await waitFor(() => {
|
|
expect(within(dropdown).queryByText("Option 1")).not.toBeInTheDocument()
|
|
expect(within(dropdown).getByText("Option 2")).toBeInTheDocument()
|
|
})
|
|
|
|
// Close dropdown
|
|
await user.click(document.body)
|
|
|
|
// Change the value prop
|
|
rerender(<SearchableSelect {...defaultProps} value="option2" />)
|
|
|
|
// Wait for the component to update
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("combobox")).toHaveTextContent("Option 2")
|
|
})
|
|
|
|
// Wait for the effect to run (100ms timeout in component)
|
|
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
|
|
// Open dropdown again
|
|
await user.click(trigger)
|
|
|
|
// All options should be visible (search cleared) - use within to scope
|
|
const newDropdown = screen.getByRole("dialog")
|
|
await waitFor(() => {
|
|
expect(within(newDropdown).getByText("Option 1")).toBeInTheDocument()
|
|
expect(within(newDropdown).getByText("Option 2")).toBeInTheDocument()
|
|
expect(within(newDropdown).getByText("Option 3")).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it("handles rapid value changes without issues", async () => {
|
|
const { rerender } = render(<SearchableSelect {...defaultProps} value="option1" />)
|
|
|
|
// Rapidly change values
|
|
rerender(<SearchableSelect {...defaultProps} value="option2" />)
|
|
rerender(<SearchableSelect {...defaultProps} value="option3" />)
|
|
rerender(<SearchableSelect {...defaultProps} value="option1" />)
|
|
|
|
// Wait for the final value to be reflected
|
|
await waitFor(() => {
|
|
const trigger = screen.getByRole("combobox")
|
|
expect(trigger).toHaveTextContent("Option 1")
|
|
})
|
|
|
|
// Component should still be functional - open dropdown
|
|
const user = userEvent.setup()
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
// Should be able to search
|
|
const searchInput = screen.getByPlaceholderText("Search options...")
|
|
fireEvent.change(searchInput, { target: { value: "2" } })
|
|
|
|
// Check filtering works - use within to scope to dropdown
|
|
const dropdown = screen.getByRole("dialog")
|
|
await waitFor(() => {
|
|
expect(within(dropdown).getByText("Option 2")).toBeInTheDocument()
|
|
expect(within(dropdown).queryByText("Option 1")).not.toBeInTheDocument()
|
|
expect(within(dropdown).queryByText("Option 3")).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it("closes the dropdown when ESC key is pressed", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
// Open the dropdown
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
// Verify dropdown is open
|
|
expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
expect(screen.getByRole("listbox")).toBeInTheDocument()
|
|
|
|
// Press ESC key
|
|
fireEvent.keyDown(window, { key: "Escape" })
|
|
|
|
// Verify dropdown is closed
|
|
await waitFor(() => {
|
|
expect(trigger).toHaveAttribute("aria-expanded", "false")
|
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it("does not close the dropdown when ESC is pressed while dropdown is closed", async () => {
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
const trigger = screen.getByRole("combobox")
|
|
|
|
// Ensure dropdown is closed
|
|
expect(trigger).toHaveAttribute("aria-expanded", "false")
|
|
|
|
// Press ESC key
|
|
fireEvent.keyDown(window, { key: "Escape" })
|
|
|
|
// Verify dropdown remains closed
|
|
expect(trigger).toHaveAttribute("aria-expanded", "false")
|
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
|
})
|
|
|
|
it("prevents default and stops propagation when ESC is pressed", async () => {
|
|
const user = userEvent.setup()
|
|
render(<SearchableSelect {...defaultProps} />)
|
|
|
|
// Open the dropdown
|
|
const trigger = screen.getByRole("combobox")
|
|
await user.click(trigger)
|
|
|
|
// Create a mock event to track preventDefault and stopPropagation
|
|
const escapeEvent = new KeyboardEvent("keydown", {
|
|
key: "Escape",
|
|
bubbles: true,
|
|
cancelable: true,
|
|
})
|
|
const preventDefaultSpy = vi.spyOn(escapeEvent, "preventDefault")
|
|
const stopPropagationSpy = vi.spyOn(escapeEvent, "stopPropagation")
|
|
|
|
// Dispatch the event
|
|
window.dispatchEvent(escapeEvent)
|
|
|
|
// Verify preventDefault and stopPropagation were called
|
|
expect(preventDefaultSpy).toHaveBeenCalled()
|
|
expect(stopPropagationSpy).toHaveBeenCalled()
|
|
})
|
|
})
|