import React from "react"; import { fireEvent, render, screen, within, } from "@testing-library/react-native"; import { Pressable } from "react-native"; import type { TriggerRef } from "@rn-primitives/select"; import { SelectRoot } from "./SelectRoot"; import { SelectTrigger } from "./components/SelectTrigger"; import { SelectLabel } from "./components/SelectLabel"; import type { SelectRootProps } from "./SelectComposableTypes"; import { SelectPrimitive } from "../primitives/Select"; import { Text } from "../Text"; import { tokens } from "../utils/design"; function renderRoot(props: Partial = {}) { return render( , ); } describe("SelectRoot chrome", () => { it("shows the placeholder when a controlled value is cleared to undefined", () => { function ControlledSelect() { const [value, setValue] = React.useState< { value: string; label: string } | undefined >(); return ( <> setValue(undefined)} /> ); } render(); fireEvent.press(screen.getByTestId("toronto-option")); expect(screen.getByText("Toronto")).toBeDefined(); fireEvent.press(screen.getByTestId("clear-value")); expect(screen.queryByText("Toronto")).toBeNull(); expect(screen.getByText("Select an option")).toHaveStyle({ color: tokens["color-text--secondary"], }); }); it("preserves an uncontrolled default value across rerenders", () => { const { rerender } = renderRoot({ defaultValue: { value: "tor", label: "Toronto" }, }); rerender( , ); expect(screen.getByText("Toronto")).toBeDefined(); }); it("renders a composed label (visible, hidden from assistive tech)", () => { render( City , ); // The label renders but is hidden from a11y (the trigger carries the name). expect(screen.queryByText("City")).toBeNull(); expect( screen.getByText("City", { includeHiddenElements: true }), ).toBeDefined(); }); it("renders a string description as neutral helper text", () => { renderRoot({ description: "Pick your city" }); expect(screen.getByText("Pick your city")).toBeDefined(); // Neutral helper text has no alert icon. expect(screen.queryByTestId("alert")).toBeNull(); }); it("renders an error as critical helper text", () => { renderRoot({ error: "This field is required" }); expect(screen.getByText("This field is required")).toBeDefined(); expect(screen.getByTestId("alert")).toBeDefined(); }); it("gives error priority over description", () => { renderRoot({ description: "Pick your city", error: "Required" }); expect(screen.getByText("Required")).toBeDefined(); expect(screen.queryByText("Pick your city")).toBeNull(); }); it("treats an empty-string error as no error", () => { renderRoot({ error: "", description: "Pick your city" }); // No critical styling, no error helper — the description shows instead. expect(screen.getByTestId("ATL-Select-Trigger")).not.toHaveStyle({ borderColor: tokens["color-critical"], }); expect(screen.queryByTestId("alert")).toBeNull(); expect(screen.getByText("Pick your city")).toBeDefined(); }); it.each([ ["error", { error: "Required" }], ["status critical", { status: "critical" as const }], ["invalid", { invalid: true }], ])("applies critical trigger styling from %s", (_label, props) => { renderRoot(props); expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({ borderColor: tokens["color-critical"], }); }); it("makes the trigger read-only (no chevron) via the Root prop", () => { renderRoot({ readOnly: true }); expect(screen.getByTestId("ATL-Select-Trigger")).toHaveStyle({ backgroundColor: tokens["color-surface--background--subtle"], }); expect(screen.queryByTestId("arrowDown")).toBeNull(); }); it.each([false, true])( "uses a provided trigger testID verbatim (readOnly=%s)", readOnly => { render( , ); expect(screen.getByTestId("city")).toBeDefined(); expect(screen.queryByTestId("ATL-Select-Trigger")).toBeNull(); }, ); it("greys the value when disabled via the Root prop", () => { renderRoot({ disabled: true, defaultValue: { value: "tor", label: "Toronto" }, }); expect(screen.getByText("Toronto")).toHaveStyle({ color: tokens["color-disabled"], }); }); }); describe("SelectRoot label placement", () => { const hidden = { includeHiddenElements: true } as const; function renderComposed(props: Partial = {}) { return render( City , ); } it("places the label inside the trigger by default (labelPlacement inside)", () => { renderComposed(); const trigger = screen.getByTestId("ATL-Select-Trigger"); expect(within(trigger).getByText("City", hidden)).toBeDefined(); }); it("places the label above the trigger with labelPlacement='above'", () => { renderComposed({ labelPlacement: "above" }); // Rendered exactly once, and outside the trigger. expect(screen.getAllByText("City", hidden)).toHaveLength(1); const trigger = screen.getByTestId("ATL-Select-Trigger"); expect(within(trigger).queryByText("City", hidden)).toBeNull(); }); it("ignores where Select.Label is written — placement follows the prop", () => { render( {/* Written last, but still renders inside the trigger. */} City , ); const trigger = screen.getByTestId("ATL-Select-Trigger"); expect(within(trigger).getByText("City", hidden)).toBeDefined(); }); it("auto-publishes a string Select.Label as the trigger's accessible name", () => { renderComposed(); expect( screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel, ).toBe("City"); }); it("uses an explicit accessibilityLabel for non-string label content", () => { render( City , ); expect( screen.getByTestId("ATL-Select-Trigger").props.accessibilityLabel, ).toBe("City"); }); it("warns when more than one Select.Label is composed", () => { const warn = jest .spyOn(console, "warn") .mockImplementation(() => undefined); render( City Town , ); expect(warn).toHaveBeenCalledWith( expect.stringContaining("multiple "), ); warn.mockRestore(); }); }); describe("SelectTrigger ref", () => { it("forwards a ref exposing open/close", () => { const triggerRef = React.createRef(); render( , ); expect(triggerRef.current).not.toBeNull(); expect(typeof triggerRef.current?.open).toBe("function"); expect(typeof triggerRef.current?.close).toBe("function"); }); it("does not attach a ref when readOnly (no interactive trigger)", () => { const triggerRef = React.createRef(); render( , ); // readOnly mounts a static surface, so the ref stays null and an // optional-chained open()/close() is a safe no-op. expect(triggerRef.current).toBeNull(); expect(() => triggerRef.current?.open()).not.toThrow(); }); });