import React from "react"; import { render, screen, userEvent, within, } from "@testing-library/react-native"; import { Pressable } from "react-native"; import { darkTokens } from "@jobber/design"; import type { TriggerRef } from "@rn-primitives/select"; import { PortalHost } from "@rn-primitives/portal"; import { SelectPrimitive } from "./SelectPrimitive"; import { tokens } from "../../utils/design"; import { PortalHostWrapper } from "../../utils/test"; import { Icon } from "../../Icon"; import { AtlantisThemeContextProvider } from "../../AtlantisThemeContext"; const user = userEvent.setup(); beforeAll(() => { const MockNativeMethods = require("react-native/jest/MockNativeMethods"); const mockedMethods = MockNativeMethods.default ?? MockNativeMethods; mockedMethods.measure.mockImplementation( ( callback: ( x: number, y: number, width: number, height: number, pageX: number, pageY: number, ) => void, ) => { callback(0, 0, 320, 48, 20, 100); }, ); }); function renderSelect({ defaultValue, onOpenChange, disabled, invalid, testID, triggerRef, }: { defaultValue?: { value: string; label: string }; onOpenChange?: (open: boolean) => void; disabled?: boolean; invalid?: boolean; testID?: string; triggerRef?: React.Ref; } = {}) { return render( , ); } describe("SelectPrimitive.Root", () => { it("does not select an empty-string item when a controlled value is cleared", async () => { function ControlledSelect() { const [value, setValue] = React.useState< { value: string; label: string } | undefined >({ value: "tor", label: "Toronto" }); return ( <> setValue(undefined)} /> ); } render(, { wrapper: PortalHostWrapper }); await user.press(screen.getByTestId("clear-value")); await openSelect(); expect( screen.getByTestId("empty-option").props.accessibilityState, ).toMatchObject({ checked: false }); expect(screen.queryByTestId("checkmark")).toBeNull(); }); }); describe("SelectPrimitive.Trigger", () => { it("renders with the default testID", () => { renderSelect(); expect(screen.getByTestId("ATL-Select-Trigger")).toBeDefined(); }); it("uses a provided testID verbatim", () => { renderSelect({ testID: "city" }); expect(screen.getByTestId("city")).toBeDefined(); expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull(); }); it("fires onOpenChange and applies open styling when pressed", async () => { const onOpenChange = jest.fn(); renderSelect({ onOpenChange }); const trigger = screen.getByTestId("ATL-Select-Trigger"); expect(trigger).toHaveStyle({ borderColor: tokens["color-border--interactive"], }); await user.press(trigger); expect(onOpenChange).toHaveBeenCalledWith(true); expect(trigger).toHaveStyle({ borderColor: tokens["color-interactive--subtle"], borderWidth: tokens["border-thick"], }); }); it("does not open when disabled", async () => { const onOpenChange = jest.fn(); renderSelect({ onOpenChange, disabled: true }); await user.press(screen.getByTestId("ATL-Select-Trigger")); expect(onOpenChange).not.toHaveBeenCalled(); }); it("applies disabled styling: subtle fill and disabled value text", () => { renderSelect({ disabled: true }); expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({ backgroundColor: tokens["color-disabled--secondary"], }); expect(screen.getByText("Select an option")).toHaveStyle({ color: tokens["color-disabled"], }); }); it("applies critical border styling when invalid", () => { renderSelect({ invalid: true }); expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({ borderColor: tokens["color-critical"], }); }); it("exposes a combobox role and conveys disabled to the accessibility state", () => { renderSelect({ disabled: true }); const trigger = screen.getByRole("combobox"); expect(trigger.props.accessibilityState).toMatchObject({ disabled: true }); }); it("announces the selected option as the trigger's accessibility value", () => { renderSelect({ defaultValue: { value: "tor", label: "Toronto" } }); const trigger = screen.getByTestId("ATL-Select-Trigger"); expect(trigger.props.accessibilityValue).toEqual({ text: "Toronto" }); }); }); describe("SelectPrimitive.Trigger ref", () => { it("forwards a ref exposing the trigger with open/close", () => { const triggerRef = React.createRef(); renderSelect({ triggerRef }); expect(triggerRef.current).not.toBeNull(); expect(typeof triggerRef.current?.open).toBe("function"); expect(typeof triggerRef.current?.close).toBe("function"); }); }); const CITIES = [ { value: "tor", label: "Toronto" }, { value: "van", label: "Vancouver" }, { value: "edm", label: "Edmonton" }, ]; function renderSelectWithContent({ defaultValue, onValueChange, closeOnPress, disabledItem, pressedItem, theme, }: { defaultValue?: { value: string; label: string }; onValueChange?: (option?: { value: string; label: string }) => void; closeOnPress?: boolean; disabledItem?: string; pressedItem?: string; theme?: "dark"; } = {}) { const wrapper = theme === "dark" ? ({ children }: { readonly children: React.ReactNode }) => ( {children} ) : PortalHostWrapper; return render( Cities {CITIES.map(city => ( ))} , { wrapper }, ); } async function openSelect() { await user.press(screen.getByTestId("ATL-Select-Trigger")); } describe("SelectPrimitive.Content", () => { it("renders nothing until the trigger opens it", async () => { renderSelectWithContent(); expect(screen.queryByText("Toronto")).toBeNull(); await openSelect(); expect(screen.getByText("Toronto")).toBeDefined(); expect(screen.getByText("Vancouver")).toBeDefined(); expect(screen.getByText("Cities")).toBeDefined(); }); it("dismisses when tapping outside (overlay), without selecting", async () => { const onValueChange = jest.fn(); renderSelectWithContent({ onValueChange }); await openSelect(); await user.press(screen.getByTestId("ATL-Select-Overlay")); expect(screen.queryByText("Toronto")).toBeNull(); expect(onValueChange).not.toHaveBeenCalled(); }); it("uses the elevated dark surface style for content", async () => { renderSelectWithContent({ theme: "dark" }); await openSelect(); expect(screen.getByTestId("ATL-Select-Content")).toHaveStyle({ backgroundColor: darkTokens["color-surface--background--subtle"], }); }); }); describe("SelectPrimitive.Item", () => { it("uses the elevated dark surface active token when pressed", async () => { renderSelectWithContent({ theme: "dark", pressedItem: "tor" }); await openSelect(); expect(screen.getByTestId("ATL-Select-Item-tor")).toHaveStyle({ backgroundColor: darkTokens["color-surface--active"], }); }); it("selects the option, emits { value, label }, and closes", async () => { const onValueChange = jest.fn(); renderSelectWithContent({ onValueChange }); await openSelect(); await user.press(screen.getByText("Vancouver")); expect(onValueChange).toHaveBeenCalledWith({ value: "van", label: "Vancouver", }); // Dropdown closed; trigger now shows the selection. expect(screen.queryByText("Cities")).toBeNull(); expect(screen.getByText("Vancouver")).toBeDefined(); }); it("passes closeOnPress through — dropdown stays open when false", async () => { const onValueChange = jest.fn(); renderSelectWithContent({ onValueChange, closeOnPress: false }); await openSelect(); await user.press(screen.getByText("Vancouver")); expect(onValueChange).toHaveBeenCalled(); expect(screen.getByText("Cities")).toBeDefined(); }); it("does not select a disabled option", async () => { const onValueChange = jest.fn(); renderSelectWithContent({ onValueChange, disabledItem: "van" }); await openSelect(); await user.press(screen.getByText("Vancouver")); expect(onValueChange).not.toHaveBeenCalled(); }); it("greys a disabled option's text when ItemText is told it is disabled", async () => { renderSelectWithContent({ disabledItem: "van" }); await openSelect(); expect(screen.getByText("Vancouver")).toHaveStyle({ color: tokens["color-disabled"], }); expect(screen.getByText("Toronto")).toHaveStyle({ color: tokens["color-text"], }); }); it("shows the checkmark indicator only on the selected option", async () => { renderSelectWithContent({ defaultValue: { value: "tor", label: "Toronto" }, }); await openSelect(); // One checkmark icon in the dropdown (the selected row's indicator). expect(screen.getAllByTestId("checkmark")).toHaveLength(1); }); it("renders whatever marker is composed into ItemIndicator on the selected row", async () => { render( , { wrapper: PortalHostWrapper }, ); await openSelect(); expect(screen.getByTestId("person")).toBeDefined(); expect(screen.queryByTestId("checkmark")).toBeNull(); }); it("conveys option role plus selected and disabled states to assistive tech", async () => { renderSelectWithContent({ defaultValue: { value: "tor", label: "Toronto" }, disabledItem: "van", }); await openSelect(); const options = screen.getAllByRole("option"); const optionWith = (label: string) => options.find(option => within(option).queryByText(label)); expect(optionWith("Toronto")?.props.accessibilityState).toMatchObject({ checked: true, }); expect(optionWith("Vancouver")?.props.accessibilityState).toMatchObject({ disabled: true, }); }); }); describe("SelectPrimitive.Portal", () => { it("preserves the selected theme when the portal host is outside the provider", async () => { render( , { wrapper: ({ children }: { readonly children: React.ReactNode }) => ( <> {children} ), }, ); await openSelect(); expect(screen.getByTestId("ATL-Select-Content")).toHaveStyle({ backgroundColor: darkTokens["color-surface--background--subtle"], }); }); }); describe("SelectPrimitive.Value", () => { it("shows the placeholder with subdued styling when nothing is selected", () => { renderSelect(); const placeholder = screen.getByText("Select an option"); expect(placeholder).toHaveStyle({ color: tokens["color-text--secondary"], }); }); it("shows the selected option's label", () => { renderSelect({ defaultValue: { value: "tor", label: "Toronto" } }); const value = screen.getByText("Toronto"); expect(value).toHaveStyle({ color: tokens["color-text"] }); expect(screen.queryByText("Select an option")).toBeNull(); }); it("greys the value when the field is disabled (set on Root)", () => { renderSelect({ disabled: true, defaultValue: { value: "tor", label: "Toronto" }, }); expect(screen.getByText("Toronto")).toHaveStyle({ color: tokens["color-disabled"], }); }); });