import { expect, userEvent, within } from "storybook/test"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { type ReactNode, useRef } from "react"; import type { TextInput as RNTextInput } from "react-native"; import { InputText } from "../inputs/InputText"; import { Story } from "../story-components/Story"; import { FormItem } from "./FormItem"; function NameField({ details }: { details?: string }): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } function RequiredField(): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } function ErrorField({ details }: { details?: string }): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } function RequiredEmptyErrorField(): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } function RequiredOtherErrorField(): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } function IndentedFormItem(): ReactNode { const ref = useRef(null); return ( ( )} onLabelPress={() => ref.current?.focus()} /> ); } type ThisStory = StoryObj; export default { title: "alouette/Forms/FormItem", component: FormItem, parameters: { componentSubtitle: "Label, error message and layout for a single form field — form-library agnostic.", }, } satisfies Meta; export const Variants: ThisStory = { render: () => ( ), }; export const Tests: ThisStory = { name: "FormItem Tests", render: () => ( ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const nameInput = canvas.getByLabelText("Name"); const input = canvas.getByLabelText("Password"); const emailInput = canvas.getByLabelText("Email"); await expect(input).toBeInTheDocument(); const message = canvas.getByText("Password is required."); await expect(message).toBeVisible(); // The input announces the state the label and message show: invalid, // required, and described by the message. await expect(input).toHaveAttribute("aria-invalid", "true"); await expect(input).toHaveAttribute("aria-required", "true"); await expect(input).toHaveAttribute("aria-describedby", message.id); await expect(nameInput).toHaveAttribute("aria-invalid", "false"); await expect(nameInput).toHaveAttribute("aria-required", "false"); // `details` is a description too, before the error. await expect(nameInput).toHaveAttribute( "aria-describedby", canvas.getByText("Enter your full name.").id, ); await expect(emailInput).toHaveAttribute( "aria-describedby", `${canvas.getByText("We only use it to sign you in.").id} ${canvas.getByText("Enter a valid email address.").id}`, ); // The field in error sits in the danger scope, like its message: its // border token resolves to the danger value, not the neutral one. const outlinedToken = (element: Element) => getComputedStyle(element).getPropertyValue( "--color-interactive-outlined-pressable", ); await expect(outlinedToken(input)).toBe(outlinedToken(message)); await expect(outlinedToken(input)).not.toBe(outlinedToken(nameInput)); await expect(getComputedStyle(input).borderTopColor).not.toBe( getComputedStyle(nameInput).borderTopColor, ); // Pressing the label focuses the input, like a native