// @vitest-environment jsdom
import {
render,
screen,
cleanup,
renderHook,
waitFor,
} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import React, { useState } from "react"
import { afterEach, describe, expect, it, vi } from "vitest"
vi.hoisted(() => {
const g = global as any
g.__BACKEND_URL__ = "http://localhost:9000"
g.__AUTH_TYPE__ = "session"
g.__JWT_TOKEN_STORAGE_KEY__ = ""
})
import { Combobox } from "../combobox"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useComboboxData } from "../../../../hooks/use-combobox-data"
import { productTagsQueryKeys } from "../../../../hooks/api/tags"
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock("@medusajs/icons", () => ({
CheckMini: () =>
,
EllipseMiniSolid: () => ,
PlusMini: () => ,
TrianglesMini: () => ,
XMarkMini: () => ,
}))
class MockIntersectionObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
configurable: true,
value: MockIntersectionObserver,
})
afterEach(() => {
cleanup()
})
describe("Combobox", () => {
it("should allow typing in search input in controlled mode", async () => {
const user = userEvent.setup()
const ControlledCombobox = () => {
const [searchValue, setSearchValue] = useState("")
return (
)
}
render()
const input = screen.getByPlaceholderText("Search...")
await user.click(input)
await user.type(input, "App")
expect((input as HTMLInputElement).value).toBe("App")
})
it("should allow typing and local option filtering in uncontrolled mode", async () => {
const user = userEvent.setup()
render(
)
const input = screen.getByPlaceholderText("Search...")
await user.click(input)
await user.type(input, "App")
expect((input as HTMLInputElement).value).toBe("App")
// Apple should be visible, Banana should not be visible
expect(screen.queryByText("Apple")).not.toBeNull()
expect(screen.queryByText("Banana")).toBeNull()
})
it("should clear search value upon value selection", async () => {
const user = userEvent.setup()
const SelectionWrapper = () => {
const [value, setValue] = useState("")
const [searchValue, setSearchValue] = useState("")
return (
)
}
render()
const input = screen.getByPlaceholderText("Search...")
await user.click(input)
await user.type(input, "App")
expect((input as HTMLInputElement).value).toBe("App")
const option = screen.getByRole("option", { name: "Apple" })
await user.click(option)
// Selection should clear search input
expect((input as HTMLInputElement).value).toBe("")
})
it("should invalidate combobox queries when lists query key is invalidated", async () => {
const queryClientInstance = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const queryFnMock = vi.fn().mockResolvedValue({
product_tags: [{ id: "tag-1", value: "Tag 1" }],
offset: 0,
limit: 10,
count: 1,
})
const { result } = renderHook(
() =>
useComboboxData({
queryKey: productTagsQueryKeys.lists(),
queryFn: queryFnMock,
getOptions: (data: any) =>
data.product_tags.map((tag: any) => ({
label: tag.value,
value: tag.id,
})),
}),
{ wrapper }
)
// Wait for the query to load options
await waitFor(() => {
expect(result.current.options).toHaveLength(1)
})
expect(result.current.options[0].label).toBe("Tag 1")
expect(queryFnMock).toHaveBeenCalledTimes(1)
// Now, simulate a mutation by invalidating lists query key
queryClientInstance.invalidateQueries({
queryKey: productTagsQueryKeys.lists(),
})
// It should trigger a refetch because the infinite query key starts with productTagsQueryKeys.lists()
await waitFor(() => {
expect(queryFnMock).toHaveBeenCalledTimes(2)
})
})
})