I am writing a test for a Select
component using the shadcn/ui
package. This component has some parts (such as selectable items) that are rendered inside a Portal
. Because of this, I’m having trouble accessing and selecting items from the Portal
in my test. My expectation is that the test should click on the component, select an item with the text “Item 3”, and update the associated state correctly.
my Component:
const SelectComponent = forwardRef<ElementRef<typeof Root>, SelectProps>(
({ disabled, ...props }) => {
return (
<SelectContext.Provider value={{ disabled: disabled }}>
<Root {...props} disabled={disabled} />
</SelectContext.Provider>
);
}
);
SelectComponent.displayName = "SelectComponent";
const SelectTrigger = forwardRef<
ElementRef<typeof Trigger>,
ComponentPropsWithoutRef<typeof Trigger>
>(({ className, children, ...props }, ref) => {
const context = useContext(SelectContext);
return (
<Trigger
ref={ref}
className={cn(
"flex w-full items-center justify-between whitespace-nowrap p-4 pl-6 rounded-xl border border-input text-base focus:outline-none ",
context?.disabled
? "bg-gray-10 cursor-not-allowed opacity-50"
: "bg-transparent",
className
)}
dir="rtl"
disabled={context?.disabled}
data-testid="select"
{...props}
>
{children}
<Icon>
<ArrowDownIcon />
</Icon>
</Trigger>
);
});
SelectTrigger.displayName = Trigger.displayName;
const SelectScrollUpButton = forwardRef<
ElementRef<typeof ScrollUpButton>,
ComponentPropsWithoutRef<typeof ScrollUpButton>
>(({ className, ...props }, ref) => (
<ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1 ",
className
)}
{...props}
>
<ChevronUpIcon />
</ScrollUpButton>
));
SelectScrollUpButton.displayName = ScrollUpButton.displayName;
const SelectScrollDownButton = forwardRef<
ElementRef<typeof ScrollDownButton>,
ComponentPropsWithoutRef<typeof ScrollDownButton>
>(({ className, ...props }, ref) => (
<ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</ScrollDownButton>
));
SelectScrollDownButton.displayName = ScrollDownButton.displayName;
const SelectContent = forwardRef<
ElementRef<typeof Content>,
ComponentPropsWithoutRef<typeof Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<Portal data-testid="select-portal">
<Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
data-testid="select-content"
{...props}
>
<SelectScrollUpButton />
<Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</Viewport>
<SelectScrollDownButton />
</Content>
</Portal>
));
SelectContent.displayName = Content.displayName;
const SelectLabel = forwardRef<
ElementRef<typeof Label>,
ComponentPropsWithoutRef<typeof Label>
>(({ className, ...props }, ref) => (
<Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = Label.displayName;
const SelectItem = forwardRef<
ElementRef<typeof Item>,
ComponentPropsWithoutRef<typeof Item>
>(({ className, children, ...props }, ref) => (
<Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
dir="rtl"
data-testid="select-item"
{...props}
>
<span className="absolute left-5 flex h-3.5 w-3.5 items-center justify-center">
<ItemIndicator>
<CheckIcon className="h-4 w-4" />
</ItemIndicator>
</span>
<ItemText>{children}</ItemText>
</Item>
));
SelectItem.displayName = Item.displayName;
const SelectSeparator = forwardRef<
ElementRef<typeof Separator>,
ComponentPropsWithoutRef<typeof Separator>
>(({ className, ...props }, ref) => (
<Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = Separator.displayName;
type SelectComponentType = typeof SelectComponent & {
Item: typeof SelectItem;
Group: typeof SelectGroup;
Content: typeof SelectContent;
Trigger: typeof SelectTrigger;
Label: typeof SelectLabel;
Value: typeof SelectValue;
Separator: typeof SelectSeparator;
};
const Select = SelectComponent as SelectComponentType;
Select.Item = SelectItem;
Select.Group = SelectGroup;
Select.Content = SelectContent;
Select.Trigger = SelectTrigger;
Select.Label = SelectLabel;
Select.Value = SelectValue;
Select.Separator = SelectSeparator;
export default Select;
Here are the tests I’ve written so far:
const container = document.createElement("div");
document.body.appendChild(container);
const ComponentSample = () => {
const [selectedOption, setSelectedOption] = useState("1");
return (
<Component value={selectedOption} onValueChange={setSelectedOption}>
<Component.Trigger>{selectedOption}</Component.Trigger>
<Component.Content>
<Component.Item value="1">item 1</Component.Item>
<Component.Item value="2">item 2</Component.Item>
<Component.Item value="3">item 3</Component.Item>
<Component.Item value="4">item 4</Component.Item>
</Component.Content>
</Component>
);
};
const { getByTestId, getByText } = render(<ComponentSample />, {
container,
});
-
In my first test, I used
fireEvent
to simulate clicking on the trigger and selecting an item. However,getByText
is unable to find “Item 3” because it is rendered inside aPortal
.fireEvent.click(getByTestId("select")); fireEvent.click(getByText("item 3"));
-
In my second attempt, I used
waitFor
to wait for the items to render in the DOM, but still, “Item 3” is not accessible within the test scope.fireEvent.click(getByTestId("select")); await waitFor(() => getByText("item 3").click());
How can I properly find and select items rendered inside a Portal
in my tests? Is there a specific configuration or method in React Testing Library
or Vitest
that I need to apply?
Thank you in advance for your help! 🙏